diff --git a/change/@fluentui-react-table-9bafa4eb-c107-4426-b422-56d0113f018b.json b/change/@fluentui-react-table-9bafa4eb-c107-4426-b422-56d0113f018b.json new file mode 100644 index 0000000000000..42bbd5b511f14 --- /dev/null +++ b/change/@fluentui-react-table-9bafa4eb-c107-4426-b422-56d0113f018b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "28456", + "packageName": "@fluentui/react-table", + "email": "lingfan.gao@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-table/src/hooks/useTableSelection.test.ts b/packages/react-components/react-table/src/hooks/useTableSelection.test.ts index f0cf882f0c043..39a97d4254f03 100644 --- a/packages/react-components/react-table/src/hooks/useTableSelection.test.ts +++ b/packages/react-components/react-table/src/hooks/useTableSelection.test.ts @@ -219,6 +219,36 @@ describe('useTableSelectionState', () => { }); describe('allRowsSelected', () => { + it('should return true after items updated if all selectable rows are selected', () => { + const getRowId = (item: { value: string }) => item.value; + let tableState = mockTableState({ items, getRowId }); + const { result, rerender } = renderHook(() => + useTableSelectionState(tableState, { selectionMode: 'multiselect' }), + ); + + act(() => { + result.current.selection.toggleAllRows(mockSyntheticEvent()); + }); + + act(() => { + result.current.selection.deselectRow(mockSyntheticEvent(), 'c'); + }); + + expect(result.current.selection.allRowsSelected).toBe(false); + + // remove the deselected item + const nextItems = [...items]; + const indexToDelete = nextItems.findIndex(x => x.value === 'c'); + nextItems.splice(indexToDelete, 1); + tableState = mockTableState({ items: nextItems, getRowId }); + + act(() => { + rerender(); + }); + + expect(result.current.selection.allRowsSelected).toBe(true); + }); + it('should return true if all rows are selected', () => { const { result } = renderHook(() => useTableSelectionState(mockTableState({ items }), { selectionMode: 'multiselect' }), @@ -258,6 +288,32 @@ describe('useTableSelectionState', () => { }); describe('someRowsSelected', () => { + it('should return false after selectedItems are removed', () => { + const getRowId = (item: { value: string }) => item.value; + let tableState = mockTableState({ items, getRowId }); + const { result, rerender } = renderHook(() => + useTableSelectionState(tableState, { selectionMode: 'multiselect' }), + ); + + act(() => { + result.current.selection.selectRow(mockSyntheticEvent(), 'a'); + }); + + expect(result.current.selection.someRowsSelected).toBe(true); + + // remove the deselected item + const nextItems = [...items]; + const indexToDelete = nextItems.findIndex(x => x.value === 'a'); + nextItems.splice(indexToDelete, 1); + tableState = mockTableState({ items: nextItems, getRowId }); + + act(() => { + rerender(); + }); + + expect(result.current.selection.someRowsSelected).toBe(false); + }); + it('should return true if there is a selected row', () => { const { result } = renderHook(() => useTableSelectionState(mockTableState({ items }), { selectionMode: 'multiselect' }), diff --git a/packages/react-components/react-table/src/hooks/useTableSelection.ts b/packages/react-components/react-table/src/hooks/useTableSelection.ts index 659b32cae8a56..7f2b94bf8de82 100644 --- a/packages/react-components/react-table/src/hooks/useTableSelection.ts +++ b/packages/react-components/react-table/src/hooks/useTableSelection.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; import type { TableRowId, TableSelectionState, TableFeaturesState } from './types'; @@ -36,6 +37,52 @@ export function useTableSelectionState( onSelectionChange, }); + // Selection state can contain obselete items (i.e. rows that are removed) + const selectableRowIds = React.useMemo(() => { + const rowIds = new Set(); + for (let i = 0; i < items.length; i++) { + rowIds.add(getRowId?.(items[i]) ?? i); + } + + return rowIds; + }, [items, getRowId]); + + const allRowsSelected = React.useMemo(() => { + if (selectionMode === 'single') { + const selectedRow = Array.from(selected)[0]; + return selectableRowIds.has(selectedRow); + } + + // multiselect case + if (selected.size < selectableRowIds.size) { + return false; + } + + let res = true; + selectableRowIds.forEach(selectableRowId => { + if (!selected.has(selectableRowId)) { + res = false; + } + }); + + return res; + }, [selectableRowIds, selected, selectionMode]); + + const someRowsSelected = React.useMemo(() => { + if (selected.size <= 0) { + return false; + } + + let res = false; + selectableRowIds.forEach(selectableRowId => { + if (selected.has(selectableRowId)) { + res = true; + } + }); + + return res; + }, [selectableRowIds, selected]); + const toggleAllRows: TableSelectionState['toggleAllRows'] = useEventCallback(e => { selectionMethods.toggleAllItems( e, @@ -63,8 +110,8 @@ export function useTableSelectionState( ...tableState, selection: { selectionMode, - someRowsSelected: selected.size > 0, - allRowsSelected: selectionMode === 'single' ? selected.size > 0 : selected.size === items.length, + someRowsSelected, + allRowsSelected, selectedRows: selected, toggleRow, toggleAllRows,