diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index 8ea5bce6b201..3133b2790368 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -24,7 +24,7 @@ import { } from "./generated/i18n/i18n-defaults.js"; import BusyIndicator from "./BusyIndicator.js"; import TableCell from "./TableCell.js"; -import { isFeature } from "./TableUtils.js"; +import { findVerticalScrollContainer, scrollElementIntoView, isFeature } from "./TableUtils.js"; /** * Interface for components that can be slotted inside the features slot of the ui5-table. @@ -404,36 +404,8 @@ class Table extends UI5Element { } _onfocusin(e: FocusEvent) { - // Handles focus that is below sticky element - const stickyElements = this._stickyElements; - - if (stickyElements.length === 0) { - return; - } - - // Find the sticky element that is closest to the focused element - const target = e.target as HTMLElement; - const element = target.closest("ui5-table-cell, ui5-table-row") as HTMLElement ?? target; - const elementRect = element.getBoundingClientRect(); - const stickyBottom = stickyElements.reduce((min, stickyElement) => { - const stickyRect = stickyElement.getBoundingClientRect(); - - if (stickyRect.bottom > elementRect.top) { - return Math.max(min, stickyRect.bottom); - } - return min; - }, -Infinity); - - // If the focused element is not behind any sticky element, do nothing - if (stickyBottom === -Infinity) { - return; - } - - // Scroll the focused element into view - const scrollContainer = this._scrollContainer; - scrollContainer.scrollBy({ - top: elementRect.top - stickyBottom, - }); + // 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"); } /** @@ -519,7 +491,7 @@ class Table extends UI5Element { } get _tableOverflowX() { - return (this.overflowMode === TableOverflowMode.Popin) ? "hidden" : "auto"; + return (this.overflowMode === TableOverflowMode.Popin) ? "clip" : "auto"; } get _tableOverflowY() { @@ -567,22 +539,15 @@ class Table extends UI5Element { return this.features.find(feature => this._isGrowingFeature(feature)) as ITableGrowing; } - // TODO: Could be moved to UI5Element. TBD - get _scrollContainer() { - let element: HTMLElement = this as HTMLElement; - while (element) { - const { overflowY } = window.getComputedStyle(element); - if (overflowY === "auto" || overflowY === "scroll") { - return element; - } - element = element.parentElement as HTMLElement; - } + get _stickyElements() { + const stickyRows = this.headerRow.filter(row => row.sticky); + const stickyColumns = this.headerRow[0]._stickyCells as TableHeaderCell[]; - return document.scrollingElement as HTMLElement || document.documentElement; + return [...stickyRows, ...stickyColumns]; } - get _stickyElements() { - return [this.headerRow[0]].filter(row => row.sticky); + get _scrollContainer() { + return findVerticalScrollContainer(this._tableElement); } get isTable() { diff --git a/packages/main/src/TableHeaderRow.hbs b/packages/main/src/TableHeaderRow.hbs index 3f7e566045a9..de3218f7bed7 100644 --- a/packages/main/src/TableHeaderRow.hbs +++ b/packages/main/src/TableHeaderRow.hbs @@ -2,6 +2,7 @@ {{#if _isMultiSelect}} diff --git a/packages/main/src/TableHeaderRow.ts b/packages/main/src/TableHeaderRow.ts index b3872d4b49ab..b3ec30366d3b 100644 --- a/packages/main/src/TableHeaderRow.ts +++ b/packages/main/src/TableHeaderRow.ts @@ -66,6 +66,9 @@ class TableHeaderRow extends TableRowBase { /** * Sticks the `ui5-table-header-row` to the top of a table. * + * Note: If used in combination with overflowMode "Scroll", the table needs a defined height + * or needs to be inside of a container with a defined height for the sticky header to work as expected. + * * @default false * @public */ diff --git a/packages/main/src/TableRow.hbs b/packages/main/src/TableRow.hbs index 21d9b81a4f2f..d96044c38157 100644 --- a/packages/main/src/TableRow.hbs +++ b/packages/main/src/TableRow.hbs @@ -1,5 +1,5 @@ {{#if _hasRowSelector}} - + {{#if _isMultiSelect}} c._popin); } - get _i18nRowSelector(): string { - return TableRowBase.i18nBundle.getText(TABLE_ROW_SELECTOR); + get _stickyCells() { + const selectionCell = this.shadowRoot?.querySelector("#selection-cell"), + navigatedCell = this.shadowRoot?.querySelector("#navigated-cell"); + + // filter out null/undefined + return [selectionCell, ...this.cells, navigatedCell].filter(cell => cell?.hasAttribute("fixed")); } - get isTableRowBase() { - return true; + get _i18nRowSelector(): string { + return TableRowBase.i18nBundle.getText(TABLE_ROW_SELECTOR); } } diff --git a/packages/main/src/TableSelection.ts b/packages/main/src/TableSelection.ts index 7232c7ecf62d..ae425e421d5f 100644 --- a/packages/main/src/TableSelection.ts +++ b/packages/main/src/TableSelection.ts @@ -252,9 +252,11 @@ class TableSelection extends UI5Element implements ITableFeature { return; } - if (!eventOrigin.hasAttribute("ui5-table-row") || !this._rangeSelection || isShift(e) || !isSelectionCheckbox(e)) { + if (!eventOrigin.hasAttribute("ui5-table-row") || !this._rangeSelection || !isShift(e)) { // Stop range selection if a) Shift is relased or b) the event target is not a row or c) the event is not from the selection checkbox - this._stopRangeSelection(); + if (isSelectionCheckbox(e)) { + this._stopRangeSelection(); + } } if (this._rangeSelection) { diff --git a/packages/main/src/TableUtils.ts b/packages/main/src/TableUtils.ts index e2d818d4cb7d..08c66e7646e9 100644 --- a/packages/main/src/TableUtils.ts +++ b/packages/main/src/TableUtils.ts @@ -17,6 +17,62 @@ const findRowInPath = (composedPath: Array) => { return composedPath.find((el: EventTarget) => el instanceof HTMLElement && el.hasAttribute("ui5-table-row")) as TableRow; }; +const findVerticalScrollContainer = (element: HTMLElement): HTMLElement => { + while (element) { + const { overflowY } = window.getComputedStyle(element); + if (overflowY === "auto" || overflowY === "scroll") { + return element; + } + + if (element.parentNode instanceof ShadowRoot) { + element = element.parentNode.host as HTMLElement; + } else { + element = element.parentElement as HTMLElement; + } + } + + return document.scrollingElement as HTMLElement || document.documentElement; +}; + +const scrollElementIntoView = (scrollContainer: HTMLElement, element: HTMLElement, stickyElements: HTMLElement[], isRtl: boolean) => { + if (stickyElements.length === 0) { + return; + } + + const elementRect = element.getBoundingClientRect(); + const inline = isRtl ? "right" : "left"; + + const { x: stickyX, y: stickyY } = stickyElements.reduce(({ x, y }, stickyElement) => { + const { top, [inline]: inlineStart } = getComputedStyle(stickyElement); + const stickyElementRect = stickyElement.getBoundingClientRect(); + if (top !== "auto" && stickyElementRect.bottom > elementRect.top) { + y = Math.max(y, stickyElementRect.bottom); + } + if (inlineStart !== "auto") { + if (!isRtl && stickyElementRect.right > elementRect.left) { + x = Math.max(x, stickyElementRect.right); + } else if (isRtl && stickyElementRect.left < elementRect.right) { + x = Math.min(x, stickyElementRect.left); + } + } + + return { x, y }; + }, { x: elementRect[inline], y: elementRect.top }); + + const scrollX = elementRect[inline] - stickyX; + const scrollY = elementRect.top - stickyY; + + if (scrollX === 0 && scrollY === 0) { + // avoid unnecessary scroll call, when nothing changes + return; + } + + scrollContainer.scrollBy({ + top: scrollY, + left: scrollX, + }); +}; + const isFeature = (element: any, identifier: string): element is T => { return element.identifier === identifier; }; @@ -26,5 +82,7 @@ export { isSelectionCheckbox, isHeaderSelector, findRowInPath, + findVerticalScrollContainer, + scrollElementIntoView, isFeature, }; diff --git a/packages/main/src/themes/Table.css b/packages/main/src/themes/Table.css index ffab6b155632..85e7af1752c1 100644 --- a/packages/main/src/themes/Table.css +++ b/packages/main/src/themes/Table.css @@ -9,6 +9,10 @@ display: none; } +:host([overflow-mode="Scroll"]) { + overflow-x: scroll; +} + #table { display: grid; grid-auto-rows: minmax(min-content, auto); diff --git a/packages/main/src/themes/TableCellBase.css b/packages/main/src/themes/TableCellBase.css index 515710acbe42..19345e623998 100644 --- a/packages/main/src/themes/TableCellBase.css +++ b/packages/main/src/themes/TableCellBase.css @@ -18,4 +18,10 @@ :host(#selection-cell) { width: auto; min-width: auto; + background-color: inherit; +} + +:host([fixed]) { + position: sticky; + z-index: 1; } \ No newline at end of file diff --git a/packages/main/src/themes/TableHeaderRow.css b/packages/main/src/themes/TableHeaderRow.css index de8760eb9dd7..057c81c52c2e 100644 --- a/packages/main/src/themes/TableHeaderRow.css +++ b/packages/main/src/themes/TableHeaderRow.css @@ -10,7 +10,7 @@ :host([sticky]) { position: sticky; top: var(--ui5_grid_sticky_top, 0); - z-index: 1; + z-index: 2; } #popin-cell { diff --git a/packages/main/src/themes/TableRow.css b/packages/main/src/themes/TableRow.css index 6b5ef4b5aae1..1d4257ffe777 100644 --- a/packages/main/src/themes/TableRow.css +++ b/packages/main/src/themes/TableRow.css @@ -39,6 +39,12 @@ grid-row: span 2; min-width: 0; padding: 0; + position: sticky; + right: 0; +} + +:dir(rtl)#navigated-cell { + left: 0; } :host([navigated]) #navigated { diff --git a/packages/main/src/themes/TableRowBase.css b/packages/main/src/themes/TableRowBase.css index eb487a5a063d..6af56bc22633 100644 --- a/packages/main/src/themes/TableRowBase.css +++ b/packages/main/src/themes/TableRowBase.css @@ -2,8 +2,9 @@ display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; - border-bottom: var(--sapList_BorderWidth) solid var(--sapList_BorderColor); min-height: var(--_ui5_list_item_base_height); + box-sizing: border-box; + border-bottom: var(--sapList_BorderWidth) solid var(--sapList_BorderColor); } :host([tabindex]:focus) { @@ -13,8 +14,21 @@ #selection-cell { padding: 0; + left: 0; +} + +:dir(rtl)#selection-cell { + right: 0; } #selection-component { vertical-align: middle; } + +/** Focus outline for the selection cell */ +:host([tabindex]:focus) #selection-cell { + outline: none; + box-shadow: var(--_ui5_table_shadow_border_top), + var(--_ui5_table_shadow_border_left), + var(--_ui5_table_shadow_border_bottom); /* There is a 1px difference between row and cell, therefore the outline is also 1px difference between their outline offsets */ +} diff --git a/packages/main/src/themes/base/Table-parameters.css b/packages/main/src/themes/base/Table-parameters.css index 202a244990da..3000b9071ca8 100644 --- a/packages/main/src/themes/base/Table-parameters.css +++ b/packages/main/src/themes/base/Table-parameters.css @@ -2,4 +2,13 @@ --_ui5_table_cell_valign: center; --_ui5_table_cell_min_width: 2.75rem; --_ui5_table_navigated_cell_width: 0.1875rem; + --_ui5_table_shadow_border_left: inset var(--sapContent_FocusWidth) 0 var(--sapContent_FocusColor); + --_ui5_table_shadow_border_right: inset calc(-1 * var(--sapContent_FocusWidth)) 0 var(--sapContent_FocusColor); + --_ui5_table_shadow_border_top: inset 0 var(--sapContent_FocusWidth) var(--sapContent_FocusColor); + --_ui5_table_shadow_border_bottom: inset 0 -1px var(--sapContent_FocusColor); +} + +[dir="rtl"] { + --_ui5_table_shadow_border_left: inset calc(-1 * var(--sapContent_FocusWidth)) 0 var(--sapContent_FocusColor); + --_ui5_table_shadow_border_right: inset var(--sapContent_FocusWidth) 0 var(--sapContent_FocusColor); } \ No newline at end of file diff --git a/packages/main/test/pages/Table.html b/packages/main/test/pages/Table.html index babddf6b6978..389b196e0435 100644 --- a/packages/main/test/pages/Table.html +++ b/packages/main/test/pages/Table.html @@ -23,14 +23,12 @@ label-interval="0"> - - - + - + Product - Supplier + Supplier Dimensions Weight Price @@ -86,8 +84,6 @@ - - + + + + + + + + + Product + Supplier + Dimensions + Weight + Price + + + Notebook Basic 15
HT-1000
+ Very Best Screens + 30 x 18 x 3 cm + 4.2 KG + 956 EUR +
+ + Notebook Basic 16
HT-1001
+ Smartcards + + 4.5 KG + 1249 EUR +
+ + Notebook Basic 17
HT-1002
+ Technocom + 32 x 21 x 4 cm + 3.7 KG + 29 EUR +
+ + Notebook Basic 18
HT-1003
+ Technocom + 32 x 21 x 4 cm + 3.7 KG + 29 EUR +
+ + Notebook Basic 19
HT-1004
+ Technocom + 32 x 21 x 4 cm + 3.7 KG + 29 EUR +
+ + Notebook Basic 20
HT-1005
+ Technocom + 32 x 21 x 4 cm + 3.7 KG + 29 EUR +
+ + Notebook Basic 21
HT-1006
+ Technocom + 32 x 21 x 4 cm + 3.7 KG + 29 EUR +
+
+ + + + + + \ No newline at end of file diff --git a/packages/main/test/specs/Table.spec.js b/packages/main/test/specs/Table.spec.js index 90b3dbafb343..8cc64483b03c 100644 --- a/packages/main/test/specs/Table.spec.js +++ b/packages/main/test/specs/Table.spec.js @@ -198,6 +198,62 @@ describe("Table - Fixed Header", async () => { }); }); +describe("Table - Horizontal Scrolling", async () => { + before(async () => { + await browser.url(`test/pages/TableHorizontal.html`); + }); + + it("navigated indicator is fixed to the right", async () => { + const table = await browser.$("#table"); + + assert.ok(await table.isExisting(), "Table exists"); + + const row = await browser.$("#firstRow"); + const navigatedCell = await row.shadow$("#navigated-cell"); + + assert.ok(await navigatedCell.isExisting(), "Navigated cell exists"); + + const stickyProperty = await navigatedCell.getCSSProperty("position"); + const rightProperty = await navigatedCell.getCSSProperty("right"); + + assert.strictEqual(stickyProperty.value, "sticky", "Navigated cell is sticky"); + assert.strictEqual(rightProperty.value, "0px", "Navigated cell is at the right edge"); + }); + + it("selection column should be fixed to the left", async () => { + const table = await browser.$("#table"); + const lastColumn = await browser.$("#lastCell"); + + assert.ok(await table.isExisting(), "Table exists"); + + const { leftOffset, fixedX } = await browser.execute(() => { + const table = document.getElementById("table"); + const row = document.getElementById("firstRow"); + return { + fixedX: row.shadowRoot.querySelector("#selection-cell").getBoundingClientRect().x, + leftOffset: table.shadowRoot.querySelector("#table")?.scrollLeft || 0 + }; + }); + + assert.equal(leftOffset, 0, "Table is not scrolled horizontally"); + assert.equal(fixedX, 0, "Selection column is fixed to the left"); + + await lastColumn.scrollIntoView(); + + const { leftOffset2, fixedX2 } = await browser.execute(() => { + const table = document.getElementById("table"); + const row = document.getElementById("firstRow"); + return { + fixedX2: row.shadowRoot.querySelector("#selection-cell").getBoundingClientRect().x, + leftOffset2: table.scrollLeft || 0 + }; + }); + + assert.ok(leftOffset2 > 0, "Table is scrolled horizontally"); + assert.equal(fixedX2, 0, "Selection column is still fixed to the left"); + }); +}); + // Tests navigated property of rows describe("Table - Navigated Rows", async () => { before(async () => {