Skip to content

Commit 7bb0aba

Browse files
committed
feat(ui5-table-selection-multi): headerSelector property added
- provides an enumeration property to switch in the multi-select case between a select all and a clear all option (in future further variants might be added). Fixes: #5873
1 parent cbf0dfe commit 7bb0aba

17 files changed

+260
-25
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Label from "../../src/Label.js";
2+
import Table from "../../src/Table.js";
3+
import TableHeaderRow from "../../src/TableHeaderRow.js";
4+
import TableHeaderCell from "../../src/TableHeaderCell.js";
5+
import TableRow from "../../src/TableRow.js";
6+
import TableCell from "../../src/TableCell.js";
7+
import TableSelectionMulti from "../../src/TableSelectionMulti.js";
8+
9+
// packages/main/cypress/specs/TableSelections.cy.test.tsx
10+
11+
describe("TableSelectionMulti headerSelector=ClearAll", () => {
12+
beforeEach(() => {
13+
cy.mount(
14+
<Table id="table2">
15+
<TableSelectionMulti id="selection" selected="1" headerSelector="ClearAll" slot="features"></TableSelectionMulti>
16+
<TableHeaderRow id="headerRow" slot="headerRow">
17+
<TableHeaderCell>ColumnA</TableHeaderCell>
18+
</TableHeaderRow>
19+
<TableRow id="row1" rowKey="1">
20+
<TableCell><Label>Cell A</Label></TableCell>
21+
</TableRow>
22+
<TableRow id="row2" rowKey="2">
23+
<TableCell><Label>Cell B</Label></TableCell>
24+
</TableRow>
25+
</Table>
26+
);
27+
cy.get("#headerRow").shadow().find("#selection-cell").as("headerRowSelectionCell");
28+
});
29+
30+
it("renders ClearAll icon with correct attributes", () => {
31+
cy.get("@headerRowSelectionCell").find('[data-testid="clear-all-icon"]').as("clearAllIcon");
32+
cy.get("@clearAllIcon").should("exist");
33+
cy.get("@clearAllIcon").should("have.attr", "title", "Clear All Selections");
34+
// Optionally check for role, tabindex, or other attributes
35+
});
36+
37+
it("ClearAll icon is active when at least one row is selected", () => {
38+
cy.get("@headerRowSelectionCell").find('[data-testid="clear-all-icon"]').as("clearAllIcon");
39+
cy.get("@clearAllIcon").should("have.class", "active");
40+
});
41+
42+
it("ClearAll icon is deactive when no rows are selected", () => {
43+
// Deselect all rows
44+
cy.get("#selection").invoke("attr", "selected", "");
45+
cy.get("@headerRowSelectionCell").find('[data-testid="clear-all-icon"]').as("clearAllIcon");
46+
cy.get("@clearAllIcon").should("not.have.class", "active");
47+
});
48+
49+
it("ClearAll icon becomes active when a new selected row is added", () => {
50+
// Deselect all first
51+
cy.get("#selection").invoke("attr", "selected", "");
52+
cy.get("@headerRowSelectionCell").find('[data-testid="clear-all-icon"]').as("clearAllIcon");
53+
cy.get("@clearAllIcon").should("not.have.class", "active");
54+
// Add a new selected row
55+
cy.get("#table2").then($table => {
56+
$table.append(
57+
`<ui5-table-row id="row3" row-key="3" selected>
58+
<ui5-table-cell>Cell C</ui5-table-cell>
59+
</ui5-table-row>`
60+
);
61+
});
62+
cy.get("#selection").invoke("attr", "selected", "3");
63+
cy.get("@headerRowSelectionCell").find('[data-testid="clear-all-icon"]').as("clearAllIcon");
64+
cy.get("@clearAllIcon").should("have.class", "active");
65+
});
66+
67+
it("ClearAll icon becomes deactive when the selected row is removed", () => {
68+
// Remove selected row
69+
cy.get("#row1").invoke("remove");
70+
cy.get("#selection").invoke("attr", "selected", "");
71+
cy.get("@headerRowSelectionCell").find('[data-testid="clear-all-icon"]').as("clearAllIcon");
72+
cy.get("@clearAllIcon").should("not.have.class", "active");
73+
});
74+
75+
it("No selection cell or ClearAll icon when all rows are removed", () => {
76+
cy.get("#row1").invoke("remove");
77+
cy.get("#row2").invoke("remove");
78+
cy.get("#headerRow").shadow().find("#selection-cell").should("not.exist");
79+
});
80+
});

packages/main/cypress/specs/TableSelections.cy.tsx

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ Object.entries(testConfig).forEach(([mode, testConfigEntry]) => {
346346
});
347347

