diff --git a/packages/main/cypress/specs/TableVirtualizer.cy.ts b/packages/main/cypress/specs/TableVirtualizer.cy.ts new file mode 100644 index 000000000000..7a9ee167d6ba --- /dev/null +++ b/packages/main/cypress/specs/TableVirtualizer.cy.ts @@ -0,0 +1,239 @@ +import { html } from "lit"; +import "../../src/Table.js"; +import "../../src/TableHeaderRow.js"; +import "../../src/TableHeaderCell.js"; +import "../../src/TableRow.js"; +import "../../src/TableCell.js"; +import "../../src/TableVirtualizer.js"; + +import type Table from "../../src/Table.js"; +import type TableVirtualizer from "../../src/TableVirtualizer.js"; +import type { RangeChangeEventDetail } from "../../src/TableVirtualizer.js"; + +describe("TableVirtualizer", () => { + function mountTable(rowHeight = 50, rowCount = 100, tableHeight = 250) { + cy.mount(html` + + + + + `); + + cy.get("[ui5-table-virtualizer]").as("virtualizer").then($virtualizer => { + $virtualizer[0].addEventListener("range-change", updateRows as EventListener); + $virtualizer[0].reset(); + }); + + cy.get("[ui5-table]").shadow().find("#table").as("innerTable"); + cy.get("[ui5-table]").children("ui5-table-row").as("rows"); + cy.get("@rows").first().as("firstRow"); + cy.get("@rows").last().as("lastRow"); + } + + function updateRows(e: CustomEvent) { + const table = document.querySelector("ui5-table") as Table; + for (let i = e.detail.first; i < e.detail.last; i++) { + const row = table.rows[i - e.detail.first]; + if (row) { + row.position = i; + row.cells[0].textContent = `${i}`; + } else { + const newRow = `${i}`; + table.insertAdjacentHTML("beforeend", newRow); + } + } + } + + function testRows(start: number, end: number) { + for (let i = start; i < end; i++) { + cy.get("[ui5-table-row]") + .eq(i - start) + .should("have.attr", "position", `${i}`) + .should("have.attr", "aria-rowindex", `${i + 1}`) + .find("ui5-table-cell") + .should("have.text", `${i}`); + } + + cy.get("[ui5-table]") + .shadow() + .find("#spacer") + .then($spacer => { + const transform = getComputedStyle($spacer[0]).transform; + return new DOMMatrix(transform); + }) + .its("f") + .should("equal", start * 50); + } + + describe("Rendering", () => { + it("should render initially 5 rows", () => { + mountTable(); + + cy.get("@innerTable") + .should("have.attr", "aria-rowcount", "100") + .should("have.css", "overflow-y", "auto") + .then($innerTable => window.getComputedStyle($innerTable[0])) + .invoke("getPropertyValue", "--row-height") + .and("equal", "50px"); + + cy.get("@innerTable") + .children("#rows") + .should("have.css", "min-height", "5000px"); + + cy.get("@innerTable") + .find("#spacer") + .should("have.css", "will-change", "transform"); + + testRows(0, 5); + }); + + it("should react to rowHeight changes", () => { + mountTable(); + + cy.get("@virtualizer").invoke("attr", "row-height", "60"); + + cy.get("@innerTable") + .then($innerTable => window.getComputedStyle($innerTable[0])) + .invoke("getPropertyValue", "--row-height") + .and("equal", "60px"); + + cy.get("@innerTable") + .children("#rows") + .should("have.css", "min-height", "6000px"); + }); + + it("should react to rowCount changes", () => { + mountTable(); + + cy.get("@virtualizer").invoke("attr", "row-count", "200"); + + cy.get("@innerTable") + .should("have.attr", "aria-rowcount", "200") + .children("#rows") + .should("have.css", "min-height", "10000px"); + }); + + it("should update rows on scroll", () => { + mountTable(); + + cy.get("@innerTable").scrollTo(0, 250); + testRows(5, 10); + + cy.get("@innerTable").scrollTo("bottom"); + testRows(95, 100); + + cy.get("@innerTable").scrollTo(0, 4000); + testRows(80, 85); + + cy.get("@innerTable").scrollTo("top"); + testRows(0, 5); + }); + + it("should update rows via keyboard while focus is on the row", () => { + mountTable(); + + cy.get("@firstRow").realClick(); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("PageDown"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("PageDown"); + testRows(5, 10); + + cy.realPress("ArrowDown"); + testRows(6, 11); + + cy.realPress("PageUp"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("PageUp"); + testRows(1, 6); + + cy.realPress("ArrowUp"); + testRows(0, 5); + + cy.realPress("End"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("End"); + testRows(95, 100); + + cy.realPress("Home"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("Home"); + testRows(0, 5); + }); + + it("should update rows via keyboard while focus is on the cell", () => { + mountTable(); + + cy.get("@firstRow").find("ui5-table-cell").first().as("firstRowFirstCell"); + cy.get("@lastRow").find("ui5-table-cell").first().as("lastRowFirstCell"); + + cy.get("@firstRow").realClick().realPress("ArrowRight"); + cy.get("@firstRowFirstCell").should("have.focus"); + + cy.realPress("PageDown"); + cy.get("@lastRowFirstCell").should("have.focus"); + + cy.realPress("PageDown"); + testRows(5, 10); + + cy.realPress("ArrowDown"); + testRows(6, 11); + + cy.realPress("PageUp"); + cy.get("@firstRowFirstCell").should("have.focus"); + + cy.realPress("PageUp"); + testRows(1, 6); + + cy.realPress("ArrowUp"); + testRows(0, 5); + + cy.realPress("End"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("End"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("End"); + testRows(95, 100); + + cy.get("@lastRow").realPress("ArrowRight"); + cy.get("@lastRowFirstCell").should("have.focus"); + + cy.realPress("Home"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("Home"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("Home"); + testRows(0, 5); + }); + + it("should have the reset() API", () => { + mountTable(); + + cy.get("@virtualizer").then($virtualizer => { + $virtualizer[0].addEventListener("range-change", cy.stub().as("rangeChange")); + }); + + cy.get("@innerTable").scrollTo("bottom"); + cy.get("@rangeChange").should("have.been.calledOnce"); + testRows(95, 100); + + cy.get("@virtualizer").invoke("get", 0).invoke("reset"); + cy.get("@rangeChange").should("have.been.calledTwice"); + testRows(0, 5); + + cy.get("@virtualizer").invoke("get", 0).invoke("reset"); + cy.get("@rangeChange").should("have.been.calledThrice"); + }); + }); +}); diff --git a/packages/main/src/Table.hbs b/packages/main/src/Table.hbs index 554bb45e9539..4688ba22e56c 100644 --- a/packages/main/src/Table.hbs +++ b/packages/main/src/Table.hbs @@ -3,10 +3,15 @@
- +
+
+ +
+
{{#unless rows.length}} diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index 4ef5af1314a3..6fc906a5dc15 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -25,6 +25,7 @@ import { import BusyIndicator from "./BusyIndicator.js"; import TableCell from "./TableCell.js"; import { findVerticalScrollContainer, scrollElementIntoView, isFeature } from "./TableUtils.js"; +import type TableVirtualizer from "./TableVirtualizer.js"; /** * Interface for components that can be slotted inside the features slot of the ui5-table. @@ -42,7 +43,7 @@ interface ITableFeature extends UI5Element { /** * Called when the table finished rendering. */ - onTableRendered?(): void; + onTableAfterRendering?(): void; } /** @@ -195,7 +196,7 @@ class Table extends UI5Element { type: HTMLElement, "default": true, invalidateOnChildChange: { - properties: ["navigated"], + properties: ["navigated", "position"], slots: false, }, }) @@ -347,13 +348,17 @@ class Table extends UI5Element { } onAfterRendering(): void { - this.features.forEach(feature => feature.onTableRendered?.()); + this.features.forEach(feature => feature.onTableAfterRendering?.()); } _getSelection(): TableSelection | undefined { return this.features.find(feature => isFeature(feature, "TableSelection")) as TableSelection; } + _getVirtualizer(): TableVirtualizer | undefined { + return this.features.find(feature => isFeature(feature, "TableVirtualizer")) as TableVirtualizer; + } + _onEvent(e: Event) { const composedPath = e.composedPath(); const eventOrigin = composedPath[0] as HTMLElement; @@ -404,6 +409,10 @@ class Table extends UI5Element { } _onfocusin(e: FocusEvent) { + if (e.target === this) { + return; + } + // Handles focus in the table, when the focus is below a sticky element scrollElementIntoView(this._scrollContainer, e.target as HTMLElement, this._stickyElements, this.effectiveDir === "rtl"); } @@ -452,7 +461,7 @@ class Table extends UI5Element { } _isFeature(feature: any) { - return Boolean(feature.onTableActivate && feature.onTableRendered); + return Boolean(feature.onTableActivate && feature.onTableAfterRendering); } _isGrowingFeature(feature: any) { @@ -464,6 +473,7 @@ class Table extends UI5Element { } get styles() { + const virtualizer = this._getVirtualizer(); const headerStyleMap = this.headerRow?.[0]?.cells?.reduce((headerStyles, headerCell) => { if (headerCell.horizontalAlign !== undefined && !headerCell._popin) { headerStyles[`--horizontal-align-${headerCell._individualSlot}`] = headerCell.horizontalAlign; @@ -473,8 +483,13 @@ class Table extends UI5Element { return { table: { "grid-template-columns": this._gridTemplateColumns, + "--row-height": virtualizer ? `${virtualizer.rowHeight}px` : "auto", ...headerStyleMap, }, + spacer: { + "transform": this._spacerTransform, + "will-change": this._getVirtualizer() ? "transform" : undefined, + }, }; } @@ -501,6 +516,16 @@ class Table extends UI5Element { return widths.join(" "); } + get _spacerTransform() { + const firstRow = this.rows[0]; + const virtualizer = this._getVirtualizer(); + + if (virtualizer && firstRow && firstRow.position > -1) { + const transform = firstRow.position * virtualizer.rowHeight; + return `translateY(${transform}px)`; + } + } + get _tableOverflowX() { return (this.overflowMode === TableOverflowMode.Popin) ? "clip" : "auto"; } @@ -537,6 +562,10 @@ class Table extends UI5Element { return getEffectiveAriaLabelText(this) || undefined; } + get _ariaRowCount() { + return this._getVirtualizer()?.rowCount || undefined; + } + get _ariaMultiSelectable() { const selection = this._getSelection(); return (selection?.isSelectable() && this.rows.length) ? selection.isMultiSelect() : undefined; @@ -558,7 +587,7 @@ class Table extends UI5Element { } get _scrollContainer() { - return findVerticalScrollContainer(this._tableElement); + return findVerticalScrollContainer(this); } get isTable() { diff --git a/packages/main/src/TableGrowing.ts b/packages/main/src/TableGrowing.ts index 60edcf93e1b0..26c3e339e414 100644 --- a/packages/main/src/TableGrowing.ts +++ b/packages/main/src/TableGrowing.ts @@ -148,7 +148,7 @@ class TableGrowing extends UI5Element implements ITableGrowing { this._shouldFocusRow = false; } - onTableRendered(): void { + onTableAfterRendering(): void { // Focus the first row after growing, when the growing button is used if (this._shouldFocusRow) { this._shouldFocusRow = false; diff --git a/packages/main/src/TableNavigation.ts b/packages/main/src/TableNavigation.ts index afda84d60e37..d963d33b150b 100644 --- a/packages/main/src/TableNavigation.ts +++ b/packages/main/src/TableNavigation.ts @@ -115,7 +115,11 @@ class TableNavigation extends TableExtension { } this._ignoreFocusIn = ignoreFocusIn; - element.focus(); + if (element === this._table._beforeElement || element === this._table._afterElement) { + element.focus({ preventScroll: true }); + } else { + element.focus(); + } if (element instanceof HTMLInputElement) { element.select(); } @@ -210,6 +214,11 @@ class TableNavigation extends TableExtension { this._gridWalker.setCurrent(eventOrigin); } + this._table._getVirtualizer()?._onKeyDown(e); + if (e.defaultPrevented) { + return; + } + const keydownHandlerName = `_handle${e.code}` as keyof TableNavigation; const keydownHandler = this[keydownHandlerName] as (e: KeyboardEvent, eventOrigin: HTMLElement) => void | false; if (typeof keydownHandler === "function" && keydownHandler.call(this, e, eventOrigin) === undefined) { @@ -284,6 +293,7 @@ class TableNavigation extends TableExtension { if (this._table.loading) { this._table._loadingElement.focus(); } else { + this._getNavigationItemsOfGrid(); this._gridWalker.setColPos(0); this._focusCurrentItem(); } diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index 9a02f6043209..cfafe418a377 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -60,6 +60,15 @@ class TableRow extends TableRowBase { @property() rowKey = ""; + /** + * Defines the position of the row respect to the total number of rows within the table when the ui5-table-virtualizer feature is used. + * + * @default -1 + * @public + */ + @property({ type: Number }) + position = -1; + /** * Defines the interactive state of the row. * @@ -84,6 +93,9 @@ class TableRow extends TableRowBase { onBeforeRendering() { super.onBeforeRendering(); this.toggleAttribute("_interactive", this._isInteractive); + if (this.position !== -1) { + this.setAttribute("aria-rowindex", `${this.position + 1}`); + } if (this._renderNavigated && this.navigated) { this.setAttribute("aria-current", "true"); } else { diff --git a/packages/main/src/TableUtils.ts b/packages/main/src/TableUtils.ts index 21f8d0089928..4b93441f787a 100644 --- a/packages/main/src/TableUtils.ts +++ b/packages/main/src/TableUtils.ts @@ -77,6 +77,16 @@ const isFeature = (element: any, identifier: string): element is T => { return element.identifier === identifier; }; +const throttle = (callback: () => void) => { + let timer: number; + return () => { + cancelAnimationFrame(timer); + timer = requestAnimationFrame(() => { + callback(); + }); + }; +}; + export { isInstanceOfTable, isSelectionCheckbox, @@ -85,4 +95,5 @@ export { findVerticalScrollContainer, scrollElementIntoView, isFeature, + throttle, }; diff --git a/packages/main/src/TableVirtualizer.ts b/packages/main/src/TableVirtualizer.ts new file mode 100644 index 000000000000..ebb67be75e17 --- /dev/null +++ b/packages/main/src/TableVirtualizer.ts @@ -0,0 +1,279 @@ +/* eslint-disable no-bitwise */ +import { + isUp, + isUpShift, + isDown, + isDownShift, + isPageUp, + isPageDown, + isHome, + isEnd, + isTabNext, + isTabPrevious, +} from "@ui5/webcomponents-base/dist/Keys.js"; +import UI5Element, { type InvalidationInfo } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event.js"; +import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; +import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js"; +import { throttle } from "./TableUtils.js"; +import type { ITableFeature } from "./Table.js"; +import type Table from "./Table.js"; + +enum TabBlocking { + None = 0, + Next = 1, + Previous = 2, + Released = 4, +} + +/** + * Fired when the virtualizer is changed by user interaction e.g. on scrolling. + * @param {first} number The 0-based index of the first children currently rendered + * @param {last} number The 0-based index of the last children currently rendered + * @public + */ +type RangeChangeEventDetail = { + first: number, + last: number, +}; + +/** + * @class + * + * ### Overview + * + * The `ui5-table-virtualizer` component is used inside the `ui5-table` to virtualize the table rows. + * It is responsible for rendering only the rows that are visible in the viewport and updating them on scroll. + * This allows large numbers of rows to exist, but maintain high performance by only paying the cost for those that are currently visible. + * + * Note: The maximum number of virtualized rows is limited by browser constraints, specifically the maximum supported height for a DOM element. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents/dist/TableVirtualizer.js";` + * + * @constructor + * @extends UI5Element + * @public + * @experimental This component is not intended to be used in a productive enviroment. The API is under development and may be changed in the future. + */ +@customElement({ tag: "ui5-table-virtualizer" }) + +/** + * Fired when the virtualizer is changed by user interaction e.g. on scrolling. + * + * @public + */ +@event("range-change", { + detail: { + /** + * The 0-based index of the first children currently rendered. + * @public + */ + first: { type: Number }, + /** + * The 0-based index of the last children currently rendered. + * @public + */ + last: { type: Number }, + }, +}) + +class TableVirtualizer extends UI5Element implements ITableFeature { + /** + * Defines the height of the rows in the table. + * + * Note: This property is mandatory for the virtualization to work properly. + * + * @default 45 + * @public + */ + @property({ type: Number }) + rowHeight = 45; + + /** + * Defines the total count of rows in the table. + * + * Note: This property is mandatory for the virtualization to work properly. + * + * @default 100 + * @public + */ + @property({ type: Number }) + rowCount = 100; + + /** + * Defines the count of extra rows to be rendered in the table. + * + * @default 0 + * @public + */ + @property({ type: Number }) + extraRows = 0; + + readonly identifier = "TableVirtualizer"; + + _table?: Table; + _lastRowPosition: number = 0; + _firstRowPosition: number = 0; + _visibleRowCount: number = 0; + _tabBlockingState: TabBlocking = TabBlocking.None; + _onRowInvalidateBound: (invalidationInfo: InvalidationInfo) => void; + _onScrollBound: () => void; + + constructor() { + super(); + this._onScrollBound = throttle(this._onScroll.bind(this)); + this._onRowInvalidateBound = this._onRowInvalidate.bind(this); + } + + onTableActivate(table: Table): void { + this._table = table; + this._updateRowsHeight(); + this._scrollContainer.addEventListener("scroll", this._onScrollBound, { passive: true }); + this._onScroll(); + } + + onAfterRendering(): void { + this._table && this._table._invalidate++; + } + + onTableAfterRendering(): void { + if (!this._table) { + return; + } + + this._updateRowsHeight(); + if (this._tabBlockingState & TabBlocking.Released) { + const tabBlockingRow = this._table.rows.at(this._tabBlockingState & TabBlocking.Next ? -1 : 0) as HTMLElement; + const tabForwardingElement = getTabbableElements(tabBlockingRow).at(this._tabBlockingState & TabBlocking.Next ? 0 : -1); + this._tabBlockingState = TabBlocking.None; + (tabForwardingElement || tabBlockingRow).focus(); + } + } + + onExitDOM(): void { + this._scrollContainer.removeEventListener("scroll", this._onScrollBound); + this._table = undefined; + } + + /** + * Resets the virtualizer to its initial state and triggers the range-change event. + * @public + */ + reset(): void { + this._lastRowPosition = -1; + this._firstRowPosition = -1; + if (this._scrollContainer.scrollTop > 0) { + this._scrollContainer.scrollTop = 0; + } else { + this._onScroll(); + } + } + + get _scrollContainer() { + return this._table!._tableElement; + } + + get _rowsContainer() { + return this._table!.shadowRoot!.getElementById("rows")!; + } + + _onScroll(): void { + if (!this._table) { + return; + } + + const scrollTop = this._scrollContainer.scrollTop; + const fixHeight = this._table.headerRow[0].offsetHeight; + const scrollableHeight = this._scrollContainer.clientHeight - fixHeight; + this._visibleRowCount = Math.ceil(scrollableHeight / this.rowHeight); + + let firstRowPosition = Math.floor(scrollTop / this.rowHeight) - this.extraRows; + firstRowPosition = Math.max(0, firstRowPosition); + + let lastRowPosition = Math.max(0, firstRowPosition + this._visibleRowCount + 2 * this.extraRows); + lastRowPosition = Math.min(lastRowPosition, this.rowCount); + + if (this._firstRowPosition === firstRowPosition && this._lastRowPosition === lastRowPosition) { + return; + } + + this._lastRowPosition = lastRowPosition; + this._firstRowPosition = firstRowPosition; + this.fireDecoratorEvent("range-change", { + first: firstRowPosition, + last: lastRowPosition, + }); + } + + _updateRowsHeight() { + const rowsHeight = this.rowCount * this.rowHeight; + this._rowsContainer.style.minHeight = `${rowsHeight}px`; + } + + _onRowInvalidate(invalidationInfo: InvalidationInfo) { + if (invalidationInfo.name === "position") { + invalidationInfo.target.detachInvalidate(this._onRowInvalidateBound); + this._tabBlockingState |= TabBlocking.Released; + } + } + + _onKeyDown(e: KeyboardEvent) { + if (!this._table) { + return; + } + + let scrollTopChange = 0; + const rows = this._table.rows; + const firstRow = rows[0]; + const lastRow = rows[rows.length - 1]; + const hasDataBeforeFirstRow = firstRow.position !== 0; + const hasDataAfterLastRow = lastRow.position !== this.rowCount - 1; + const tableNavigation = this._table._tableNavigation!; + const activeElement = getActiveElement() as HTMLElement; + + if (isTabNext(e) && hasDataAfterLastRow && getTabbableElements(this._rowsContainer).pop() === activeElement) { + this._tabBlockingState = TabBlocking.Next; + lastRow.attachInvalidate(this._onRowInvalidateBound); + scrollTopChange = this.rowHeight; + } else if (isTabPrevious(e) && hasDataBeforeFirstRow && getTabbableElements(this._rowsContainer).shift() === activeElement) { + this._tabBlockingState = TabBlocking.Previous; + firstRow.attachInvalidate(this._onRowInvalidateBound); + scrollTopChange = this.rowHeight * -1; + } else if (hasDataAfterLastRow && tableNavigation._getNavigationItemsOfRow(lastRow).includes(activeElement)) { + if (isDown(e) || isDownShift(e)) { + scrollTopChange = this.rowHeight; + } else if (isPageDown(e)) { + scrollTopChange = this._visibleRowCount * this.rowHeight; + } else if (isEnd(e) && activeElement === lastRow) { + scrollTopChange = this.rowCount * this.rowHeight; + } + } else if (hasDataBeforeFirstRow && tableNavigation._getNavigationItemsOfRow(firstRow).includes(activeElement)) { + if (isUp(e) || isUpShift(e)) { + scrollTopChange = this.rowHeight * -1; + } else if (isPageUp(e)) { + scrollTopChange = this._visibleRowCount * this.rowHeight * -1; + } else if (isHome(e) && activeElement === firstRow) { + scrollTopChange = this.rowCount * this.rowHeight * -1; + } + } + + if (scrollTopChange) { + const scrollTop = this._table.scrollTop; + this._scrollContainer.scrollTop += scrollTopChange; + if (this._scrollContainer.scrollTop !== scrollTop) { + e.preventDefault(); + } + } + } +} + +TableVirtualizer.define(); + +export default TableVirtualizer; + +export type { + RangeChangeEventDetail, +}; diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts index 8f67245661ac..2546061c0488 100644 --- a/packages/main/src/bundle.esm.ts +++ b/packages/main/src/bundle.esm.ts @@ -56,6 +56,7 @@ import TableHeaderCell from "./TableHeaderCell.js"; import TableHeaderRow from "./TableHeaderRow.js"; import TableGrowing from "./TableGrowing.js"; import TableSelection from "./TableSelection.js"; +import TableVirtualizer from "./TableVirtualizer.js"; import Icon from "./Icon.js"; import Input from "./Input.js"; import SuggestionItemCustom from "./SuggestionItemCustom.js"; diff --git a/packages/main/src/themes/Table.css b/packages/main/src/themes/Table.css index 7bb0d299645f..e98560db5ccb 100644 --- a/packages/main/src/themes/Table.css +++ b/packages/main/src/themes/Table.css @@ -13,7 +13,20 @@ display: grid; grid-auto-rows: minmax(min-content, auto); background: var(--sapList_Background); - height: -moz-fill, -webkit-fill-available, 100%; +} + +:host([overflow-mode="Scroll"]) #table { + overflow-x: auto; + height: 100%; + height: -webkit-fill-available; + height: fill-available; +} + +#rows, #spacer { + display: grid; + grid-template-rows: min-content; + grid-template-columns: subgrid; + grid-column: 1 / -1; } #nodata-cell { diff --git a/packages/main/src/themes/TableCellBase.css b/packages/main/src/themes/TableCellBase.css index 19345e623998..df6912f2afee 100644 --- a/packages/main/src/themes/TableCellBase.css +++ b/packages/main/src/themes/TableCellBase.css @@ -10,7 +10,7 @@ box-sizing: border-box; } -:host(:focus) { +:host([tabindex]:focus) { outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); outline-offset: calc(-1 * var(--sapContent_FocusWidth)); } diff --git a/packages/main/src/themes/TableRow.css b/packages/main/src/themes/TableRow.css index 1d4257ffe777..a2bcd93fb63a 100644 --- a/packages/main/src/themes/TableRow.css +++ b/packages/main/src/themes/TableRow.css @@ -2,6 +2,11 @@ background: var(--sapList_Background); } +:host([position]) { + height: var(--row-height); + overflow: clip; +} + :host([aria-selected=true]) { background-color: var(--sapList_SelectionBackgroundColor); border-bottom: var(--sapList_BorderWidth) solid var(--sapList_SelectionBorderColor); diff --git a/packages/main/test/pages/TableVirtualizer.html b/packages/main/test/pages/TableVirtualizer.html new file mode 100644 index 000000000000..5c3506881670 --- /dev/null +++ b/packages/main/test/pages/TableVirtualizer.html @@ -0,0 +1,99 @@ + + + + + + + Table (in development) + + + + + + + + + + + + My Selectable Products (1000) + + + + + + + + Product + Supplier + Dimensions + Weight + Price + + + + + + + + \ No newline at end of file