Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 191 additions & 17 deletions packages/main/cypress/specs/Table.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -429,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 => {
Expand All @@ -443,7 +442,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++;
}
Expand Down Expand Up @@ -883,26 +882,24 @@ describe("Table - Navigated Rows", () => {
</Table>
);

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 => {
Expand Down Expand Up @@ -1146,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);

Expand Down Expand Up @@ -1193,3 +1190,180 @@ 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(
<Table id="table">
<TableHeaderRow slot="headerRow">
<TableHeaderCell width="200px">Product</TableHeaderCell>
<TableHeaderCell width="40%">Supplier</TableHeaderCell>
</TableHeaderRow>
<TableRow id="row1">
<TableCell><Label>Product 1</Label></TableCell>
<TableCell><Label>Supplier 1</Label></TableCell>
</TableRow>
<TableRow id="row2" interactive>
<TableCell><Label>Product 2</Label></TableCell>
<TableCell><Label>Supplier 2</Label></TableCell>
</TableRow>
</Table>
);

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(
<Table id="table">
<TableHeaderRow slot="headerRow">
{widths.map((w, i) => (
<TableHeaderCell width={w}>{`Col ${i}`}</TableHeaderCell>
))}
</TableHeaderRow>
<TableRow id="row1">
{widths.map((_, i) => (
<TableCell><Label>{`Cell ${i}`}</Label></TableCell>
))}
</TableRow>
</Table>
);

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(
<Table id="table" rowActionCount={2} overflowMode="Popin">
<TableHeaderRow slot="headerRow">
<TableHeaderCell width="200px" minWidth="200px">Product</TableHeaderCell>
<TableHeaderCell width="200px" minWidth="200px">Supplier</TableHeaderCell>
</TableHeaderRow>
<TableRow id="row1">
<TableCell><Label>Product 1</Label></TableCell>
<TableCell><Label>Supplier 1</Label></TableCell>
<TableRowAction slot="actions" icon="edit" text="Edit"></TableRowAction>
<TableRowAction slot="actions" icon="delete" text="Delete"></TableRowAction>
</TableRow>
</Table>
);

// 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);
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);
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");
cy.wait(50);

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);
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);
expect(position & Node.DOCUMENT_POSITION_FOLLOWING).to.be.greaterThan(0);
});
});

it("should adapt dummy cell border, navigation attribute, and custom focus outline with popin", () => {
cy.mount(
<Table id="table" overflowMode="Popin">
<TableSelectionMulti slot="features" />
<TableHeaderRow slot="headerRow">
<TableHeaderCell width="200px" minWidth="200px">Product</TableHeaderCell>
<TableHeaderCell width="200px" minWidth="200px">Supplier</TableHeaderCell>
</TableHeaderRow>
<TableRow id="row1">
<TableCell><Label>Product 1</Label></TableCell>
<TableCell><Label>Supplier 1</Label></TableCell>
</TableRow>
</Table>
);

// 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");

// 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");
cy.wait(50);

cy.get("#row1").should("have.attr", "_has-popin");

// 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 back - border and nofocus should return, custom outline reapplied via onAfterRendering
cy.get("ui5-table").invoke("css", "width", "800px");
cy.wait(50);

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");
});
});
23 changes: 20 additions & 3 deletions packages/main/src/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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._rowActionCount = this.rows.length > 0 ? this.rowActionCount : 0;
row._renderNavigated = this._renderNavigated;
row._rowActionCount = this.rowActionCount;
row._renderDummyCell = !this._hasFlexibleColumns;
row._alternate = this.alternateRowColors && index % 2 === 0;
});

Expand Down Expand Up @@ -649,8 +650,16 @@ 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) {
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)`);
}

Expand All @@ -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();
}
Expand Down Expand Up @@ -701,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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/TableCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion packages/main/src/TableHeaderRowTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,35 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde
return [<slot name={cell._individualSlot}></slot>];
})}

{ this._renderDummyCell && this._hasPopin &&
<TableHeaderCell id="dummy-cell" role="none" aria-hidden={true}
data-excluded-from-navigation="">
</TableHeaderCell>
}

{ this._rowActionCount > 0 &&
<TableHeaderCell id="actions-cell" aria-colindex={ariaColIndex++}>
<div id="actions-cell-content">{this._i18nRowActions}</div>
</TableHeaderCell>
}

{ this._popinCells.length > 0 &&
{ this._renderNavigated &&
<TableHeaderCell id="navigated-cell"
data-excluded-from-navigation
aria-hidden={true}
role="none"
>
<div id="navigated"></div>
</TableHeaderCell>
}

{ this._renderDummyCell && !this._hasPopin &&
<TableHeaderCell id="dummy-cell" role="none" aria-hidden={true}
data-excluded-from-navigation="nofocus">
</TableHeaderCell>
}

{ this._hasPopin &&
<TableHeaderCell id="popin-cell" aria-colindex={ariaColIndex++} data-excluded-from-navigation>
<div id="popin-cell-content">{this._i18nRowPopin}</div>
</TableHeaderCell>
Expand Down
3 changes: 3 additions & 0 deletions packages/main/src/TableNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 0 additions & 11 deletions packages/main/src/TableRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,6 @@ class TableRow extends TableRowBase<TableCell> {
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<void> {
this.setAttribute("tabindex", "-1");
HTMLElement.prototype.focus.call(this, focusOptions);
return Promise.resolve();
}

async _onpointerdown(e: PointerEvent) {
Expand Down Expand Up @@ -198,10 +191,6 @@ class TableRow extends TableRowBase<TableCell> {
}) !== undefined;
}

get _hasPopin() {
return this.cells.some(c => c._popin && !c._popinHidden);
}

get _rowIndex() {
if (this.position !== undefined) {
return this.position;
Expand Down
Loading
Loading