348348
describe("TableSelectionMulti", () => {
349-
it("updates the header row checkbox when rows are added or removed", () => {
349+
beforeEach(() => {
350350
cy.mount(
351351
<Table id="table1">
352352
<TableSelectionMulti id="selection" selected="1 2" slot="features"></TableSelectionMulti>
@@ -361,10 +361,14 @@ describe("TableSelectionMulti", () => {
361361
</TableRow>
362362
</Table>
363363
);
364-
365364
cy.get("#headerRow").shadow().find("#selection-cell").as("headerRowSelectionCell");
366-
cy.get("@headerRowSelectionCell").find("#selection-component").as("headerRowCheckBox");
365+
cy.get("#selection").invoke("on", "change", cy.stub().as("selectionChangeSpy"));
366+
});
367+
368+
it("updates the header row checkbox when rows are added or removed", () => {
369+
cy.get("@headerRowSelectionCell").children().first().as("headerRowCheckBox");
367370
cy.get("@headerRowCheckBox").should("have.attr", "checked");
371+
cy.get("@headerRowCheckBox").should("have.attr", "title", "Deselect All Rows");
368372
cy.get("#table1").then($table => {
369373
$table.append(
370374
`<ui5-table-row id="row3" row-key="3">
@@ -374,10 +378,51 @@ describe("TableSelectionMulti", () => {
374378
);
375379
});
376380
cy.get("@headerRowCheckBox").should("not.have.attr", "checked");
381+
cy.get("@headerRowCheckBox").should("have.attr", "title", "Select All Rows");
377382
cy.get("#row3").invoke("remove");
378383
cy.get("@headerRowCheckBox").should("have.attr", "checked");
384+
cy.get("@headerRowCheckBox").should("have.attr", "title", "Deselect All Rows");
379385
cy.get("#row2").invoke("remove");
386+
cy.get("@headerRowCheckBox").should("have.attr", "checked");
380387
cy.get("#row1").invoke("remove");
381-
cy.get("@headerRowCheckBox").should("not.have.attr", "checked");
388+
cy.get("#headerRow").shadow().find("#selection-cell").should("not.exist");
389+
});
390+
391+
it("should handle header-selector=ClearAll", () => {
392+
cy.get("#headerRow").shadow().find("#selection-cell").children().first().as("headerRowIcon");
393+
function checkClearAll(hasSelection: boolean) {
394+
cy.get("@headerRowIcon").should("have.attr", "name", "clear-all");
395+
cy.get("@headerRowIcon").should("have.attr", "mode", "Decorative");
396+
cy.get("@headerRowIcon").should(hasSelection ? "have.attr" : "not.have.attr", "show-tooltip");
397+
cy.get("@headerRowIcon").should("have.attr", "design", hasSelection ? "Default" : "NonInteractive");
398+
if (hasSelection) {
399+
cy.get("@headerRowIcon").should("have.attr", "accessible-name", "Deselect All Rows");
400+
} else {
401+
cy.get("@headerRowIcon").should("not.have.attr", "accessible-name");
402+
}
403+
}
404+
405+
cy.get("#selection").invoke("attr", "header-selector", "ClearAll");
406+
checkClearAll(true);
407+
checkSelectionChangeSpy(0);
408+
409+
cy.get("@headerRowIcon").realClick();
410+
checkClearAll(false)
411+
checkSelection("");
412+
checkSelectionChangeSpy(1);
413+
414+
cy.get("#row1").shadow().find("#selection-component").realClick();
415+
checkClearAll(true);
416+
checkSelection("1");
417+
checkSelectionChangeSpy(2);
418+
419+
cy.get("@headerRowIcon").realPress("Space");
420+
checkClearAll(false)
421+
checkSelection("");
422+
checkSelectionChangeSpy(3);
423+
424+
cy.get("#row2").invoke("remove");
425+
cy.get("#row1").invoke("remove");
426+
cy.get("#headerRow").shadow().find("#selection-cell").should("not.exist");
382427
});
383428
});

packages/main/src/Table.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ class Table extends UI5Element {
613613
const visibleHeaderCells = this.headerRow[0]._visibleCells as TableHeaderCell[];
614614

615615
// Selection Cell Width
616-
if (this._getSelection()?.isRowSelectorRequired()) {
616+
if (this._isRowSelectorRequired) {
617617
widths.push("min-content");
618618
}
619619

@@ -640,6 +640,10 @@ class Table extends UI5Element {
640640
return widths.join(" ");
641641
}
642642

643+
get _isRowSelectorRequired() {
644+
return this.rows.length > 0 && this._getSelection()?.isRowSelectorRequired();
645+
}
646+
643647
get _scrollContainer() {
644648
return this._getVirtualizer() ? this._tableElement : findVerticalScrollContainer(this);
645649
}

packages/main/src/TableHeaderRow.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import TableRowBase from "./TableRowBase.js";
33
import TableHeaderRowTemplate from "./TableHeaderRowTemplate.js";
44
import TableHeaderRowStyles from "./generated/themes/TableHeaderRow.css.js";
55
import type TableHeaderCell from "./TableHeaderCell.js";
6+
import type TableSelectionMulti from "./TableSelectionMulti.js";
67
import {
78
TABLE_SELECTION,
89
TABLE_ROW_POPIN,
910
TABLE_ROW_ACTIONS,
1011
TABLE_COLUMN_HEADER_ROW,
12+
TABLE_SELECT_ALL_ROWS,
13+
TABLE_DESELECT_ALL_ROWS,
1114
} from "./generated/i18n/i18n-defaults.js";
1215

1316
/**
@@ -92,16 +95,33 @@ class TableHeaderRow extends TableRowBase {
9295
return this._isMultiSelect;
9396
}
9497

98+
get _hasSelectedRows() {
99+
return (this._tableSelection as TableSelectionMulti).getSelectedRows().length > 0;
100+
}
101+
102+
get _shouldRenderClearAll() {
103+
return (this._tableSelection as TableSelectionMulti).headerSelector === "ClearAll";
104+
}
105+
95106
get _i18nSelection() {
96107
return TableRowBase.i18nBundle.getText(TABLE_SELECTION);
97108
}
98109

99110
get _i18nRowPopin() {
100111
return TableRowBase.i18nBundle.getText(TABLE_ROW_POPIN);
101112
}
113+
102114
get _i18nRowActions() {
103115
return TableRowBase.i18nBundle.getText(TABLE_ROW_ACTIONS);
104116
}
117+
118+
get _i18nSelectAllRows() {
119+
return TableRowBase.i18nBundle.getText(TABLE_SELECT_ALL_ROWS);
120+
}
121+
122+
get _i18nDeselectAllRows() {
123+
return TableRowBase.i18nBundle.getText(TABLE_DESELECT_ALL_ROWS);
124+
}
105125
}
106126

107127
TableHeaderRow.define();

packages/main/src/TableHeaderRowTemplate.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
import CheckBox from "./CheckBox.js";
22
import TableHeaderCell from "./TableHeaderCell.js";
3+
import Icon from "./Icon.js";
4+
import IconMode from "./types/IconMode.js";
5+
import ClearAll from "@ui5/webcomponents-icons/dist/clear-all.js";
6+
import IconDesign from "./types/IconDesign.js";
37
import type TableHeaderRow from "./TableHeaderRow.js";
48

59
export default function TableHeaderRowTemplate(this: TableHeaderRow) {
610
return (
711
<>
8-
{ this._hasRowSelector &&
12+
{ this._hasSelector &&
913
<TableHeaderCell id="selection-cell"
1014
aria-selected={this._isSelected}
1115
aria-label={this._i18nSelection}
1216
data-ui5-table-cell-fixed
1317
data-ui5-table-selection-component
1418
>
15-
{ this._isMultiSelect &&
16-
<CheckBox id="selection-component"
17-
tabindex={-1}
18-
checked={this._isSelected}
19-
onChange={this._onSelectionChange}
20-
accessibleName={this._i18nRowSelector}
21-
></CheckBox>
19+
{ !this._isMultiSelect ?
20+
<></>
21+
:
22+
this._shouldRenderClearAll ?
23+
<Icon
24+
name={ClearAll}
25+
mode={IconMode.Decorative}
26+
showTooltip={this._hasSelectedRows}
27+
accessibleName={this._hasSelectedRows ? this._i18nDeselectAllRows : undefined}
28+
design={this._hasSelectedRows ? IconDesign.Default : IconDesign.NonInteractive}
29+
onClick={this._onSelectionChange}
30+
></Icon>
31+
:
32+
<CheckBox id="selection-component"
33+
tabindex={-1}
34+
checked={this._isSelected}
35+
onChange={this._onSelectionChange}
36+
accessibleName={this._i18nRowSelector}
37+
title={this._isSelected ? this._i18nDeselectAllRows : this._i18nSelectAllRows}
38+
></CheckBox>
2239
}
2340
</TableHeaderCell>
2441
}

packages/main/src/TableRow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class TableRow extends TableRowBase {
142142

143143
_onclick() {
144144
if (this === getActiveElement()) {
145-
if (this._isSelectable && !this._hasRowSelector) {
145+
if (this._isSelectable && !this._hasSelector) {
146146
this._onSelectionChange();
147147
} else if (this.interactive) {
148148
this._table?._onRowClick(this);
@@ -165,7 +165,7 @@ class TableRow extends TableRowBase {
165165
}
166166

167167
get _isInteractive() {
168-
return this.interactive || (this._isSelectable && !this._hasRowSelector);
168+
return this.interactive || (this._isSelectable && !this._hasSelector);
169169
}
170170

171171
get _hasOverflowActions() {

packages/main/src/TableRowBase.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ abstract class TableRowBase extends UI5Element {
101101
return !!this._tableSelection?.isMultiSelectable();
102102
}
103103

104-
get _hasRowSelector() {
105-
return !!this._tableSelection?.isRowSelectorRequired();
104+
get _hasSelector() {
105+
return this._table?._isRowSelectorRequired;
106106
}
107107

108108
get _visibleCells() {

packages/main/src/TableRowTemplate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type TableRow from "./TableRow.js";
88
export default function TableRowTemplate(this: TableRow) {
99
return (
1010
<>
11-
{ this._hasRowSelector &&
11+
{ this._hasSelector &&
1212
<TableCell
1313
id="selection-cell"
1414
aria-selected={this._isSelected}

packages/main/src/TableSelectionMulti.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isSelectionCheckbox, isHeaderSelector, findRowInPath } from "./TableUti
55
import { isUpShift } from "@ui5/webcomponents-base/dist/Keys.js";
66
import type TableRow from "./TableRow.js";
77
import type TableRowBase from "./TableRowBase.js";
8+
import type TableSelectionMultiHeaderSelector from "./types/TableSelectionMultiHeaderSelector.js";
89

910
/**
1011
* @class
@@ -47,6 +48,16 @@ class TableSelectionMulti extends TableSelectionBase {
4748
@property()
4849
selected?: string;
4950

51+
/**
52+
* Defines the selector of the header row.
53+
*
54+
* @default "SelectAll"
55+
* @public
56+
* @since 2.12
57+
*/
58+
@property()
59+
headerSelector: `${TableSelectionMultiHeaderSelector}` = "SelectAll";
60+
5061
private _rowsLength = 0;
5162
private _rangeSelection?: {
5263
selected: boolean,
@@ -69,7 +80,7 @@ class TableSelectionMulti extends TableSelectionBase {
6980

7081
isSelected(row: TableRowBase): boolean {
7182
if (row.isHeaderRow()) {
72-
return this.areAllRowsSelected();
83+
return this.headerSelector === "ClearAll" ? true : this.areAllRowsSelected();
7384
}
7485

7586
const rowKey = this.getRowKey(row as TableRow);
@@ -81,15 +92,26 @@ class TableSelectionMulti extends TableSelectionBase {
8192
return;
8293
}
8394

95+
let selectionChanged = false;
8496
const tableRows = row.isHeaderRow() ? this._table!.rows : [row as TableRow];
8597
const selectedSet = this.getSelectedAsSet();
8698
tableRows.forEach(tableRow => {
8799
const rowKey = this.getRowKey(tableRow);
100+
if (!rowKey) {
101+
return;
102+
}
103+
104+
const setSize = selectedSet.size;
88105
selectedSet[selected ? "add" : "delete"](rowKey);
106+
if (!selectionChanged && setSize !== selectedSet.size) {
107+
selectionChanged = true;
108+
}
89109
});
90110

91-
this.setSelectedAsSet(selectedSet);
92-
fireEvent && this.fireDecoratorEvent("change");
111+
if (selectionChanged) {
112+
this.setSelectedAsSet(selectedSet);
113+
fireEvent && this.fireDecoratorEvent("change");
114+
}
93115
}
94116

95117
/**

packages/main/src/i18n/messagebundle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,10 @@ TABLE_ROW_ACTIONS=Row Actions
667667
TABLE_NAVIGATION=Navigation
668668
#XTOL: Tooltip for the AI button in the column header to indicate that the column is generated by AI
669669
TABLE_GENERATED_BY_AI=Generated by AI
670+
#XTOL: Tooltip of the header row checkbox to select all rows in the table
671+
TABLE_SELECT_ALL_ROWS=Select All Rows
672+
#XTOL: Tooltip of the header row checkbox to deselect all rows in the table
673+
TABLE_DESELECT_ALL_ROWS=Deselect All Rows
670674

671675
#XFLD: Text for the "Yesterday" option in the DynamicDateRange component.
672676
DYNAMIC_DATE_RANGE_YESTERDAY_TEXT=Yesterday

0 commit comments

Comments
 (0)