diff --git a/REUSE.toml b/REUSE.toml
index 0ab823c6285..0aac9260a43 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -38,3 +38,9 @@ path = "packages/main/src/internal/safeGetChildrenArray.ts"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Adobe Inc."
SPDX-License-Identifier = "Apache-2.0"
+
+[[annotations]]
+path = "packages/main/src/components/AnalyticalTable/hooks/useRowSelect.ts"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2019-2021 Tanner Linsley"
+SPDX-License-Identifier = "MIT"
diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
index 587f171af94..87636bdcc8c 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
@@ -627,7 +627,7 @@ describe('AnalyticalTable', () => {
filterable
columns={columns}
onRowSelect={(e) => {
- const { allRowsSelected, isSelected, row, rowsById, selectedRowIds } = e.detail;
+ const { allRowsSelected, allVisibleRowsSelected, isSelected, row, rowsById, selectedRowIds } = e.detail;
const selectedRowIdsArrayMapped = Object.keys(selectedRowIds).reduce((acc, key) => {
if (selectedRowIds[key]) {
acc.push(rowsById[key]);
@@ -636,6 +636,7 @@ describe('AnalyticalTable', () => {
}, []);
setRelevantPayload({
allRowsSelected,
+ allVisibleRowsSelected,
isSelected,
row: row.id,
selectedFlatRows: selectedRowIdsArrayMapped.map((item) => ({
@@ -654,6 +655,8 @@ describe('AnalyticalTable', () => {
{JSON.stringify(relevantPayload?.selectedFlatRows?.filter(Boolean).length)}
{JSON.stringify(relevantPayload?.selectedRowIds)}
+
{`${relevantPayload?.allRowsSelected ?? false}`}
+ {`${relevantPayload?.allVisibleRowsSelected ?? false}`}
>
);
};
@@ -716,6 +719,16 @@ describe('AnalyticalTable', () => {
});
cy.get('@onRowSelectSpy').should('have.callCount', 4);
cy.findByTestId('payloadHelper').should('have.text', '4{"0":true,"1":true,"0.2":true,"0.2.0":true}');
+
+ cy.findByTestId('payloadAllRowsSelected').should('have.text', 'false');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'true');
+
+ cy.findByText('Name').click();
+ cy.get(`[ui5-input][show-clear-icon]`).typeIntoUi5Input('{selectall}{backspace}{enter}', { force: true });
+
+ cy.findByText('Flowers Mcfarland').click();
+ cy.findByTestId('payloadAllRowsSelected').should('have.text', 'false');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'false');
});
it('programmatic and user selection + filtering', () => {
@@ -737,6 +750,7 @@ describe('AnalyticalTable', () => {
const [selectedFlatRows, setSelectedFlatRows] = useState([]);
const [selectedRowIdsCb, setSelectedRowIdsCb] = useState({});
const [allRowsSelected, setAllRowsSelected] = useState(false);
+ const [allVisibleRowsSelected, setAllVisibleRowsSelected] = useState(false);
const [globalFilterVal, setGlobalFilterVal] = useState('');
return (
<>
@@ -764,6 +778,7 @@ describe('AnalyticalTable', () => {
setSelectedFlatRows(selectedRowIdsArrayMapped.map((item) => item.id));
setSelectedRowIdsCb(e.detail.selectedRowIds);
setAllRowsSelected(e.detail.allRowsSelected);
+ setAllVisibleRowsSelected(e.detail.allVisibleRowsSelected);
onRowSelect(e);
}}
onFilter={filterSpy}
@@ -781,6 +796,10 @@ describe('AnalyticalTable', () => {
"e.detail.allRowsSelected:"
{`${allRowsSelected}`}
+
+ "e.detail.allVisibleRowsSelected:"
+ {`${allVisibleRowsSelected}`}
+
>
);
};
@@ -868,6 +887,7 @@ describe('AnalyticalTable', () => {
cy.findByTestId('payload').should('have.text', '["0","1","5","7","17","20"]');
cy.findByTestId('payloadRowsById').should('have.text', '{"0":true,"1":true,"5":true,"7":true,"17":true,"20":true}');
cy.findByTestId('payloadAllRowsSelected').should('have.text', 'false');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'true');
cy.findByText('Name').click();
cy.get('[ui5-li-custom]').shadow().get('[ui5-input]').typeIntoUi5Input('{selectall}{backspace}{enter}');
@@ -892,6 +912,7 @@ describe('AnalyticalTable', () => {
'{"0":true,"1":true,"2":true,"3":true,"4":true,"5":true,"6":true,"7":true,"8":true,"9":true,"10":true,"11":true,"12":true,"13":true,"14":true,"15":true,"16":true,"18":true,"19":true,"20":true}',
);
cy.findByTestId('payloadAllRowsSelected').should('have.text', 'false');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'false');
cy.findByText('Name-17').click();
cy.findByTestId('payload').should(
'have.text',
@@ -902,6 +923,7 @@ describe('AnalyticalTable', () => {
'{"0":true,"1":true,"2":true,"3":true,"4":true,"5":true,"6":true,"7":true,"8":true,"9":true,"10":true,"11":true,"12":true,"13":true,"14":true,"15":true,"16":true,"17":true,"18":true,"19":true,"20":true}',
);
cy.findByTestId('payloadAllRowsSelected').should('have.text', 'true');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'true');
cy.findByText('Name').click();
cy.get('[ui5-li-custom]').shadow().get('[ui5-input]').typeIntoUi5Input('{selectall}{backspace}{enter}');
@@ -918,6 +940,7 @@ describe('AnalyticalTable', () => {
'{"0":true,"1":true,"2":true,"3":true,"4":true,"5":true,"6":true,"7":true,"8":true,"9":true,"10":true,"11":true,"12":true,"13":true,"14":true,"15":true,"16":true,"18":true,"19":true,"20":true}',
);
cy.findByTestId('payloadAllRowsSelected').should('have.text', 'false');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'false');
cy.findByText('Name-17').click();
cy.findByTestId('payload').should(
'have.text',
@@ -928,6 +951,7 @@ describe('AnalyticalTable', () => {
'{"0":true,"1":true,"2":true,"3":true,"4":true,"5":true,"6":true,"7":true,"8":true,"9":true,"10":true,"11":true,"12":true,"13":true,"14":true,"15":true,"16":true,"17":true,"18":true,"19":true,"20":true}',
);
cy.findByTestId('payloadAllRowsSelected').should('have.text', 'true');
+ cy.findByTestId('payloadAllVisibleRowsSelected').should('have.text', 'true');
cy.get('@onRowSelectSpy').should('have.callCount', 19);
});
@@ -1005,7 +1029,7 @@ describe('AnalyticalTable', () => {
columns={columns}
tableInstance={tableInstance}
onRowSelect={(e) => {
- const { allRowsSelected, isSelected, row, rowsById, selectedRowIds } = e.detail;
+ const { allRowsSelected, allVisibleRowsSelected, isSelected, row, rowsById, selectedRowIds } = e.detail;
const selectedRowIdsArrayMapped = Object.keys(selectedRowIds).reduce((acc, key) => {
if (selectedRowIds[key]) {
acc.push(rowsById[key]);
@@ -1014,8 +1038,9 @@ describe('AnalyticalTable', () => {
}, []);
setRelevantPayload({
allRowsSelected,
+ allVisibleRowsSelected,
isSelected,
- row: row.id,
+ row: row?.id,
selectedFlatRows: selectedRowIdsArrayMapped.map((item) => ({
id: item?.id,
})),
@@ -1032,17 +1057,25 @@ describe('AnalyticalTable', () => {
{JSON.stringify(relevantPayload?.selectedRowIds)}
{`${relevantPayload.isSelected}`}
+ {`${relevantPayload.allRowsSelected}`}
+ {`${relevantPayload.allVisibleRowsSelected}`}
>
);
};
const select = cy.spy().as('onRowSelectSpy');
cy.mount();
+ const selectAllCheckbox = '[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]';
+ const selectAllCell = '[data-column-id="__ui5wcr__internal_selection_column"]';
+
cy.findByText('QWE').click();
cy.get('@onRowSelectSpy').should('have.callCount', 1);
cy.findByTestId('selectedFlatRowsLength').should('have.text', '1');
cy.findByTestId('selectedRowIds').should('have.text', '{"2":true}');
cy.findByTestId('isSelected').should('have.text', 'true');
+ cy.findByTestId('allRowsSelected').should('have.text', 'false');
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'false');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
cy.findByText('Friend Name').click();
cy.get('[ui5-list]').clickUi5ListItemByText('Group');
@@ -1058,12 +1091,28 @@ describe('AnalyticalTable', () => {
cy.findByTestId('selectedFlatRowsLength').should('have.text', '2');
cy.findByTestId('selectedRowIds').should('have.text', '{"2":true,"4":true}');
cy.findByTestId('isSelected').should('have.text', 'true');
+ cy.findByTestId('allRowsSelected').should('have.text', 'false');
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'false');
cy.findByText('25').click();
cy.get('@onRowSelectSpy').should('have.callCount', 3);
cy.findByTestId('selectedFlatRowsLength').should('have.text', '1');
cy.findByTestId('selectedRowIds').should('have.text', '{"2":true}');
cy.findByTestId('isSelected').should('have.text', 'false');
+ cy.findByTestId('allRowsSelected').should('have.text', 'false');
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'false');
+
+ cy.get(selectAllCell).click();
+ cy.get('@onRowSelectSpy').should('have.callCount', 4);
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'true');
+ cy.get(selectAllCheckbox).should('not.have.attr', 'indeterminate');
+ cy.get(selectAllCheckbox).should('have.attr', 'checked');
+
+ cy.get(selectAllCell).click();
+ cy.get('@onRowSelectSpy').should('have.callCount', 5);
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'false');
+ cy.get(selectAllCheckbox).should('not.have.attr', 'indeterminate');
+ cy.get(selectAllCheckbox).should('not.have.attr', 'checked');
cy.findByText('Friend Name').click();
cy.get('[ui5-list]').clickUi5ListItemByText('Ungroup');
@@ -1175,19 +1224,11 @@ describe('AnalyticalTable', () => {
cy.findByTestId('selectedRows').should('have.text', '{"1.0.0.0":true,"1.0.0.1":true,"1.0.0.2":true}');
cy.findByText('Selma Kaufman').click();
- if (reactVersion.startsWith('19')) {
- // ToDo: the parent row isn't included in the `setSelectedRowIds` anymore - check if it's feasible to include it again, otherwise add a note to the hook
- cy.findByTestId('selectedRows').should(
- 'have.text',
- // '{"1.0.0.0":true,"1.0.0.1":true,"1.0.0.2":true,"1.0.0.3":true,"1.0.0":true}'
- '{"1.0.0.0":true,"1.0.0.1":true,"1.0.0.2":true,"1.0.0.3":true}',
- );
- } else {
- cy.findByTestId('selectedRows').should(
- 'have.text',
- '{"1.0.0.0":true,"1.0.0.1":true,"1.0.0.2":true,"1.0.0.3":true,"1.0.0":true}',
- );
- }
+ // parent row "1.0.0" is automatically added when all children are selected
+ cy.findByTestId('selectedRows').should(
+ 'have.text',
+ '{"1.0.0.0":true,"1.0.0.1":true,"1.0.0.2":true,"1.0.0.3":true,"1.0.0":true}',
+ );
});
it('useIndeterminateRowSelection', () => {
@@ -1850,16 +1891,35 @@ describe('AnalyticalTable', () => {
);
cy.mount();
cy.get('.ui5-busy-indicator-busy-area', { timeout: 2000 }).should('not.exist');
- cy.mount();
+ cy.mount();
cy.findByText('No data').should('be.visible');
- cy.mount();
+ cy.get('[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]').should('not.exist');
+ cy.mount(
+ ,
+ );
cy.findByText('No data found. Try adjusting the filter settings.').should('be.visible');
- cy.mount();
+ cy.get('[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]').should('not.exist');
+ cy.mount(
+ ,
+ );
cy.findByText('Lorem').should('be.visible');
+ cy.get('[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]').should('exist');
cy.findByText('Name').realClick();
cy.get('[ui5-input]').typeIntoUi5Input('test123');
cy.findByText('Lorem').should('not.exist');
cy.findByText('No data found. Try adjusting the filter settings.').should('be.visible');
+ cy.get('[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]').should('not.exist');
});
it('NoDataComponent', () => {
@@ -3132,7 +3192,7 @@ describe('AnalyticalTable', () => {
const TestComp = () => {
const [stringifiedPl, setStringifiedPl] = useState('');
const handleSelect = (e) => {
- const { allRowsSelected, rowsById, selectedRowIds } = e.detail;
+ const { allRowsSelected, allVisibleRowsSelected, rowsById, selectedRowIds } = e.detail;
const selectedRowIdsArrayMapped = Object.keys(selectedRowIds).reduce((acc, key) => {
if (selectedRowIds[key]) {
@@ -3148,6 +3208,7 @@ describe('AnalyticalTable', () => {
id: item?.id,
})),
allRowsSelected,
+ allVisibleRowsSelected,
}),
);
select(e);
@@ -3179,28 +3240,172 @@ describe('AnalyticalTable', () => {
cy.get('@selAll').should('have.attr', 'title', 'Deselect All');
cy.findByTestId('payload').should(
'have.text',
- '{"selectedRowIds":{"0":true,"1":true,"2":true,"3":true},"selectedFlatRows":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}],"allRowsSelected":true}',
+ '{"selectedRowIds":{"0":true,"1":true,"2":true,"3":true},"selectedFlatRows":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}],"allRowsSelected":true,"allVisibleRowsSelected":true}',
);
cy.findByText('X').click();
cy.get('@selectSpy').should('have.been.calledTwice');
cy.findByTestId('payload').should(
'have.text',
- '{"selectedRowIds":{"0":true,"1":true,"3":true},"selectedFlatRows":[{"id":"0"},{"id":"1"},{"id":"3"}],"allRowsSelected":false}',
+ '{"selectedRowIds":{"0":true,"1":true,"3":true},"selectedFlatRows":[{"id":"0"},{"id":"1"},{"id":"3"}],"allRowsSelected":false,"allVisibleRowsSelected":false}',
);
cy.get('@selAll').should('have.attr', 'title', 'Select All').click();
cy.get('@selectSpy').should('have.been.calledThrice');
cy.findByTestId('payload').should(
'have.text',
- '{"selectedRowIds":{"0":true,"1":true,"2":true,"3":true},"selectedFlatRows":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}],"allRowsSelected":true}',
+ '{"selectedRowIds":{"0":true,"1":true,"2":true,"3":true},"selectedFlatRows":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"}],"allRowsSelected":true,"allVisibleRowsSelected":true}',
);
cy.get('@selAll').click();
cy.get('@selectSpy').should('have.callCount', 4);
cy.findByTestId('payload').should(
'have.text',
- '{"selectedRowIds":{},"selectedFlatRows":[],"allRowsSelected":false}',
+ '{"selectedRowIds":{},"selectedFlatRows":[],"allRowsSelected":false,"allVisibleRowsSelected":false}',
);
});
+ it('select-all with filtered rows', () => {
+ const select = cy.spy().as('selectSpy');
+ // John, Jane, Bob, Alice - filtering 'J' gives John, Jane (excludes Bob, Alice)
+ const filterData = mockNames.slice(0, 4).map((name) => ({ name }));
+ const filterColumns: AnalyticalTableColumnDefinition[] = [{ Header: 'Name', accessor: 'name' }];
+ const TestComp = () => {
+ const [filter, setFilter] = useState('');
+ const [payload, setPayload] = useState<{
+ allRowsSelected?: boolean;
+ allVisibleRowsSelected?: boolean;
+ selectedRowIds?: Record;
+ }>({});
+
+ const handleRowSelect: AnalyticalTablePropTypes['onRowSelect'] = (e) => {
+ const { allRowsSelected, allVisibleRowsSelected, selectedRowIds } = e.detail;
+ setPayload({ allRowsSelected, allVisibleRowsSelected, selectedRowIds });
+ select(e);
+ };
+ return (
+ <>
+ setFilter(e.target.value)} />
+
+ {`${payload.allRowsSelected}`}
+ {`${payload.allVisibleRowsSelected}`}
+ {JSON.stringify(payload.selectedRowIds)}
+ >
+ );
+ };
+ cy.mount();
+
+ const selectAllCheckbox = '[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]';
+ const selectAllCell = '[data-column-id="__ui5wcr__internal_selection_column"]';
+
+ cy.get(selectAllCheckbox).should('not.have.attr', 'indeterminate');
+ cy.get(selectAllCheckbox).should('not.have.attr', 'checked');
+
+ // filtered 0/2 (0/4)
+ cy.findByTestId('filterInput').typeIntoUi5Input('J');
+ cy.findByText('Bob').should('not.exist');
+ cy.findByText('Alice').should('not.exist');
+
+ // filtered 1/2 (1/4)
+ cy.findByText('John').click();
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+ cy.findByTestId('allRowsSelected').should('have.text', 'false');
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'false');
+
+ // filtered 2/2 (2/4)
+ cy.findByText('Jane').click();
+ cy.get(selectAllCheckbox).should('not.have.attr', 'indeterminate');
+ cy.get(selectAllCheckbox).should('have.attr', 'checked');
+ cy.findByTestId('allRowsSelected').should('have.text', 'false');
+ cy.findByTestId('allVisibleRowsSelected').should('have.text', 'true');
+
+ // 2/4
+ cy.findByTestId('filterInput').typeIntoUi5Input('{selectall}{backspace}');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ // 3/4
+ cy.findByText('Bob').click();
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ // filtered 2/2 (3/4)
+ cy.findByTestId('filterInput').typeIntoUi5Input('J');
+ cy.get(selectAllCheckbox).should('have.attr', 'checked');
+ cy.get(selectAllCheckbox).should('not.have.attr', 'indeterminate');
+
+ // filtered 1/2 (2/4)
+ cy.findByText('John').click();
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ // filtered 0/2 (1/4)
+ cy.findByText('Jane').click();
+ cy.get(selectAllCheckbox).should('not.have.attr', 'indeterminate');
+ cy.get(selectAllCheckbox).should('not.have.attr', 'checked');
+
+ // filtered 2/2 (3/4)
+ cy.get(selectAllCell).click();
+ cy.get(selectAllCheckbox).should('have.attr', 'checked');
+
+ // 3/4
+ cy.findByTestId('filterInput').typeIntoUi5Input('{selectall}{backspace}');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+ });
+
+ it('selection state preserved during loading/overlay', () => {
+ const TestComp = () => {
+ const [loading, setLoading] = useState(false);
+ const [showOverlay, setShowOverlay] = useState(false);
+ return (
+ <>
+
+
+
+ >
+ );
+ };
+ cy.mount();
+
+ const selectAllCheckbox = '[data-column-id="__ui5wcr__internal_selection_column"] [ui5-checkbox]';
+
+ cy.findByText('A').click();
+ cy.findByText('B').click();
+ cy.get('[aria-rowindex="2"]').should('have.attr', 'data-is-selected');
+ cy.get('[aria-rowindex="3"]').should('have.attr', 'data-is-selected');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ cy.findByTestId('toggleLoading').click();
+ cy.get('[aria-rowindex="2"]').should('have.attr', 'data-is-selected');
+ cy.get('[aria-rowindex="3"]').should('have.attr', 'data-is-selected');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ cy.findByTestId('toggleLoading').click();
+ cy.get('[aria-rowindex="2"]').should('have.attr', 'data-is-selected');
+ cy.get('[aria-rowindex="3"]').should('have.attr', 'data-is-selected');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ cy.findByTestId('toggleOverlay').click();
+ cy.get('[aria-rowindex="2"]').should('have.attr', 'data-is-selected');
+ cy.get('[aria-rowindex="3"]').should('have.attr', 'data-is-selected');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+
+ cy.findByTestId('toggleOverlay').click();
+ cy.get('[aria-rowindex="2"]').should('have.attr', 'data-is-selected');
+ cy.get('[aria-rowindex="3"]').should('have.attr', 'data-is-selected');
+ cy.get(selectAllCheckbox).should('have.attr', 'indeterminate');
+ });
+
it('manualGroupBy - backend grouping', () => {
const cols = [
{
diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css b/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css
index 8e1149631f6..570c239800e 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css
@@ -369,14 +369,20 @@
user-select: none;
}
-.checkBox::part(root) {
- display: flex;
- width: unset;
- height: unset;
- justify-content: center;
- min-height: unset;
- min-width: unset;
- padding: 0;
+.checkBox {
+ vertical-align: middle;
+ pointer-events: none;
+ display: block;
+
+ &::part(root) {
+ display: flex;
+ width: unset;
+ height: unset;
+ justify-content: center;
+ min-height: unset;
+ min-width: unset;
+ padding: 0;
+ }
}
/* ==========================================================================
diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx
index 40cefa4437f..aedcb2ae4b2 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.tsx
@@ -169,7 +169,29 @@ const meta = {
chromatic: { disableSnapshot: true },
},
args: {
- data: dataLarge,
+ // reactTableOptions: { autoResetSelectedRows: true },
+ data: [
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ...dataLarge,
+ ],
columns: [
{
Header: 'Name',
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useRowSelect.ts b/packages/main/src/components/AnalyticalTable/hooks/useRowSelect.ts
new file mode 100644
index 00000000000..33c42ce8203
--- /dev/null
+++ b/packages/main/src/components/AnalyticalTable/hooks/useRowSelect.ts
@@ -0,0 +1,377 @@
+import { useCallback } from 'react';
+import { actions, makePropGetter, ensurePluginOrder, useGetLatest, useMountedLayoutEffect } from 'react-table';
+import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
+import type { ReactTableHooks, RowType, TableInstance } from '../types/index.js';
+
+const pluginName = 'useRowSelect';
+
+// Actions
+actions.resetSelectedRows = 'resetSelectedRows';
+actions.toggleAllRowsSelected = 'toggleAllRowsSelected';
+actions.toggleRowSelected = 'toggleRowSelected';
+actions.toggleAllPageRowsSelected = 'toggleAllPageRowsSelected';
+
+const noopToggle = () => {};
+const noopGetProps = () => ({});
+const emptyArray: RowType[] = [];
+
+/**
+ * UI5WCR optimized version of react-table v7's useRowSelect hook.
+ * Original source: https://github.com/TanStack/table/blob/v7/src/plugin-hooks/useRowSelect.js
+ *
+ * This is a fork of react-table's `useRowSelect` with performance optimizations:
+ * - Early exit when `selectionMode` is 'None'
+ * - Skips `selectedFlatRows` computation, `isAllRowsSelected` checks, and `prepareRow` overhead when selection is disabled
+ * - `isAllRowsSelected` computation is memoized
+ * - Uses stable noop references when disabled
+ * - Fixes select-all indeterminate state considering filtered-out rows (now only visible rows are considered)
+ *
+ * _Pagination specific implementation where adjusted as well, although they are currently not being used_
+ */
+export const useRowSelect = (hooks: ReactTableHooks) => {
+ hooks.getToggleRowSelectedProps = [defaultGetToggleRowSelectedProps];
+ hooks.getToggleAllRowsSelectedProps = [defaultGetToggleAllRowsSelectedProps];
+ hooks.getToggleAllPageRowsSelectedProps = [defaultGetToggleAllPageRowsSelectedProps];
+ hooks.stateReducers.push(reducer);
+ hooks.useInstance.push(useInstance);
+ hooks.prepareRow.push(prepareRow);
+};
+
+useRowSelect.pluginName = pluginName;
+
+const defaultGetToggleRowSelectedProps = (
+ props: Record,
+ { instance, row }: { instance: TableInstance; row: RowType },
+) => {
+ const { manualRowSelectedKey = 'isSelected', webComponentsReactProperties } = instance;
+ // UI5WCR: use className instead of inline style
+ const { classes } = webComponentsReactProperties;
+ let checked = false;
+
+ if (row.original && row.original[manualRowSelectedKey]) {
+ checked = true;
+ } else {
+ checked = row.isSelected;
+ }
+
+ return [
+ props,
+ {
+ onChange: (e: { target: { checked: boolean } }) => {
+ row.toggleRowSelected(e.target.checked);
+ },
+ // UI5WCR: removed style/title, added className
+ className: classes.checkBox,
+ checked,
+ indeterminate: row.isSomeSelected,
+ },
+ ];
+};
+
+const defaultGetToggleAllRowsSelectedProps = (
+ props: Record,
+ { instance }: { instance: TableInstance },
+) => {
+ // UI5WCR: use className instead of inline style
+ const { classes } = instance.webComponentsReactProperties;
+ return [
+ props,
+ {
+ onChange: (e: { target: { checked: boolean } }) => {
+ instance.toggleAllRowsSelected?.(e.target.checked);
+ },
+ // UI5WCR: removed style/title, added className
+ className: classes.checkBox,
+ checked: instance.isAllRowsSelected,
+ // UI5WCR: use selectedFlatRows (visible rows only) instead of selectedRowIds
+ indeterminate: Boolean(!instance.isAllRowsSelected && instance.selectedFlatRows?.length),
+ },
+ ];
+};
+
+const defaultGetToggleAllPageRowsSelectedProps = (
+ props: Record,
+ { instance }: { instance: TableInstance },
+) => {
+ // UI5WCR: use className instead of inline style
+ const { classes } = instance.webComponentsReactProperties;
+ return [
+ props,
+ {
+ onChange: (e: { target: { checked: boolean } }) => {
+ instance.toggleAllPageRowsSelected?.(e.target.checked);
+ },
+ // UI5WCR: removed style/title, added className
+ className: classes.checkBox,
+ checked: instance.isAllPageRowsSelected,
+ indeterminate: Boolean(
+ !instance.isAllPageRowsSelected &&
+ instance.page?.some(({ id }: { id: string }) => instance.state.selectedRowIds[id]),
+ ),
+ },
+ ];
+};
+
+const reducer: TableInstance['stateReducer'] = (state, action, _previousState, instance) => {
+ if (action.type === actions.init) {
+ return {
+ selectedRowIds: {},
+ ...state,
+ };
+ }
+
+ if (action.type === actions.resetSelectedRows) {
+ return {
+ ...state,
+ selectedRowIds: instance.initialState.selectedRowIds || {},
+ };
+ }
+
+ if (action.type === actions.toggleAllRowsSelected) {
+ const { value: setSelected } = action;
+ const { isAllRowsSelected, rowsById, nonGroupedRowsById = rowsById } = instance;
+
+ const selectAll = typeof setSelected !== 'undefined' ? setSelected : !isAllRowsSelected;
+
+ const selectedRowIds = { ...state.selectedRowIds };
+
+ for (const rowId in nonGroupedRowsById) {
+ if (selectAll) {
+ selectedRowIds[rowId] = true;
+ } else {
+ delete selectedRowIds[rowId];
+ }
+ }
+
+ return { ...state, selectedRowIds };
+ }
+
+ if (action.type === actions.toggleRowSelected) {
+ const { id, value: setSelected } = action;
+ const { rowsById, selectSubRows = true, getSubRows } = instance;
+ const isSelected = state.selectedRowIds[id];
+ const shouldExist = typeof setSelected !== 'undefined' ? setSelected : !isSelected;
+
+ if (isSelected === shouldExist) {
+ return state;
+ }
+
+ const newSelectedRowIds = { ...state.selectedRowIds };
+
+ const handleRowById = (rowId: string) => {
+ const row = rowsById[rowId];
+
+ if (row) {
+ if (!row.isGrouped) {
+ if (shouldExist) {
+ newSelectedRowIds[rowId] = true;
+ } else {
+ delete newSelectedRowIds[rowId];
+ }
+ }
+
+ if (selectSubRows) {
+ const subRows = getSubRows(row);
+ if (subRows) {
+ subRows.forEach((r: RowType) => {
+ handleRowById(r.id);
+ });
+ }
+ }
+ }
+ };
+
+ handleRowById(id);
+
+ return { ...state, selectedRowIds: newSelectedRowIds };
+ }
+
+ if (action.type === actions.toggleAllPageRowsSelected) {
+ const { value: setSelected } = action;
+ const { page, rowsById, selectSubRows = true, isAllPageRowsSelected, getSubRows } = instance;
+
+ const selectAll = typeof setSelected !== 'undefined' ? setSelected : !isAllPageRowsSelected;
+
+ const newSelectedRowIds = { ...state.selectedRowIds };
+
+ const handleRowById = (rowId: string) => {
+ const row = rowsById[rowId];
+
+ if (!row.isGrouped) {
+ if (selectAll) {
+ newSelectedRowIds[rowId] = true;
+ } else {
+ delete newSelectedRowIds[rowId];
+ }
+ }
+
+ if (selectSubRows) {
+ const subRows = getSubRows(row);
+
+ if (subRows) {
+ subRows.forEach((r: RowType) => {
+ handleRowById(r.id);
+ });
+ }
+ }
+ };
+
+ if (page) {
+ page.forEach((row: RowType) => {
+ handleRowById(row.id);
+ });
+ }
+
+ return { ...state, selectedRowIds: newSelectedRowIds };
+ }
+
+ return state;
+};
+
+function useInstance(instance: TableInstance) {
+ const {
+ data,
+ rows,
+ getHooks,
+ plugins,
+ rowsById,
+ nonGroupedRowsById = rowsById,
+ autoResetSelectedRows = true,
+ state: { selectedRowIds },
+ selectSubRows = true,
+ dispatch,
+ page,
+ getSubRows,
+ webComponentsReactProperties,
+ } = instance;
+
+ const { selectionMode } = webComponentsReactProperties;
+ const isSelectionEnabled = selectionMode !== AnalyticalTableSelectionMode.None;
+
+ ensurePluginOrder(plugins, ['useFilters', 'useGroupBy', 'useSortBy', 'useExpanded', 'usePagination'], 'useRowSelect');
+
+ // UI5WCR: early exit when selection disabled
+ let selectedFlatRows: RowType[] = emptyArray;
+ let isAllRowsSelected = false;
+ let isAllPageRowsSelected = false;
+
+ if (isSelectionEnabled) {
+ selectedFlatRows = [];
+ rows.forEach((row) => {
+ const isSelected = selectSubRows ? getRowIsSelected(row, selectedRowIds, getSubRows) : !!selectedRowIds[row.id];
+ row.isSelected = !!isSelected;
+ row.isSomeSelected = isSelected === null;
+
+ if (isSelected) {
+ selectedFlatRows.push(row);
+ }
+ });
+
+ // isAllRowsSelected
+ const rowIds = Object.keys(nonGroupedRowsById);
+ const selectedIds = Object.keys(selectedRowIds);
+ if (rowIds.length && selectedIds.length) {
+ isAllRowsSelected = rowIds.every((id) => selectedRowIds[id]);
+ }
+
+ // isAllPageRowsSelected
+ if (page?.length) {
+ isAllPageRowsSelected = isAllRowsSelected || page.every((row) => selectedRowIds[row.id]);
+ }
+ }
+
+ const getAutoResetSelectedRows = useGetLatest(autoResetSelectedRows);
+
+ useMountedLayoutEffect(() => {
+ if (getAutoResetSelectedRows()) {
+ dispatch({ type: actions.resetSelectedRows });
+ }
+ }, [dispatch, data]);
+
+ const toggleAllRowsSelected = useCallback(
+ (value?: boolean) => dispatch({ type: actions.toggleAllRowsSelected, value }),
+ [dispatch],
+ );
+
+ const toggleAllPageRowsSelected = useCallback(
+ (value?: boolean) => dispatch({ type: actions.toggleAllPageRowsSelected, value }),
+ [dispatch],
+ );
+
+ const toggleRowSelected = useCallback(
+ (id: string, value?: boolean) => dispatch({ type: actions.toggleRowSelected, id, value }),
+ [dispatch],
+ );
+
+ const getInstance = useGetLatest(instance);
+
+ // UI5WCR: use noop when selection disabled
+ const getToggleAllRowsSelectedProps = isSelectionEnabled
+ ? makePropGetter(getHooks().getToggleAllRowsSelectedProps, { instance: getInstance() })
+ : noopGetProps;
+ const getToggleAllPageRowsSelectedProps = isSelectionEnabled
+ ? makePropGetter(getHooks().getToggleAllPageRowsSelectedProps, { instance: getInstance() })
+ : noopGetProps;
+
+ Object.assign(instance, {
+ selectedFlatRows,
+ isAllRowsSelected,
+ isAllPageRowsSelected,
+ toggleRowSelected,
+ toggleAllRowsSelected,
+ getToggleAllRowsSelectedProps,
+ getToggleAllPageRowsSelectedProps,
+ toggleAllPageRowsSelected,
+ });
+}
+
+function prepareRow(row: RowType, { instance }: { instance: TableInstance }) {
+ const { selectionMode } = instance.webComponentsReactProperties;
+ const isSelectionEnabled = selectionMode !== AnalyticalTableSelectionMode.None;
+
+ // UI5WCR: skip per-row setup when selection disabled
+ if (!isSelectionEnabled) {
+ row.isSelected = false;
+ row.isSomeSelected = false;
+ row.toggleRowSelected = noopToggle;
+ row.getToggleRowSelectedProps = noopGetProps;
+ return;
+ }
+
+ row.toggleRowSelected = (set?: boolean) => instance.toggleRowSelected?.(row.id, set);
+
+ row.getToggleRowSelectedProps = makePropGetter(instance.getHooks().getToggleRowSelectedProps, { instance, row });
+}
+
+function getRowIsSelected(
+ row: RowType,
+ selectedRowIds: Record,
+ getSubRows: (row: RowType) => RowType[],
+): boolean | null {
+ if (selectedRowIds[row.id]) {
+ return true;
+ }
+
+ const subRows = getSubRows(row);
+
+ if (subRows && subRows.length) {
+ let allChildrenSelected = true;
+ let someSelected = false;
+
+ for (const subRow of subRows) {
+ // Bail out early if we know both of these
+ if (someSelected && !allChildrenSelected) {
+ break;
+ }
+
+ if (getRowIsSelected(subRow, selectedRowIds, getSubRows)) {
+ someSelected = true;
+ } else {
+ allChildrenSelected = false;
+ }
+ }
+ return allChildrenSelected ? true : someSelected ? null : false;
+ }
+
+ return false;
+}
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useRowSelectionColumn.tsx b/packages/main/src/components/AnalyticalTable/hooks/useRowSelectionColumn.tsx
index 4e2090cf84a..4d8045a3b08 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useRowSelectionColumn.tsx
+++ b/packages/main/src/components/AnalyticalTable/hooks/useRowSelectionColumn.tsx
@@ -1,16 +1,8 @@
-import { CssSizeVariablesNames, enrichEventWithDetails } from '@ui5/webcomponents-react-base';
-import type { CSSProperties } from 'react';
+import { CssSizeVariablesNames } from '@ui5/webcomponents-react-base/CssSizeVariables';
import { AnalyticalTableSelectionBehavior } from '../../../enums/AnalyticalTableSelectionBehavior.js';
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
import { CheckBox } from '../../../webComponents/CheckBox/index.js';
import type { ReactTableHooks, TableInstance } from '../types/index.js';
-
-const customCheckBoxStyling = {
- verticalAlign: 'middle',
- pointerEvents: 'none',
- display: 'block',
-} as CSSProperties;
-
/*
* COMPONENTS
*/
@@ -18,10 +10,11 @@ const customCheckBoxStyling = {
const Header = (instance: TableInstance) => {
const {
getToggleAllRowsSelectedProps,
+ rows,
webComponentsReactProperties: { selectionMode, translatableTexts, classes },
} = instance;
- if (selectionMode === AnalyticalTableSelectionMode.Single) {
+ if (selectionMode === AnalyticalTableSelectionMode.Single || !rows.length) {
return null;
}
const checkBoxProps = getToggleAllRowsSelectedProps();
@@ -29,7 +22,6 @@ const Header = (instance: TableInstance) => {
<>
{
{...row.getToggleRowSelectedProps()}
tabIndex={-1}
aria-hidden="true"
- style={customCheckBoxStyling}
data-name="internal_selection_column"
/>
);
};
-function getNextSelectedRowIds(rowsById) {
- return Object.keys(rowsById).reduce((acc, cur) => {
- acc[cur] = true;
- return acc;
- }, {});
-}
-
const headerProps = (props, { instance }: { instance: TableInstance }) => {
const {
webComponentsReactProperties: {
@@ -72,14 +56,8 @@ const headerProps = (props, { instance }: { instance: TableInstance }) => {
},
toggleAllRowsSelected,
isAllRowsSelected,
- rowsById,
- preFilteredRowsById,
- dispatch,
- state: { filters, globalFilter },
} = instance;
const style = { ...props.style, cursor: 'pointer', display: 'flex', justifyContent: 'center' };
- const isFiltered = filters?.length > 0 || !!globalFilter;
- const _rowsById = isFiltered ? preFilteredRowsById : rowsById;
if (
props.key === 'header___ui5wcr__internal_selection_column' &&
selectionMode === AnalyticalTableSelectionMode.Multiple
@@ -88,21 +66,10 @@ const headerProps = (props, { instance }: { instance: TableInstance }) => {
if (typeof props.onClick === 'function') {
props.onClick(e);
}
- toggleAllRowsSelected(!isAllRowsSelected);
if (typeof onRowSelect === 'function') {
- if (isFiltered) {
- dispatch({ type: 'SELECT_ROW_CB', payload: { event: e, row: undefined, selectAll: true, fired: true } });
- } else {
- onRowSelect(
- // cannot use instance.selectedFlatRows here as it only returns all rows on the first level
- enrichEventWithDetails(e, {
- rowsById: _rowsById,
- allRowsSelected: !isAllRowsSelected,
- selectedRowIds: !isAllRowsSelected ? getNextSelectedRowIds(rowsById) : {},
- }),
- );
- }
+ instance.pendingSelectEvent = { event: e, row: undefined, selectAll: true };
}
+ toggleAllRowsSelected(!isAllRowsSelected);
};
const onKeyDown = (e) => {
@@ -192,23 +159,9 @@ const getCellProps = (props, { cell }: { cell: TableInstance['cell'] }) => {
return props;
};
-const setToggleAllRowsSelectedProps = (
- props,
- { instance: { webComponentsReactProperties } }: { instance: TableInstance },
-) => {
- const { classes } = webComponentsReactProperties;
- return [props, { className: classes.checkBox, title: undefined }];
-};
-const setToggleRowSelectedProps = (props, { instance: { webComponentsReactProperties } }) => {
- const { classes } = webComponentsReactProperties;
- return [props, { className: classes.checkBox, title: undefined }];
-};
-
export const useRowSelectionColumn = (hooks: ReactTableHooks) => {
hooks.getCellProps.push(getCellProps);
hooks.getHeaderProps.push(headerProps);
- hooks.getToggleRowSelectedProps.push(setToggleRowSelectedProps);
- hooks.getToggleAllRowsSelectedProps.push(setToggleAllRowsSelectedProps);
hooks.columns.push(columns);
hooks.visibleColumns.push(visibleColumns);
};
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts b/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts
index 4c0d42af9b5..b441ee71130 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useSelectionChangeCallback.ts
@@ -1,56 +1,69 @@
-import { enrichEventWithDetails } from '@ui5/webcomponents-react-base';
-import { useEffect } from 'react';
+import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/internal/utils';
+import { useEffect, useRef } from 'react';
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
-import type { ReactTableHooks, TableInstance } from '../types/index.js';
+import type { AnalyticalTablePropTypes, ReactTableHooks, TableInstance } from '../types/index.js';
-export const useSelectionChangeCallback = (hooks: ReactTableHooks) => {
- hooks.useControlledState.push((state, { instance }: { instance: TableInstance }) => {
- const { selectedRowPayload, selectedRowIds, filters, globalFilter } = state;
- const { rowsById, preFilteredRowsById, webComponentsReactProperties, dispatch } = instance;
- const isFiltered = filters?.length > 0 || !!globalFilter;
-
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useEffect(() => {
- if (selectedRowPayload?.fired) {
- const { event: e, row: selRow, selectAll } = selectedRowPayload;
- const row = rowsById[selRow?.id];
- // when selecting a row on a filtered table, `preFilteredRowsById` has to be used, otherwise filtered out rows are undefined
- const _rowsById = isFiltered ? preFilteredRowsById : rowsById;
-
- if (row || selectAll) {
- const payload = {
- row: row,
- rowsById: _rowsById,
- isSelected: row?.isSelected,
- allRowsSelected: false,
- selectedRowIds,
- };
-
- if (webComponentsReactProperties.selectionMode === AnalyticalTableSelectionMode.Multiple) {
- if (Object.keys(selectedRowIds).length === Object.keys(_rowsById).length) {
- payload.allRowsSelected = true;
- }
-
- if (selectAll) {
- dispatch({ type: 'SELECT_ROW_CB', payload: { event: e, row, selectAll: false, fired: false } });
- webComponentsReactProperties?.onRowSelect(
- enrichEventWithDetails(e, {
- rowsById: payload.rowsById,
- allRowsSelected: payload.allRowsSelected,
- selectedRowIds: payload.selectedRowIds,
- }),
- );
- return;
- }
- }
- dispatch({ type: 'SELECT_ROW_CB', payload: { event: e, row, fired: false } });
- webComponentsReactProperties?.onRowSelect(enrichEventWithDetails(e, payload));
+type OnRowSelectEvent = Parameters>[0];
+type OnRowSelectDetail = OnRowSelectEvent['detail'];
+
+const useInstance = (instance: TableInstance) => {
+ const { webComponentsReactProperties, rowsById, preFilteredRowsById, state } = instance;
+ const { selectedRowIds, filters, globalFilter } = state;
+ const { onRowSelect, selectionMode } = webComponentsReactProperties;
+
+ const prevSelectedRowIdsRef = useRef(selectedRowIds);
+
+ useEffect(() => {
+ const pendingEvent = instance.pendingSelectEvent;
+
+ // Only fire callback if there's a pending event and selection changed
+ if (pendingEvent && prevSelectedRowIdsRef.current !== selectedRowIds) {
+ // Clear pending event - instance is mutable
+ // eslint-disable-next-line react-hooks/immutability
+ instance.pendingSelectEvent = undefined;
+
+ const { event: e, row: eventRow, selectAll } = pendingEvent;
+ const row = eventRow ? rowsById[eventRow.id] : undefined;
+ const isFiltered = filters?.length > 0 || !!globalFilter;
+ const _rowsById = isFiltered ? preFilteredRowsById : rowsById;
+
+ const payload: Partial = {
+ row,
+ rowsById: _rowsById,
+ isSelected: row?.isSelected,
+ allRowsSelected: false,
+ allVisibleRowsSelected: !!instance.isAllRowsSelected,
+ selectedRowIds,
+ };
+
+ if (selectionMode === AnalyticalTableSelectionMode.Multiple) {
+ // Check if all rows (including filtered) are selected
+ if (Object.keys(selectedRowIds).length === Object.keys(_rowsById).length) {
+ payload.allRowsSelected = true;
}
}
- }, [selectedRowPayload?.fired, rowsById, webComponentsReactProperties.selectionMode, selectedRowIds, isFiltered]);
- return state;
- });
+ if (selectAll) {
+ // For select-all click, don't include row-specific fields
+ onRowSelect?.(
+ enrichEventWithDetails(e as Event & { detail?: OnRowSelectDetail }, {
+ rowsById: payload.rowsById,
+ allRowsSelected: payload.allRowsSelected,
+ allVisibleRowsSelected: payload.allVisibleRowsSelected,
+ selectedRowIds: payload.selectedRowIds,
+ }) as OnRowSelectEvent,
+ );
+ } else {
+ onRowSelect?.(enrichEventWithDetails(e as Event & { detail?: OnRowSelectDetail }, payload) as OnRowSelectEvent);
+ }
+ }
+
+ prevSelectedRowIdsRef.current = selectedRowIds;
+ }, [selectedRowIds, rowsById, preFilteredRowsById, filters, globalFilter, selectionMode, instance, onRowSelect]);
+};
+
+export const useSelectionChangeCallback = (hooks: ReactTableHooks) => {
+ hooks.useInstance.push(useInstance);
};
useSelectionChangeCallback.pluginName = 'useSelectionChangeCallback';
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useSingleRowStateSelection.ts b/packages/main/src/components/AnalyticalTable/hooks/useSingleRowStateSelection.ts
index 15c6a210f1d..1bc56c79a78 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useSingleRowStateSelection.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useSingleRowStateSelection.ts
@@ -5,7 +5,7 @@ import { getTagNameWithoutScopingSuffix } from '../../../internal/utils.js';
import type { ReactTableHooks, RowType, TableInstance } from '../types/index.js';
const getRowProps = (rowProps, { row, instance }: { row: RowType; instance: TableInstance }) => {
- const { webComponentsReactProperties, toggleRowSelected, selectedFlatRows, dispatch } = instance;
+ const { webComponentsReactProperties, toggleRowSelected, selectedFlatRows } = instance;
const handleRowSelect = (e) => {
const isSelectionCell = e.target.dataset.selectionCell === 'true';
if (
@@ -47,12 +47,12 @@ const getRowProps = (rowProps, { row, instance }: { row: RowType; instance: Tabl
}
}
- toggleRowSelected(row.id);
-
if (typeof onRowSelect === 'function') {
- // update state to return instance values after update (see useSelectionChangeCallback hook)
- dispatch({ type: 'SELECT_ROW_CB', payload: { event: e, row, fired: true } });
+ // Store pending event on table instance
+ instance.pendingSelectEvent = { event: e, row };
}
+
+ toggleRowSelected(row.id);
};
const handleKeyDown = (e) => {
diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx
index 0f3b5806b60..8e9e86b4073 100644
--- a/packages/main/src/components/AnalyticalTable/index.tsx
+++ b/packages/main/src/components/AnalyticalTable/index.tsx
@@ -21,7 +21,6 @@ import {
useGlobalFilter,
useGroupBy,
useResizeColumns,
- useRowSelect,
useSortBy,
useTable,
} from 'react-table';
@@ -75,6 +74,7 @@ import { usePopIn } from './hooks/usePopIn.js';
import { useResizeColumnsConfig } from './hooks/useResizeColumnsConfig.js';
import { useRowHighlight } from './hooks/useRowHighlight.js';
import { useRowNavigationIndicators } from './hooks/useRowNavigationIndicator.js';
+import { useRowSelect } from './hooks/useRowSelect.js';
import { useRowSelectionColumn } from './hooks/useRowSelectionColumn.js';
import { useScrollToRef } from './hooks/useScrollToRef.js';
import { useSelectionChangeCallback } from './hooks/useSelectionChangeCallback.js';
diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx b/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx
index f4e5a64df2d..5cc393e2084 100644
--- a/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx
+++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useIndeterminateRowSelection.tsx
@@ -1,9 +1,9 @@
'use client';
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
import { AnalyticalTableSelectionBehavior } from '../../../enums/AnalyticalTableSelectionBehavior.js';
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
-import type { ReactTableHooks, RowType, TableInstance } from '../types/index.js';
+import type { AnalyticalTableState, ReactTableHooks, RowType, TableInstance } from '../types/index.js';
type onIndeterminateChange = (e: {
indeterminateRowsById: Record;
@@ -19,8 +19,8 @@ const getParentRow = (id: string, rowsById: TableInstance['rowsById']): [RowType
return [rowsById[parentRowId], lastDotIndex];
};
-const getIndeterminateRowIds = (id) => {
- const indeterminateRowsById = {};
+const getIndeterminateRowIds = (id: string): Record => {
+ const indeterminateRowsById: Record = {};
const lastDotIndex = id.lastIndexOf('.');
indeterminateRowsById[id] = true;
if (lastDotIndex !== -1) {
@@ -30,17 +30,21 @@ const getIndeterminateRowIds = (id) => {
return indeterminateRowsById;
};
-const getIndeterminate = (rows, rowsById, state) => {
- const indeterminateRowsById = {};
+const getIndeterminate = (
+ rows: RowType[],
+ rowsById: TableInstance['rowsById'],
+ state: { selectedRowIds: AnalyticalTableState['selectedRowIds'] },
+): Record => {
+ const indeterminateRowsById: Record = {};
let usedParentIndex = '';
- const getIndeterminateRecursive = (subRows, rowIdScope = null) => {
+ const getIndeterminateRecursive = (subRows: RowType[], rowIdScope: string | null = null) => {
for (const row of subRows) {
if (row.subRows.length > 0) {
// find leaf nodes
getIndeterminateRecursive(row.subRows, row.id);
} else if (rowIdScope !== null && usedParentIndex !== rowIdScope) {
usedParentIndex = rowIdScope;
- const checkIndeterminate = (rowId) => {
+ const checkIndeterminate = (rowId: string) => {
const [parentRow, dotIndex] = getParentRow(rowId, rowsById);
const selectedRows = parentRow.subRows.filter((item) => state.selectedRowIds[item.id]);
const areAllSelected = parentRow.subRows.length === selectedRows.length;
@@ -79,7 +83,10 @@ const getIndeterminate = (rows, rowsById, state) => {
* @param {event} onIndeterminateChange Fired when the indeterminate state of rows is changed.
*/
export const useIndeterminateRowSelection = (onIndeterminateChange?: onIndeterminateChange) => {
- const toggleRowProps = (rowProps, { row, instance }: { row: RowType; instance: TableInstance }) => {
+ const toggleRowProps = (
+ rowProps: { checked?: boolean },
+ { row, instance }: { row: RowType; instance: TableInstance },
+ ) => {
let indeterminate: boolean;
if (instance.isAllRowsSelected) {
indeterminate = false;
@@ -87,10 +94,6 @@ export const useIndeterminateRowSelection = (onIndeterminateChange?: onIndetermi
indeterminate = instance?.state?.indeterminateRows?.[row.id] ?? false;
}
- if (rowProps.checked && !instance.state.selectedRowIds[row.id]) {
- row.toggleRowSelected(true);
- }
-
return [
rowProps,
{
@@ -100,8 +103,33 @@ export const useIndeterminateRowSelection = (onIndeterminateChange?: onIndetermi
];
};
- const stateReducer = (newState, action, prevState, instance) => {
- const { rowsById, state, rows } = instance;
+ const stateReducer: TableInstance['stateReducer'] = (newState, action, _prevState, instance) => {
+ const { rowsById, rows } = instance;
+
+ // check if parent row should be auto-selected
+ if (action.type === 'toggleRowSelected') {
+ const rowId = action.id;
+ const isSelected = newState.selectedRowIds[rowId];
+ if (isSelected) {
+ // check if row has parent and if all subRows are selected
+ const parentId = rowId.substring(0, rowId.lastIndexOf('.'));
+ if (parentId && rowsById[parentId]) {
+ const parentRow = rowsById[parentId];
+ const allSiblingsSelected = parentRow.subRows?.every((subRow: RowType) => newState.selectedRowIds[subRow.id]);
+ if (allSiblingsSelected && !newState.selectedRowIds[parentId]) {
+ // auto-select parent row
+ return {
+ ...newState,
+ selectedRowIds: {
+ ...newState.selectedRowIds,
+ [parentId]: true,
+ },
+ };
+ }
+ }
+ }
+ }
+
if (action.type === 'INDETERMINATE_ROW_IDS') {
if (action.payload === 'reset') {
return {
@@ -110,7 +138,7 @@ export const useIndeterminateRowSelection = (onIndeterminateChange?: onIndetermi
};
}
- const indeterminateRowsById = getIndeterminate(rows, rowsById, state);
+ const indeterminateRowsById = getIndeterminate(rows, rowsById, { selectedRowIds: newState.selectedRowIds });
return {
...newState,
@@ -128,7 +156,14 @@ export const useIndeterminateRowSelection = (onIndeterminateChange?: onIndetermi
webComponentsReactProperties: { selectionMode, selectionBehavior, isTreeTable },
} = instance;
+ const lastProcessedSelectedRowIdsRef = useRef(selectedRowIds);
+
useEffect(() => {
+ if (lastProcessedSelectedRowIdsRef.current === selectedRowIds) {
+ return;
+ }
+ lastProcessedSelectedRowIdsRef.current = selectedRowIds;
+
if (
isTreeTable &&
selectionMode === AnalyticalTableSelectionMode.Multiple &&
@@ -140,7 +175,7 @@ export const useIndeterminateRowSelection = (onIndeterminateChange?: onIndetermi
} else if (typeof indeterminateRows === 'object' && Object.keys(indeterminateRows).length) {
dispatch({ type: 'INDETERMINATE_ROW_IDS', payload: 'reset' });
}
- }, [data, selectedRowIds, isTreeTable, selectionMode, selectionBehavior]);
+ }, [data, selectedRowIds, isTreeTable, selectionMode, selectionBehavior, dispatch]);
useEffect(() => {
if (typeof onIndeterminateChange === 'function' && indeterminateRows) {
diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts b/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts
index 377a29f6adb..95dbd3a791d 100644
--- a/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts
+++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useManualRowSelect.ts
@@ -1,6 +1,7 @@
'use client';
import { useEffect } from 'react';
+import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
import type { ReactTableHooks, TableInstance } from '../types/index.js';
/**
@@ -12,18 +13,26 @@ export const useManualRowSelect = (manualRowSelectedKey = 'isSelected') => {
const instanceAfterData = ({
flatRows,
toggleRowSelected,
+ webComponentsReactProperties,
}: {
flatRows: TableInstance['flatRows'];
toggleRowSelected: TableInstance['toggleRowSelected'];
+ webComponentsReactProperties: TableInstance['webComponentsReactProperties'];
}) => {
+ const { selectionMode } = webComponentsReactProperties;
+
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
+ if (selectionMode === AnalyticalTableSelectionMode.None) {
+ return;
+ }
+
flatRows.forEach(({ id, original }) => {
if (manualRowSelectedKey in original) {
toggleRowSelected(id, original.isSelected);
}
});
- }, [flatRows, toggleRowSelected]);
+ }, [flatRows, toggleRowSelected, selectionMode]);
};
const manualRowSelect = (hooks: ReactTableHooks) => {
diff --git a/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx b/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx
index 38752544c48..eac5de1a552 100644
--- a/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx
+++ b/packages/main/src/components/AnalyticalTable/pluginHooks/useRowDisableSelection.tsx
@@ -94,6 +94,9 @@ export const useRowDisableSelection = (disableRowSelection: DisableRowSelectionT
const getRowProps = (rowProps, { row, instance }: { row: RowType; instance: TableInstance }) => {
const { webComponentsReactProperties } = instance;
+ if (webComponentsReactProperties.selectionMode === AnalyticalTableSelectionMode.None) {
+ return rowProps;
+ }
if (disableRowAccessor(row) === true) {
row.disableSelect = true;
const handleClick = (e) => {
@@ -155,7 +158,10 @@ export const useRowDisableSelection = (disableRowSelection: DisableRowSelectionT
return cellProps;
};
- const toggleRowSelectedProps = (rowProps, { row }: { row: RowType }) => {
+ const toggleRowSelectedProps = (rowProps, { row, instance }: { row: RowType; instance: TableInstance }) => {
+ if (instance.webComponentsReactProperties.selectionMode === AnalyticalTableSelectionMode.None) {
+ return rowProps;
+ }
if (disableRowAccessor(row) === true) {
const { title: _0, ...updatedRowProps } = rowProps;
return updatedRowProps;
diff --git a/packages/main/src/components/AnalyticalTable/tableReducer/stateReducer.ts b/packages/main/src/components/AnalyticalTable/tableReducer/stateReducer.ts
index fa8dba41345..3c29e79951d 100644
--- a/packages/main/src/components/AnalyticalTable/tableReducer/stateReducer.ts
+++ b/packages/main/src/components/AnalyticalTable/tableReducer/stateReducer.ts
@@ -1,7 +1,7 @@
import { actions } from 'react-table';
import type { TableInstance } from '../types/index.js';
-export const stateReducer = (state, action, _prevState, instance: TableInstance) => {
+export const stateReducer: TableInstance['stateReducer'] = (state, action, _prevState, instance) => {
const { payload } = action;
if (state.isRtl && action.type === actions.columnResizing) {
const { clientX } = action;
@@ -67,8 +67,6 @@ export const stateReducer = (state, action, _prevState, instance: TableInstance)
return { ...state, subComponentsHeight: payload };
case 'TABLE_COL_RESIZED':
return { ...state, tableColResized: payload };
- case 'SELECT_ROW_CB':
- return { ...state, selectedRowPayload: payload };
case 'ROW_COLLAPSED_FLAG':
return { ...state, rowCollapsed: payload };
case 'COLUMN_DND_START':
diff --git a/packages/main/src/components/AnalyticalTable/types/index.ts b/packages/main/src/components/AnalyticalTable/types/index.ts
index 39b03294319..e73196d7671 100644
--- a/packages/main/src/components/AnalyticalTable/types/index.ts
+++ b/packages/main/src/components/AnalyticalTable/types/index.ts
@@ -27,6 +27,17 @@ import type { classNames } from '../AnalyticalTable.module.css.js';
export type ClassNames = typeof classNames;
+interface StateReducerAction {
+ type: string;
+ id?: string;
+ value?: boolean;
+ payload?: any;
+ columnId?: string;
+ filterValue?: string;
+ clientX?: number;
+ [key: string]: unknown;
+}
+
export enum RenderColumnTypes {
Filter = 'Filter',
Grouped = 'Grouped',
@@ -109,6 +120,15 @@ export interface CellType {
export interface TableInstance {
allColumns: ColumnType[];
allColumnsHidden?: boolean;
+ autoResetExpanded?: boolean;
+ autoResetFilters?: boolean;
+ autoResetGroupBy?: boolean;
+ autoResetHiddenColumns?: boolean;
+ autoResetPage?: boolean;
+ autoResetResize?: boolean;
+ autoResetRowState?: boolean;
+ autoResetSelectedRows?: boolean;
+ autoResetSortBy?: boolean;
columns: ColumnType[];
data: Record[];
defaultColumn: Record;
@@ -120,6 +140,8 @@ export interface TableInstance {
type: string;
payload?: Record | AnalyticalTableState['popInColumns'] | boolean | string | number;
clientX?: number;
+ value?: boolean;
+ id?: string;
}) => void;
expandedDepth?: number;
expandedRows?: RowType[];
@@ -151,10 +173,12 @@ export interface TableInstance {
isAllPageRowsSelected?: boolean;
isAllRowsExpanded?: boolean;
isAllRowsSelected?: boolean;
+ manualRowSelectedKey?: string;
nonGroupedFlatRows?: RowType[];
nonGroupedRowsById?: Record;
onlyGroupedFlatRows?: RowType[];
onlyGroupedRowsById?: Record;
+ page?: RowType[];
plugins: ((hooks: ReactTableHooks) => void)[];
preExpandedRows?: RowType[];
preFilteredFlatRows?: RowType[];
@@ -192,7 +216,7 @@ export interface TableInstance {
state: AnalyticalTableState & Record;
stateReducer?: (
state: TableInstance['state'],
- action: any,
+ action: StateReducerAction,
_prevState: TableInstance['state'],
instance: TableInstance,
) => TableInstance['state'];
@@ -219,6 +243,11 @@ export interface TableInstance {
visibleColumns: ColumnType[];
visibleColumnsWidth?: number[];
webComponentsReactProperties: WCRPropertiesType;
+ pendingSelectEvent?: {
+ event: Event;
+ row?: RowType;
+ selectAll?: boolean;
+ };
[key: string]: any;
}
@@ -289,6 +318,7 @@ export interface RowType {
id: string;
index: number;
isExpanded: boolean | undefined;
+ isGrouped?: boolean;
isSelected: boolean;
isSomeSelected: boolean;
getRowProps: (props?: any) => any;
@@ -1023,7 +1053,16 @@ export interface AnalyticalTablePropTypes extends Omit {
* __Note:__ If the event invoked by select-all press, this property is not available.
*/
isSelected?: boolean;
+ /**
+ * Indicates if all rows (including filtered out rows) are selected.
+ */
allRowsSelected: boolean;
+ /**
+ * Indicates if all currently visible rows are selected.
+ *
+ * __Note:__ When the table is filtered, this reflects only the non-filtered (visible) rows.
+ */
+ allVisibleRowsSelected: boolean;
rowsById: Record;
selectedRowIds: Record;
nativeDetail: number;
@@ -1110,7 +1149,7 @@ interface ConfigParam {
export interface ReactTableHooks {
useOptions: any[];
- stateReducers: any[];
+ stateReducers: NonNullable[];
useControlledState: any[];
columns: ((columns: ColumnType[], config: ConfigParam) => ColumnType[])[];
columnsDeps: ((deps: DependencyList, config: ConfigParam) => DependencyList)[];