From 06916aed795dafb0a012526f0c32cc1d3257a8e1 Mon Sep 17 00:00:00 2001 From: Cahit Guerguec Date: Tue, 28 Apr 2026 00:26:10 +0200 Subject: [PATCH 1/2] refactor(table): implement dummy cell rendering Co-authored-by: Copilot --- packages/main/cypress/specs/Table.cy.tsx | 199 +++++++++++++++++-- packages/main/src/Table.ts | 19 +- packages/main/src/TableHeaderRowTemplate.tsx | 21 ++ packages/main/src/TableNavigation.ts | 3 + packages/main/src/TableRowBase.ts | 3 + packages/main/src/TableRowTemplate.tsx | 12 ++ packages/main/src/themes/TableRowBase.css | 8 + packages/main/test/pages/TableNoData.html | 177 +++++++++++++++++ 8 files changed, 427 insertions(+), 15 deletions(-) create mode 100644 packages/main/test/pages/TableNoData.html diff --git a/packages/main/cypress/specs/Table.cy.tsx b/packages/main/cypress/specs/Table.cy.tsx index 33f00cb737a5..f20d0b8eefc8 100644 --- a/packages/main/cypress/specs/Table.cy.tsx +++ b/packages/main/cypress/specs/Table.cy.tsx @@ -8,6 +8,7 @@ import TableHeaderCell from "../../src/TableHeaderCell.js"; import TableHeaderCellActionAI from "../../src/TableHeaderCellActionAI.js"; import Label from "../../src/Label.js"; import Input from "../../src/Input.js"; +import TableRowAction from "../../src/TableRowAction.js"; import Bar from "../../src/Bar.js"; import Title from "../../src/Title.js"; import Slider from "../../src/Slider.js"; @@ -443,7 +444,7 @@ describe("Table - Popin Mode", () => { for (const cell of row.cells) { if (cell._popin) { popinCellCount++; - const popinText = cell._headerCell.popinText || cell._headerCell.textContent; + const popinText = cell._headerCell!.popinText || cell._headerCell!.textContent; if (cell.shadowRoot!.textContent === `${popinText}:`) { validPopinTextCount++; } @@ -883,26 +884,24 @@ describe("Table - Navigated Rows", () => { ); - cy.get("#row1") - .shadow() - .find("#navigated-cell") + // Navigated cell rendered on all rows and header row + cy.get("[ui5-table-header-row]").shadow().find("#navigated-cell") .should("exist") + .should("have.attr", "aria-hidden", "true"); + cy.get("[ui5-table-header-row]").shadow().find("#navigated-cell") .should("have.attr", "data-excluded-from-navigation"); - cy.get("#row2") - .shadow() - .find("#navigated-cell") + cy.get("#row1").shadow().find("#navigated-cell") .should("exist") .should("have.attr", "data-excluded-from-navigation"); - cy.get("#row1") - .shadow() - .find("#navigated") - .as("navigated1"); + cy.get("#row2").shadow().find("#navigated-cell") + .should("exist") + .should("have.attr", "data-excluded-from-navigation"); - cy.get("#row2") - .shadow() - .find("#navigated") + // Navigated indicator differs between navigated and non-navigated rows + cy.get("#row1").shadow().find("#navigated").as("navigated1"); + cy.get("#row2").shadow().find("#navigated") .then($navigated2 => { cy.get("@navigated1") .should($navigated1 => { @@ -1193,3 +1192,175 @@ describe("Table - Cell Merging", () => { cy.get("#row2").shadow().find("#selection-cell").should("have.css", "border-top-color", TRANSPARENT); }); }); + +describe("Table - Dummy Cell", () => { + it("should render dummy cell with border and nofocus when all columns have fixed widths", () => { + cy.mount( + + + Product + Supplier + + + + + + + + + +
+ ); + + cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell").should("exist"); + cy.get("#row1").shadow().find("#dummy-cell").should("exist"); + + // Should have left border + cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") + .should("not.have.css", "border-inline-start-width", "0px"); + cy.get("#row1").shadow().find("#dummy-cell") + .should("not.have.css", "border-inline-start-width", "0px"); + + // Should have data-excluded-from-navigation="nofocus" and data-border-merged when no popin + cy.get("#row1").shadow().find("#dummy-cell") + .should("have.attr", "data-excluded-from-navigation", "nofocus") + .should("have.attr", "data-border-merged"); + + // Should not focus row or fire row-click when clicking dummy cell + cy.get("#table").invoke("on", "row-click", cy.stub().as("rowClickHandler")); + cy.get("#row2").shadow().find("#dummy-cell").realClick(); + cy.get("#row2").should("not.be.focused"); + cy.get("#row2").should("not.have.attr", "_active"); + cy.get("@rowClickHandler").should("not.have.been.called"); + }); + + it("should not render dummy cell when columns are flexible", () => { + const cases = [ + { widths: ["200px", "auto"] }, + { widths: ["Auto"] }, + { widths: ["200px", undefined] }, + ]; + + cases.forEach(({ widths }) => { + cy.mount( + + + {widths.map((w, i) => ( + {`Col ${i}`} + ))} + + + {widths.map((_, i) => ( + + ))} + +
+ ); + + cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell").should("not.exist"); + cy.get("#row1").shadow().find("#dummy-cell").should("not.exist"); + }); + }); + + it("should render dummy cell after actions when no popin and before actions when popin", () => { + cy.mount( + + + Product + Supplier + + + + + + + +
+ ); + + // No popin: dummy cell is after actions cell (rightmost) in both row and header row + cy.get("#row1").shadow().then($shadow => { + const dummyCell = $shadow.find("#dummy-cell")[0]; + const actionsCell = $shadow.find("#actions-cell")[0]; + const position = dummyCell.compareDocumentPosition(actionsCell); + // eslint-disable-next-line no-bitwise + expect(position & Node.DOCUMENT_POSITION_PRECEDING).to.be.greaterThan(0); + }); + cy.get("[ui5-table-header-row]").shadow().then($shadow => { + const dummyCell = $shadow.find("#dummy-cell")[0]; + const actionsCell = $shadow.find("#actions-cell")[0]; + const position = dummyCell.compareDocumentPosition(actionsCell); + // eslint-disable-next-line no-bitwise + expect(position & Node.DOCUMENT_POSITION_PRECEDING).to.be.greaterThan(0); + }); + + // Shrink to trigger popin: dummy cell moves before actions cell + cy.get("ui5-table").invoke("css", "width", "250px"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + cy.get("#row1").should("have.attr", "_haspopin"); + cy.get("#row1").shadow().then($shadow => { + const dummyCell = $shadow.find("#dummy-cell")[0]; + const actionsCell = $shadow.find("#actions-cell")[0]; + const position = dummyCell.compareDocumentPosition(actionsCell); + // eslint-disable-next-line no-bitwise + expect(position & Node.DOCUMENT_POSITION_FOLLOWING).to.be.greaterThan(0); + }); + cy.get("[ui5-table-header-row]").shadow().then($shadow => { + const dummyCell = $shadow.find("#dummy-cell")[0]; + const actionsCell = $shadow.find("#actions-cell")[0]; + const position = dummyCell.compareDocumentPosition(actionsCell); + // eslint-disable-next-line no-bitwise + expect(position & Node.DOCUMENT_POSITION_FOLLOWING).to.be.greaterThan(0); + }); + }); + + it("should adapt dummy cell border and navigation attribute when row has popin", () => { + cy.mount( + + + Product + Supplier + + + + + +
+ ); + + // At full width: left border visible, nofocus attribute on both row and header row + cy.get("#row1").shadow().find("#dummy-cell") + .should("not.have.css", "border-inline-start-width", "0px") + .should("have.attr", "data-excluded-from-navigation", "nofocus"); + cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") + .should("have.attr", "data-excluded-from-navigation", "nofocus"); + + // Shrink to trigger popin + cy.get("ui5-table").invoke("css", "width", "250px"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + cy.get("#row1").should("have.attr", "_haspopin"); + + // Left border removed, data-excluded-from-navigation without nofocus + cy.get("#row1").shadow().find("#dummy-cell") + .should("have.css", "border-inline-start-width", "0px") + .should("have.attr", "data-excluded-from-navigation", ""); + cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") + .should("have.attr", "data-excluded-from-navigation", ""); + + // Expand again, border and nofocus should return + cy.get("ui5-table").invoke("css", "width", "800px"); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + cy.get("#row1").should("not.have.attr", "_haspopin"); + cy.get("#row1").shadow().find("#dummy-cell") + .should("not.have.css", "border-inline-start-width", "0px") + .should("have.attr", "data-excluded-from-navigation", "nofocus"); + cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") + .should("have.attr", "data-excluded-from-navigation", "nofocus"); + }); +}); diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index 6e864a648f71..32d34cb404fa 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -457,8 +457,9 @@ class Table extends UI5Element { onBeforeRendering(): void { this._renderNavigated = this.rows.some(row => row.navigated); [...this.headerRow, ...this.rows].forEach((row, index) => { - row._renderNavigated = this._renderNavigated; row._rowActionCount = this.rowActionCount; + row._renderNavigated = this._renderNavigated; + row._renderDummyCell = !this._hasFlexibleColumns; row._alternate = this.alternateRowColors && index % 2 === 0; }); @@ -649,6 +650,14 @@ class Table extends UI5Element { return width; })); + // Dummy Cell Width (before actions when popin, after navigated otherwise) + const dummyColumnWidth = !this._hasFlexibleColumns ? "minmax(0, 1fr)" : ""; + const hasPopinCells = this.headerRow[0]._popinCells.length > 0; + + if (dummyColumnWidth && hasPopinCells) { + widths.push(dummyColumnWidth); + } + // Row Action Cell Width if (this.rowActionCount > 0) { widths.push(`calc(var(--_ui5_button_base_min_width) * ${this.rowActionCount} + var(--_ui5_table_row_actions_gap) * ${this.rowActionCount - 1} + var(--_ui5_table_cell_horizontal_padding) * 2)`); @@ -659,9 +668,17 @@ class Table extends UI5Element { widths.push(`var(--_ui5_table_navigated_cell_width)`); } + if (dummyColumnWidth && !hasPopinCells) { + widths.push(dummyColumnWidth); + } + return widths.join(" "); } + get _hasFlexibleColumns(): boolean { + return this.headerRow?.[0]?._visibleCells.some(cell => !isValidColumnWidth(cell.width)); + } + get _isRowSelectorRequired() { return this.rows.length > 0 && this._getSelection()?.isRowSelectorRequired(); } diff --git a/packages/main/src/TableHeaderRowTemplate.tsx b/packages/main/src/TableHeaderRowTemplate.tsx index 6a598a1eaadc..7cf96380d912 100644 --- a/packages/main/src/TableHeaderRowTemplate.tsx +++ b/packages/main/src/TableHeaderRowTemplate.tsx @@ -54,12 +54,33 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde return []; })} + { this._renderDummyCell && this._popinCells.length > 0 && + + + } + { this._rowActionCount > 0 &&
{this._i18nRowActions}
} + { this._renderNavigated && + + + } + + { this._renderDummyCell && this._popinCells.length === 0 && + + + } + { this._popinCells.length > 0 &&
{this._i18nRowPopin}
diff --git a/packages/main/src/TableNavigation.ts b/packages/main/src/TableNavigation.ts index 85f0d1937914..286934f844cc 100644 --- a/packages/main/src/TableNavigation.ts +++ b/packages/main/src/TableNavigation.ts @@ -246,6 +246,9 @@ class TableNavigation extends TableExtension { for (const target of e.composedPath() as any[]) { if (target.nodeType === Node.ELEMENT_NODE) { const element = target as HTMLElement; + if (element.getAttribute("data-excluded-from-navigation") === "nofocus") { + break; + } if (element.matches(":focus-within")) { focusableElement = element; break; diff --git a/packages/main/src/TableRowBase.ts b/packages/main/src/TableRowBase.ts index c9fdf238d181..8d14a5c296ee 100644 --- a/packages/main/src/TableRowBase.ts +++ b/packages/main/src/TableRowBase.ts @@ -40,6 +40,9 @@ abstract class TableRowBase extends @property({ type: Boolean, noAttribute: true }) _alternate = false; + @property({ type: Boolean, noAttribute: true }) + _renderDummyCell = false; + @query("#selection-cell") _selectionCell?: HTMLElement; diff --git a/packages/main/src/TableRowTemplate.tsx b/packages/main/src/TableRowTemplate.tsx index d882efe05185..069b78aecde0 100644 --- a/packages/main/src/TableRowTemplate.tsx +++ b/packages/main/src/TableRowTemplate.tsx @@ -47,6 +47,12 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number = return []; })} + { this._renderDummyCell && this._hasPopin && + + + } + { this._rowActionCount > 0 && } + { this._renderDummyCell && !this._hasPopin && + + + } + { this._popinCells.length > 0 && + + + + + Table - No Data + + + + + + + + + + +

Single column - no data (column should cover full width)

+ + + Product + + + +
+

Single column (width:40%) - no data with custom noData slot

+
+ Add Row + Remove Row +
+ + + Product + +
+ +
+
+ + + +
+

Multiple columns (width:200px each) - no data

+
+ Add Row + Remove Row +
+ + + Product + Supplier + Price + + + + + +
+

Popin - 4 columns (shrink below 1000px to see popin columns)

+
+ Add Row + Remove Row +
+ + + Product + Supplier + Dimensions + Price + +
+ +
+
+ + + + + \ No newline at end of file From 461ad1856416ac9baa289c99678f8a5ce5d4b8e6 Mon Sep 17 00:00:00 2001 From: Cahit Guerguec Date: Mon, 4 May 2026 14:46:52 +0200 Subject: [PATCH 2/2] refactor(table): implements empty area rendering --- packages/main/cypress/specs/Table.cy.tsx | 35 ++++--- packages/main/src/Table.ts | 6 +- packages/main/src/TableCell.ts | 4 +- packages/main/src/TableHeaderRowTemplate.tsx | 7 +- packages/main/src/TableRow.ts | 11 --- packages/main/src/TableRowBase.ts | 40 +++++++- packages/main/src/TableRowTemplate.tsx | 2 +- packages/main/src/themes/TableRow.css | 67 ++++++------- packages/main/src/themes/TableRowBase.css | 99 ++++++++++++++----- packages/main/test/pages/TableNoData.html | 2 + packages/main/test/pages/Table_Acc.html | 7 ++ .../main/Table/TableCell.mdx | 12 +-- .../main/Table/TableHeaderCell.mdx | 4 +- 13 files changed, 186 insertions(+), 110 deletions(-) diff --git a/packages/main/cypress/specs/Table.cy.tsx b/packages/main/cypress/specs/Table.cy.tsx index f20d0b8eefc8..15038f21ebf4 100644 --- a/packages/main/cypress/specs/Table.cy.tsx +++ b/packages/main/cypress/specs/Table.cy.tsx @@ -430,8 +430,6 @@ describe("Table - Popin Mode", () => { cy.get("ui5-table").then($table => { $table.css("width", "150px"); }); - - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(50); cy.get("ui5-table").then($table => { @@ -1145,7 +1143,7 @@ describe("Table - Cell Merging", () => { cy.wait(50); // Merged cell border should fall back to normal border color (not transparent) - cy.get("#row2").should("have.attr", "_haspopin"); + cy.get("#row2").should("have.attr", "_has-popin"); cy.get("#r2cA").should("not.have.css", "border-top-color", TRANSPARENT); cy.get("#row2").shadow().find("#selection-cell").should("not.have.css", "border-top-color", TRANSPARENT); @@ -1283,42 +1281,38 @@ describe("Table - Dummy Cell", () => { const dummyCell = $shadow.find("#dummy-cell")[0]; const actionsCell = $shadow.find("#actions-cell")[0]; const position = dummyCell.compareDocumentPosition(actionsCell); - // eslint-disable-next-line no-bitwise expect(position & Node.DOCUMENT_POSITION_PRECEDING).to.be.greaterThan(0); }); cy.get("[ui5-table-header-row]").shadow().then($shadow => { const dummyCell = $shadow.find("#dummy-cell")[0]; const actionsCell = $shadow.find("#actions-cell")[0]; const position = dummyCell.compareDocumentPosition(actionsCell); - // eslint-disable-next-line no-bitwise expect(position & Node.DOCUMENT_POSITION_PRECEDING).to.be.greaterThan(0); }); // Shrink to trigger popin: dummy cell moves before actions cell cy.get("ui5-table").invoke("css", "width", "250px"); - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(50); - cy.get("#row1").should("have.attr", "_haspopin"); + cy.get("#row1").should("have.attr", "_has-popin"); cy.get("#row1").shadow().then($shadow => { const dummyCell = $shadow.find("#dummy-cell")[0]; const actionsCell = $shadow.find("#actions-cell")[0]; const position = dummyCell.compareDocumentPosition(actionsCell); - // eslint-disable-next-line no-bitwise expect(position & Node.DOCUMENT_POSITION_FOLLOWING).to.be.greaterThan(0); }); cy.get("[ui5-table-header-row]").shadow().then($shadow => { const dummyCell = $shadow.find("#dummy-cell")[0]; const actionsCell = $shadow.find("#actions-cell")[0]; const position = dummyCell.compareDocumentPosition(actionsCell); - // eslint-disable-next-line no-bitwise expect(position & Node.DOCUMENT_POSITION_FOLLOWING).to.be.greaterThan(0); }); }); - it("should adapt dummy cell border and navigation attribute when row has popin", () => { + it("should adapt dummy cell border, navigation attribute, and custom focus outline with popin", () => { cy.mount( + Product Supplier @@ -1337,12 +1331,20 @@ describe("Table - Dummy Cell", () => { cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") .should("have.attr", "data-excluded-from-navigation", "nofocus"); - // Shrink to trigger popin + // Focus row - custom outline should be applied + cy.get("#row1").should("have.attr", "_render-dummy-cell"); + cy.get("#row1").should("not.have.attr", "_has-popin"); + cy.realPress("Tab"); + cy.get("#row1").shadow().find("[data-ui5-custom-outline='start']").should("exist"); + cy.get("#row1").find("[data-ui5-custom-outline='end']").should("exist"); + cy.get("#row1").shadow().find("#dummy-cell") + .should("not.have.attr", "data-ui5-custom-outline"); + + // Shrink to trigger popin to test custom outline is removed cy.get("ui5-table").invoke("css", "width", "250px"); - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(50); - cy.get("#row1").should("have.attr", "_haspopin"); + cy.get("#row1").should("have.attr", "_has-popin"); // Left border removed, data-excluded-from-navigation without nofocus cy.get("#row1").shadow().find("#dummy-cell") @@ -1351,16 +1353,17 @@ describe("Table - Dummy Cell", () => { cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") .should("have.attr", "data-excluded-from-navigation", ""); - // Expand again, border and nofocus should return + // Expand back - border and nofocus should return, custom outline reapplied via onAfterRendering cy.get("ui5-table").invoke("css", "width", "800px"); - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(50); - cy.get("#row1").should("not.have.attr", "_haspopin"); + cy.get("#row1").should("not.have.attr", "_has-popin"); cy.get("#row1").shadow().find("#dummy-cell") .should("not.have.css", "border-inline-start-width", "0px") .should("have.attr", "data-excluded-from-navigation", "nofocus"); cy.get("[ui5-table-header-row]").shadow().find("#dummy-cell") .should("have.attr", "data-excluded-from-navigation", "nofocus"); + cy.get("#row1").shadow().find("[data-ui5-custom-outline='start']").should("exist"); + cy.get("#row1").find("[data-ui5-custom-outline='end']").should("exist"); }); }); diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index 32d34cb404fa..a93ef0cbcda9 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -457,7 +457,7 @@ class Table extends UI5Element { onBeforeRendering(): void { this._renderNavigated = this.rows.some(row => row.navigated); [...this.headerRow, ...this.rows].forEach((row, index) => { - row._rowActionCount = this.rowActionCount; + row._rowActionCount = this.rows.length > 0 ? this.rowActionCount : 0; row._renderNavigated = this._renderNavigated; row._renderDummyCell = !this._hasFlexibleColumns; row._alternate = this.alternateRowColors && index % 2 === 0; @@ -659,7 +659,7 @@ class Table extends UI5Element { } // Row Action Cell Width - if (this.rowActionCount > 0) { + if (this.rowActionCount > 0 && this.rows.length > 0) { widths.push(`calc(var(--_ui5_button_base_min_width) * ${this.rowActionCount} + var(--_ui5_table_row_actions_gap) * ${this.rowActionCount - 1} + var(--_ui5_table_cell_horizontal_padding) * 2)`); } @@ -718,7 +718,7 @@ class Table extends UI5Element { if (this._isRowSelectorRequired) { ariaColCount++; } - if (this.rowActionCount > 0) { + if (this.rowActionCount > 0 && this.rows.length > 0) { ariaColCount++; } if (this.headerRow[0]._popinCells.length > 0) { diff --git a/packages/main/src/TableCell.ts b/packages/main/src/TableCell.ts index ddfb0ffedca3..45fe6f91caf1 100644 --- a/packages/main/src/TableCell.ts +++ b/packages/main/src/TableCell.ts @@ -34,9 +34,9 @@ class TableCell extends TableCellBase { /** * Defines whether the cell is visually merged with the cell directly above it. * - * This is useful when consecutive cells in a column have the same value and should visually appear as a single merged cell. + * This is useful if consecutive cells in a column have the same value and should visually appear as a single merged cell. * Although the cell is visually merged with the previous one, its content must still be provided for accessibility purposes. - * **Note:** This feature is disabled when cells are rendered as popin, and should remain `false` for interactive cell content. + * **Note:** This feature is disabled when cells are rendered as a popin, and should remain `false` for interactive cell content. * * @default false * @since 2.21.0 diff --git a/packages/main/src/TableHeaderRowTemplate.tsx b/packages/main/src/TableHeaderRowTemplate.tsx index 7cf96380d912..68b4e4595eeb 100644 --- a/packages/main/src/TableHeaderRowTemplate.tsx +++ b/packages/main/src/TableHeaderRowTemplate.tsx @@ -54,7 +54,7 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde return []; })} - { this._renderDummyCell && this._popinCells.length > 0 && + { this._renderDummyCell && this._hasPopin && @@ -72,16 +72,17 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde aria-hidden={true} role="none" > + } - { this._renderDummyCell && this._popinCells.length === 0 && + { this._renderDummyCell && !this._hasPopin && } - { this._popinCells.length > 0 && + { this._hasPopin &&
{this._i18nRowPopin}
diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index 56f13c9257b3..4067db5886be 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -130,13 +130,6 @@ class TableRow extends TableRowBase { toggleAttribute(this, "draggable", this.movable, "true"); toggleAttribute(this, "_interactive", this._isInteractive); toggleAttribute(this, "_alternate", this._alternate); - toggleAttribute(this, "_haspopin", this._hasPopin); - } - - async focus(focusOptions?: FocusOptions | undefined): Promise { - this.setAttribute("tabindex", "-1"); - HTMLElement.prototype.focus.call(this, focusOptions); - return Promise.resolve(); } async _onpointerdown(e: PointerEvent) { @@ -198,10 +191,6 @@ class TableRow extends TableRowBase { }) !== undefined; } - get _hasPopin() { - return this.cells.some(c => c._popin && !c._popinHidden); - } - get _rowIndex() { if (this.position !== undefined) { return this.position; diff --git a/packages/main/src/TableRowBase.ts b/packages/main/src/TableRowBase.ts index 8d14a5c296ee..f75a9767eac8 100644 --- a/packages/main/src/TableRowBase.ts +++ b/packages/main/src/TableRowBase.ts @@ -40,7 +40,7 @@ abstract class TableRowBase extends @property({ type: Boolean, noAttribute: true }) _alternate = false; - @property({ type: Boolean, noAttribute: true }) + @property({ type: Boolean }) _renderDummyCell = false; @query("#selection-cell") @@ -52,6 +52,10 @@ abstract class TableRowBase extends @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; + isHeaderRow(): boolean { + return false; + } + onEnterDOM() { !this.role && this.setAttribute("role", "row"); this.toggleAttribute("ui5-table-row-base", true); @@ -59,14 +63,40 @@ abstract class TableRowBase extends onBeforeRendering() { toggleAttribute(this, "aria-selected", this._isSelectable, `${this._isSelected}`); + toggleAttribute(this, "_has-popin", this._hasPopin); + } + + onAfterRendering() { + this._handleCustomFocusOutline(); } getFocusDomRef() { return this; } - isHeaderRow(): boolean { - return false; + async focus(focusOptions?: FocusOptions | undefined): Promise { + this.setAttribute("tabindex", "-1"); + HTMLElement.prototype.focus.call(this, focusOptions); + this._handleCustomFocusOutline(); + return Promise.resolve(); + } + + _handleCustomFocusOutline() { + if (this._renderDummyCell && !this._hasPopin && document.activeElement === this) { + const cells = [...this.shadowRoot!.children].flatMap(element => { + return element.localName === "slot" ? (element as HTMLSlotElement).assignedElements() : [element]; + }); + const customOutlineAttribute = "data-ui5-custom-outline"; + cells.forEach(cell => cell.removeAttribute(customOutlineAttribute)); + const firstVisibleCell = cells.at(0); + const lastVisibleCell = cells.at(-2); + if (firstVisibleCell === lastVisibleCell) { + firstVisibleCell?.setAttribute(customOutlineAttribute, "startend"); + } else { + firstVisibleCell?.setAttribute(customOutlineAttribute, "start"); + lastVisibleCell?.setAttribute(customOutlineAttribute, "end"); + } + } } _onSelectionChange() { @@ -123,6 +153,10 @@ abstract class TableRowBase extends return this.cells.filter(c => c._popin && !c._popinHidden); } + get _hasPopin() { + return (this._table?.rows.length ?? 0) > 0 && this.cells.some(c => c._popin && !c._popinHidden); + } + get _stickyCells() { return [this._selectionCell, ...this.cells, this._navigatedCell].filter(cell => cell?.hasAttribute("fixed")); } diff --git a/packages/main/src/TableRowTemplate.tsx b/packages/main/src/TableRowTemplate.tsx index 069b78aecde0..b2190320a15c 100644 --- a/packages/main/src/TableRowTemplate.tsx +++ b/packages/main/src/TableRowTemplate.tsx @@ -92,7 +92,7 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number = } - { this._popinCells.length > 0 && + { this._hasPopin && [ui5-table-cell], :host(:first-of-type) > ::slotted([ui5-table-cell]) { border-top: none; @@ -18,20 +22,30 @@ :host([aria-selected="true"]) { background: var(--sapList_SelectionBackgroundColor); box-shadow: inset 0 calc(-1 * var(--sapList_BorderWidth)) 0 0 var(--sapList_SelectionBorderColor); + + #actions-cell, + #navigated-cell, + #selection-cell { + clip-path: inset(0px 0px 1px 0px); /* selection bottom border should not overlap with sticky cells */ + } } -:host(:not([_haspopin])) { +:host(:not([_has-popin])) { /* Use CSS Space Toggles until if() or container style queries are widely supported */ --_ui5_table_cell_border_merged: ; --_ui5_table_cell_content_merged: ; } -:host(:not([_haspopin]):active), -:host(:not([_haspopin]):focus-within) { +:host(:not([_has-popin]):active), +:host(:not([_has-popin]):focus-within) { /* Provide a valid CSS value to intentionally invalidate the TableCell variable-based rules and disable visual merging. */ --_ui5_table_cell_content_merged: initial; } +:host([_interactive]) { + cursor: pointer; +} + @media (hover: hover) { :host([_interactive]:hover) { background: var(--sapList_Hover_Background); @@ -39,7 +53,7 @@ :host([_interactive][aria-selected=true]:hover) { background: var(--sapList_Hover_SelectionBackground); } - :host(:not([_haspopin]):hover) { + :host(:not([_has-popin]):hover) { /* Provide a valid CSS value to intentionally invalidate the TableCell variable-based rules and disable visual merging. */ --_ui5_table_cell_content_merged: initial; } @@ -50,14 +64,6 @@ background: var(--sapList_Active_Background); } -:host([_interactive]) { - cursor: pointer; -} - -:host([position]) { - height: var(--row-height); -} - #popin-cell { padding-inline-start: var(--_ui5_first_table_cell_horizontal_padding); align-content: initial; @@ -67,38 +73,25 @@ } #navigated-cell { - position: sticky; - inset-inline-end: 0; - z-index: 1; - background-color: inherit; overflow: visible; grid-row: span 2; - min-width: 0; - padding: 0; } :host([navigated]) #navigated { position: absolute; - inset: -1px 0px 0px 1px; + inset: 0px 0px 0px 1px; background: var(--sapList_SelectionBorderColor); } -:host([tabindex]:focus) #navigated { - transform: translateX(calc(var(--_ui5_table_navigated_cell_width) * -1)); - bottom: 3px; - top: 2px; -} - -:host([tabindex]:focus) #navigated:dir(rtl) { - transform: translateX(var(--_ui5_table_navigated_cell_width)); -} - -:host([tabindex]:focus) #navigated-cell { - clip-path: inset(var(--sapContent_FocusWidth) var(--sapContent_FocusWidth) var(--sapContent_FocusWidth) calc(var(--_ui5_table_navigated_cell_width) * -1)); -} - -:host([tabindex]:focus) #navigated-cell:dir(rtl) { - clip-path: inset(var(--sapContent_FocusWidth) calc(var(--_ui5_table_navigated_cell_width) * -1) var(--sapContent_FocusWidth) var(--sapContent_FocusWidth)); +:host([navigated][tabindex]:focus) { + #navigated { + transform: translateX(calc(var(--_ui5_table_navigated_cell_width) * -1)); + bottom: 3px; + top: 2px; + } + #navigated:dir(rtl) { + transform: translateX(var(--_ui5_table_navigated_cell_width)); + } } :host([navigated]) #popin-cell { @@ -113,7 +106,3 @@ #actions-cell { gap: var(--_ui5_table_row_actions_gap); } - -#actions-cell:has(+ #navigated-cell) { - inset-inline-end: var(--_ui5_table_navigated_cell_width); -} \ No newline at end of file diff --git a/packages/main/src/themes/TableRowBase.css b/packages/main/src/themes/TableRowBase.css index 3e4eb7a49884..bd8088f128fa 100644 --- a/packages/main/src/themes/TableRowBase.css +++ b/packages/main/src/themes/TableRowBase.css @@ -7,14 +7,19 @@ overflow: clip; } -:host([tabindex]:focus) { - outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); - outline-offset: calc(-1 * var(--sapContent_FocusWidth)); +#selection-cell, +#actions-cell, +#navigated-cell { + background-color: inherit; + position: sticky; + z-index: 1; } -:host([tabindex]:focus) #selection-cell, -:host([tabindex]:focus) #actions-cell { - clip-path: inset(var(--sapContent_FocusWidth)); /* focus outline should not overlap sticky cells */ +#selection-cell { + padding: 0; + inset-inline-start: 0; + min-width: auto; + justify-content: center; } ::slotted([ui5-table-cell-base]:first-of-type) { @@ -25,33 +30,79 @@ padding-inline-start: var(--_ui5_table_cell_horizontal_padding); } -#selection-cell, #actions-cell { - background-color: inherit; - clip-path: inset(0px 0px 1px 0px); /* selection border should not overlap with sticky cells */ - position: sticky; - z-index: 1; + inset-inline-end: 0; +} + +#actions-cell:has(+ #navigated-cell) { + inset-inline-end: var(--_ui5_table_navigated_cell_width); +} + +#navigated-cell { + inset-inline-end: 0; + min-width: 0; + padding: 0; } #dummy-cell { - border-inline-start: var(--sapList_TableFixedColumnBorderWidth) solid var(--sapList_TableFixedBorderColor); + border-inline-start: 1px solid var(--sapList_TableFixedBorderColor); } -:host([_haspopin]) #dummy-cell { +:host([_has-popin]) #dummy-cell { border-inline-start: none; } -#selection-cell { - padding: 0; - inset-inline-start: 0; - min-width: auto; -} +:host([tabindex]:focus) { + outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); + outline-offset: calc(-1 * var(--sapContent_FocusWidth)); -#actions-cell { - inset-inline-end: 0; + #selection-cell, + #actions-cell{ + clip-path: inset(var(--sapContent_FocusWidth)); /* focus outline should not overlap sticky cells */ + } + + #navigated-cell { + clip-path: inset(3px 4px 3px -4px); + } + #navigated-cell:dir(rtl) { + clip-path: inset(3px -4px 3px 4px); + } } -#selection-cell[tabindex]:focus, -#actions-cell[tabindex]:focus { - clip-path: inset(0px); -} \ No newline at end of file +:host([tabindex][_render-dummy-cell]:not([_has-popin]):focus) { + outline: none; + + --_ui5_table_cell_custom_outline_block: inset 0 var(--sapContent_FocusWidth) 0 0 var(--sapContent_FocusColor), inset 0 calc(-1 * var(--sapContent_FocusWidth)) 0 0 var(--sapContent_FocusColor); + --_ui5_table_cell_custom_outline_inline-start: inset var(--sapContent_FocusWidth) 0 0 0 var(--sapContent_FocusColor); + --_ui5_table_cell_custom_outline_inline-end: inset calc(-1 * var(--sapContent_FocusWidth)) 0 0 0 var(--sapContent_FocusColor); + :dir(rtl) { + --_ui5_table_cell_custom_outline_inline-start: inset calc(-1 * var(--sapContent_FocusWidth)) 0 0 0 var(--sapContent_FocusColor); + --_ui5_table_cell_custom_outline_inline-end: inset var(--sapContent_FocusWidth) 0 0 0 var(--sapContent_FocusColor); + } + + [ui5-table-cell-base], ::slotted([ui5-table-cell-base]) { + box-shadow: var(--_ui5_table_cell_custom_outline_block); + } + [data-ui5-custom-outline="start"], ::slotted([data-ui5-custom-outline="start"]) { + box-shadow: var(--_ui5_table_cell_custom_outline_block), var(--_ui5_table_cell_custom_outline_inline-start); + } + [data-ui5-custom-outline="end"], ::slotted([data-ui5-custom-outline="end"]) { + box-shadow: var(--_ui5_table_cell_custom_outline_block), var(--_ui5_table_cell_custom_outline_inline-end); + } + [data-ui5-custom-outline="startend"], ::slotted([data-ui5-custom-outline="startend"]) { + box-shadow: var(--_ui5_table_cell_custom_outline_block), var(--_ui5_table_cell_custom_outline_inline-start), var(--_ui5_table_cell_custom_outline_inline-end); + } + #dummy-cell { + box-shadow: none; + } + + #selection-cell, + #actions-cell, + #navigated-cell { + clip-path: none; + } + + #navigated { + top: 3px; + } +} diff --git a/packages/main/test/pages/TableNoData.html b/packages/main/test/pages/TableNoData.html index 1f26c20219c3..6bb31eaac5b1 100644 --- a/packages/main/test/pages/TableNoData.html +++ b/packages/main/test/pages/TableNoData.html @@ -14,6 +14,7 @@ import "@ui5/webcomponents-fiori/dist/illustrations/NoData.js"; import "@ui5/webcomponents/dist/TableRowAction.js"; import "@ui5/webcomponents/dist/TableRowActionNavigation.js"; + import "@ui5/webcomponents/dist/TableSelectionMulti.js";