Skip to content

Commit 347e788

Browse files
committed
feat(table): add support for group rows
Fixes: #10148
1 parent ce09f44 commit 347e788

25 files changed

Lines changed: 810 additions & 38 deletions

File tree

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import Table from "../../src/Table.js";
2+
import TableHeaderRow from "../../src/TableHeaderRow.js";
3+
import TableHeaderCell from "../../src/TableHeaderCell.js";
4+
import TableRow from "../../src/TableRow.js";
5+
import TableCell from "../../src/TableCell.js";
6+
import TableGroupRow from "../../src/TableGroupRow.js";
7+
import TableSelectionMulti from "../../src/TableSelectionMulti.js";
8+
import TableRowActionNavigation from "../../src/TableRowActionNavigation.js";
9+
import Text from "../../src/Text.js";
10+
11+
describe("Table - Group Rows", () => {
12+
function mountGroupedTable() {
13+
cy.mount(
14+
<Table id="table" accessible-name="Grouped Table">
15+
<TableHeaderRow slot="headerRow">
16+
<TableHeaderCell id="colA" width="200px">City</TableHeaderCell>
17+
<TableHeaderCell id="colB" width="200px">Country</TableHeaderCell>
18+
<TableHeaderCell id="colC" width="150px">Population</TableHeaderCell>
19+
</TableHeaderRow>
20+
<TableGroupRow id="group1">
21+
<Text>Country: Germany</Text>
22+
</TableGroupRow>
23+
<TableRow id="row1" rowKey="row-1">
24+
<TableCell><Text>Berlin</Text></TableCell>
25+
<TableCell><Text>Germany</Text></TableCell>
26+
<TableCell><Text>3,748,148</Text></TableCell>
27+
</TableRow>
28+
<TableRow id="row2" rowKey="row-2">
29+
<TableCell><Text>Munich</Text></TableCell>
30+
<TableCell><Text>Germany</Text></TableCell>
31+
<TableCell><Text>1,471,508</Text></TableCell>
32+
</TableRow>
33+
<TableGroupRow id="group2">
34+
<Text>Country: France</Text>
35+
</TableGroupRow>
36+
<TableRow id="row3" rowKey="row-3">
37+
<TableCell><Text>Paris</Text></TableCell>
38+
<TableCell><Text>France</Text></TableCell>
39+
<TableCell><Text>2,161,000</Text></TableCell>
40+
</TableRow>
41+
</Table>
42+
);
43+
}
44+
45+
it("should render group rows and data rows", () => {
46+
mountGroupedTable();
47+
48+
cy.get("[ui5-table-group-row]").should("have.length", 2);
49+
cy.get("[ui5-table-row]:not([ui5-table-group-row])").should("have.length", 3);
50+
cy.get("#group1").should("contain.text", "Country: Germany");
51+
cy.get("#group2").should("contain.text", "Country: France");
52+
});
53+
54+
it("should have aria-roledescription on group rows", () => {
55+
mountGroupedTable();
56+
57+
cy.get("#group1").should("have.attr", "aria-roledescription");
58+
cy.get("#group2").should("have.attr", "aria-roledescription");
59+
});
60+
61+
it("should use role=row on group rows", () => {
62+
mountGroupedTable();
63+
64+
cy.get("#group1").should("have.attr", "role", "row");
65+
cy.get("#table")
66+
.shadow()
67+
.find("#table")
68+
.should("have.attr", "role", "grid");
69+
});
70+
71+
it("should have group cell spanning all columns", () => {
72+
mountGroupedTable();
73+
74+
cy.get("#group1")
75+
.shadow()
76+
.find("#group-cell")
77+
.should("have.attr", "role", "gridcell")
78+
.and("have.attr", "aria-colindex", "1")
79+
.and("have.attr", "aria-colspan", "2");
80+
});
81+
82+
it("should set aria-rowindex sequentially on all rows", () => {
83+
mountGroupedTable();
84+
85+
cy.get("#group1").should("have.attr", "aria-rowindex", "2");
86+
cy.get("#row1").should("have.attr", "aria-rowindex", "3");
87+
cy.get("#row2").should("have.attr", "aria-rowindex", "4");
88+
cy.get("#group2").should("have.attr", "aria-rowindex", "5");
89+
cy.get("#row3").should("have.attr", "aria-rowindex", "6");
90+
});
91+
92+
it("should not be selectable", () => {
93+
cy.mount(
94+
<Table id="table">
95+
<TableSelectionMulti slot="features"></TableSelectionMulti>
96+
<TableHeaderRow slot="headerRow">
97+
<TableHeaderCell width="200px">City</TableHeaderCell>
98+
</TableHeaderRow>
99+
<TableGroupRow id="group1">
100+
<Text>Group</Text>
101+
</TableGroupRow>
102+
<TableRow id="row1" rowKey="row-1">
103+
<TableCell><Text>Berlin</Text></TableCell>
104+
</TableRow>
105+
</Table>
106+
);
107+
108+
cy.get("#group1").should("not.have.attr", "aria-selected");
109+
});
110+
111+
it("should not affect Select All behavior", () => {
112+
cy.mount(
113+
<Table id="table">
114+
<TableSelectionMulti slot="features" id="selection"></TableSelectionMulti>
115+
<TableHeaderRow slot="headerRow">
116+
<TableHeaderCell width="200px">City</TableHeaderCell>
117+
</TableHeaderRow>
118+
<TableGroupRow id="group1">
119+
<Text>Group</Text>
120+
</TableGroupRow>
121+
<TableRow id="row1" rowKey="row-1">
122+
<TableCell><Text>Berlin</Text></TableCell>
123+
</TableRow>
124+
<TableRow id="row2" rowKey="row-2">
125+
<TableCell><Text>Munich</Text></TableCell>
126+
</TableRow>
127+
</Table>
128+
);
129+
130+
// Select all via header checkbox
131+
cy.get("[ui5-table-header-row]")
132+
.shadow()
133+
.find("#selection-component")
134+
.realClick();
135+
136+
// Both data rows should be selected
137+
cy.get("#row1").should("have.attr", "aria-selected", "true");
138+
cy.get("#row2").should("have.attr", "aria-selected", "true");
139+
// Group row should NOT be selected
140+
cy.get("#group1").should("not.have.attr", "aria-selected");
141+
});
142+
143+
it("should reset row alternation after each group header row", () => {
144+
cy.mount(
145+
<Table id="table" alternateRowColors>
146+
<TableHeaderRow slot="headerRow">
147+
<TableHeaderCell width="200px">City</TableHeaderCell>
148+
</TableHeaderRow>
149+
<TableGroupRow id="group1">
150+
<Text>Group 1</Text>
151+
</TableGroupRow>
152+
<TableRow id="rowA" rowKey="a">
153+
<TableCell><Text>A</Text></TableCell>
154+
</TableRow>
155+
<TableRow id="rowB" rowKey="b">
156+
<TableCell><Text>B</Text></TableCell>
157+
</TableRow>
158+
<TableRow id="rowC" rowKey="c">
159+
<TableCell><Text>C</Text></TableCell>
160+
</TableRow>
161+
<TableGroupRow id="group2">
162+
<Text>Group 2</Text>
163+
</TableGroupRow>
164+
<TableRow id="rowD" rowKey="d">
165+
<TableCell><Text>D</Text></TableCell>
166+
</TableRow>
167+
<TableRow id="rowE" rowKey="e">
168+
<TableCell><Text>E</Text></TableCell>
169+
</TableRow>
170+
</Table>
171+
);
172+
173+
// After group1: rowA(1)=not, rowB(2)=alternate, rowC(3)=not
174+
cy.get("#rowA").should("not.have.attr", "_alternate");
175+
cy.get("#rowB").should("have.attr", "_alternate");
176+
cy.get("#rowC").should("not.have.attr", "_alternate");
177+
178+
// After group2: reset → rowD(1)=not, rowE(2)=alternate
179+
cy.get("#rowD").should("not.have.attr", "_alternate");
180+
cy.get("#rowE").should("have.attr", "_alternate");
181+
182+
// Group rows never get _alternate
183+
cy.get("#group1").should("not.have.attr", "_alternate");
184+
cy.get("#group2").should("not.have.attr", "_alternate");
185+
});
186+
187+
it("should not throw with popin mode and group rows", () => {
188+
cy.mount(
189+
<Table id="table" overflowMode="Popin">
190+
<TableHeaderRow slot="headerRow">
191+
<TableHeaderCell minWidth="300px">City</TableHeaderCell>
192+
<TableHeaderCell minWidth="200px">Country</TableHeaderCell>
193+
<TableHeaderCell minWidth="200px">Population</TableHeaderCell>
194+
</TableHeaderRow>
195+
<TableGroupRow id="group1">
196+
<Text>Group</Text>
197+
</TableGroupRow>
198+
<TableRow id="row1" rowKey="row-1">
199+
<TableCell><Text>Berlin</Text></TableCell>
200+
<TableCell><Text>Germany</Text></TableCell>
201+
<TableCell><Text>3,748,148</Text></TableCell>
202+
</TableRow>
203+
</Table>
204+
);
205+
206+
// Shrink to trigger popin
207+
cy.get("#table").invoke("css", "width", "300px");
208+
209+
// Should not throw — table and rows intact
210+
cy.get("#table").should("exist");
211+
cy.get("#group1").should("exist");
212+
cy.get("#row1").should("exist");
213+
214+
// Expand again
215+
cy.get("#table").invoke("css", "width", "800px");
216+
cy.get("#group1").should("contain.text", "Group");
217+
});
218+
219+
it("should be keyboard navigable as a single-cell row", () => {
220+
mountGroupedTable();
221+
222+
// Click on left edge to focus the row itself (not a cell inside)
223+
cy.get("#row1").click("left");
224+
cy.get("#row1").should("be.focused");
225+
226+
// Arrow up should land on the group row
227+
cy.get("#row1").type("{uparrow}");
228+
cy.get("#group1").should("be.focused");
229+
});
230+
231+
it("should expose empty cells array even when children are slotted", () => {
232+
cy.mount(
233+
<Table id="table">
234+
<TableHeaderRow slot="headerRow">
235+
<TableHeaderCell width="200px">Col</TableHeaderCell>
236+
</TableHeaderRow>
237+
<TableGroupRow id="group1">
238+
<Text>Group with content</Text>
239+
</TableGroupRow>
240+
</Table>
241+
);
242+
243+
cy.get("#group1").then(($el) => {
244+
const groupRow = $el[0] as unknown as TableGroupRow;
245+
expect(groupRow.cells).to.have.length(0);
246+
});
247+
});
248+
249+
it("should not render actions cell when table has rowActionCount", () => {
250+
cy.mount(
251+
<Table id="table" rowActionCount={1}>
252+
<TableHeaderRow slot="headerRow">
253+
<TableHeaderCell width="200px">City</TableHeaderCell>
254+
</TableHeaderRow>
255+
<TableGroupRow id="group1">
256+
<Text>Group</Text>
257+
</TableGroupRow>
258+
<TableRow id="row1" rowKey="row-1">
259+
<TableCell><Text>Berlin</Text></TableCell>
260+
<TableRowActionNavigation slot="actions" interactive></TableRowActionNavigation>
261+
</TableRow>
262+
</Table>
263+
);
264+
265+
// Data row should have actions cell
266+
cy.get("#row1")
267+
.shadow()
268+
.find("#actions-cell")
269+
.should("exist");
270+
271+
// Group row should NOT have actions cell
272+
cy.get("#group1")
273+
.shadow()
274+
.find("#actions-cell")
275+
.should("not.exist");
276+
});
277+
});

