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
+
+
+
+
+
+
+
+