packages/main/src/Table.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -455,12 +455,23 @@ class Table extends UI5Element {
455455
}
456456

457457
onBeforeRendering(): void {
458+
let alternateIndex = 0;
459+
const hasFlexibleColumns = this._hasFlexibleColumns;
460+
const rowActionCount = this.rowActionCount > 0 && this.rows.length > 0 ? this.rowActionCount : 0;
458461
this._renderNavigated = this.rows.some(row => row.navigated);
459-
[...this.headerRow, ...this.rows].forEach((row, index) => {
460-
row._rowActionCount = this.rows.length > 0 ? this.rowActionCount : 0;
461-
row._renderNavigated = this._renderNavigated;
462-
row._renderDummyCell = !this._hasFlexibleColumns;
463-
row._alternate = this.alternateRowColors && index % 2 === 0;
462+
[...this.headerRow, ...this.rows].forEach(row => {
463+
if (!row.isGroupRow()) {
464+
row._rowActionCount = rowActionCount;
465+
row._renderDummyCell = !hasFlexibleColumns;
466+
row._renderNavigated = this._renderNavigated;
467+
row._alternate = this.alternateRowColors && alternateIndex++ % 2 === 0;
468+
} else {
469+
row._rowActionCount = 0;
470+
row._renderDummyCell = !hasFlexibleColumns && !this._hasPopin;
471+
row._renderNavigated = false;
472+
row._alternate = false;
473+
alternateIndex = 1;
474+
}
464475
});
465476

466477
this.style.setProperty("--ui5_grid_sticky_top", this.stickyTop);
@@ -589,8 +600,8 @@ class Table extends UI5Element {
589600
this.rows.forEach(row => {
590601
const cell = row.cells[headerIndex];
591602
if (cell) {
592-
row.cells[headerIndex]._popinHidden = headerCell.popinHidden;
593-
row.cells[headerIndex]._popin = headerCell._popin;
603+
cell._popinHidden = headerCell.popinHidden;
604+
cell._popin = headerCell._popin;
594605
}
595606
});
596607
}
@@ -652,9 +663,9 @@ class Table extends UI5Element {
652663

653664
// Dummy Cell Width (before actions when popin, after navigated otherwise)
654665
const dummyColumnWidth = !this._hasFlexibleColumns ? "minmax(0, 1fr)" : "";
655-
const hasPopinCells = this.headerRow[0]._popinCells.length > 0;
666+
const hasPopin = this._hasPopin;
656667

657-
if (dummyColumnWidth && hasPopinCells) {
668+
if (dummyColumnWidth && hasPopin) {
658669
widths.push(dummyColumnWidth);
659670
}
660671

@@ -668,13 +679,17 @@ class Table extends UI5Element {
668679
widths.push(`var(--_ui5_table_navigated_cell_width)`);
669680
}
670681

671-
if (dummyColumnWidth && !hasPopinCells) {
682+
if (dummyColumnWidth && !hasPopin) {
672683
widths.push(dummyColumnWidth);
673684
}
674685

675686
return widths.join(" ");
676687
}
677688

689+
get _hasPopin() {
690+
return this.overflowMode === TableOverflowMode.Popin && this.headerRow?.[0]?._hasPopin;
691+
}
692+
678693
get _hasFlexibleColumns(): boolean {
679694
return this.headerRow?.[0]?._visibleCells.some(cell => !isValidColumnWidth(cell.width));
680695
}

0 commit comments

Comments
 (0)