From 99d12c605873bb813b632aa19abbece4a2a75624 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 09:31:14 +0200 Subject: [PATCH 1/8] feat: autocontrolled `useTable` hook `useTable` hook now supports autoncontrolling sort and selection states --- .../react-table/etc/react-table.api.md | 20 ++- .../src/components/Table/Table.types.ts | 19 +-- .../react-table/src/hooks/selectionManager.ts | 36 ++--- .../react-table/src/hooks/types.ts | 32 ++++ .../src/hooks/useSelection.test.ts | 62 ++++---- .../react-table/src/hooks/useSelection.ts | 81 +++++++--- .../react-table/src/hooks/useSort.test.ts | 18 +-- .../react-table/src/hooks/useSort.ts | 35 +++- .../react-table/src/hooks/useTable.ts | 29 +++- .../stories/Table/MultipleSelect.stories.tsx | 1 + .../MultipleSelectControlled.stories.tsx | 149 ++++++++++++++++++ .../stories/Table/SingleSelect.stories.tsx | 1 + .../Table/SingleSelectControlled.stories.tsx | 143 +++++++++++++++++ .../src/stories/Table/Sort.stories.tsx | 8 +- .../stories/Table/SortControlled.stories.tsx | 147 +++++++++++++++++ .../src/stories/Table/index.stories.tsx | 3 + 16 files changed, 664 insertions(+), 120 deletions(-) create mode 100644 packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx create mode 100644 packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx create mode 100644 packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx diff --git a/packages/react-components/react-table/etc/react-table.api.md b/packages/react-components/react-table/etc/react-table.api.md index f80ec68a76cd5..06b535f21a29a 100644 --- a/packages/react-components/react-table/etc/react-table.api.md +++ b/packages/react-components/react-table/etc/react-table.api.md @@ -240,13 +240,7 @@ export type TablePrimaryCellSlots = { export type TablePrimaryCellState = ComponentState; // @public -export type TableProps = ComponentProps & {} & Partial & { - onSortColumnChange?: (e: React_2.MouseEvent | React_2.KeyboardEvent, data: { - sortState: SortState_2; - }) => void; - sortState?: SortState_2; - defaultSortState?: SortState_2; -}; +export type TableProps = ComponentProps & {} & Partial; // @public export const TableRow: ForwardRefComponent; @@ -342,14 +336,26 @@ export const useTableHeaderStyles_unstable: (state: TableHeaderState) => TableHe export interface UseTableOptions = RowState> { // (undocumented) columns: ColumnDefinition[]; + defaultSelectedRows?: Set; + defaultSortState?: { + sortColumn: ColumnId | undefined; + sortDirection: SortDirection; + }; // (undocumented) getRowId?: (item: TItem) => RowId; // (undocumented) items: TItem[]; + onSelectionChange?: OnSelectionChangeCallback; + onSortChange?: OnSortChangeCallback; // (undocumented) rowEnhancer?: RowEnhancer; + selectedRows?: Set; // (undocumented) selectionMode?: SelectionMode_2; + sortState?: { + sortColumn: ColumnId | undefined; + sortDirection: SortDirection; + }; } // @public diff --git a/packages/react-components/react-table/src/components/Table/Table.types.ts b/packages/react-components/react-table/src/components/Table/Table.types.ts index d80c4b10f9d87..84fb43f12b276 100644 --- a/packages/react-components/react-table/src/components/Table/Table.types.ts +++ b/packages/react-components/react-table/src/components/Table/Table.types.ts @@ -1,5 +1,4 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import * as React from 'react'; export type TableSlots = { root: Slot<'table', 'div'>; @@ -14,10 +13,6 @@ export type TableContextValue = { }; export type SortDirection = 'ascending' | 'descending'; -export type SortState = { - sortColumn: string | undefined; - sortDirection: 'ascending' | 'descending'; -}; export type TableContextValues = { table: TableContextValue; @@ -26,19 +21,7 @@ export type TableContextValues = { /** * Table Props */ -export type TableProps = ComponentProps & {} & Partial & { - /** - * Called when the sorted column changes - */ - onSortColumnChange?: ( - e: React.MouseEvent | React.KeyboardEvent, - data: { sortState: SortState }, - ) => void; - - sortState?: SortState; - - defaultSortState?: SortState; - }; +export type TableProps = ComponentProps & {} & Partial; /** * State used in rendering Table diff --git a/packages/react-components/react-table/src/hooks/selectionManager.ts b/packages/react-components/react-table/src/hooks/selectionManager.ts index 87fa5a9b2c40c..88ececf1230f5 100644 --- a/packages/react-components/react-table/src/hooks/selectionManager.ts +++ b/packages/react-components/react-table/src/hooks/selectionManager.ts @@ -3,12 +3,12 @@ import { SelectionMode } from './types'; type OnSelectionChangeCallback = (selectedItems: Set) => void; export interface SelectionManager { - toggleItem(id: SelectionItemId): void; - selectItem(id: SelectionItemId): void; - deselectItem(id: SelectionItemId): void; + toggleItem(id: SelectionItemId, selectedItems: Set): void; + selectItem(id: SelectionItemId, selectedItems: Set): void; + deselectItem(id: SelectionItemId, selectedItems: Set): void; clearItems(): void; - isSelected(id: SelectionItemId): boolean; - toggleAllItems(itemIds: SelectionItemId[]): void; + isSelected(id: SelectionItemId, selectedItems: Set): boolean; + toggleAllItems(itemIds: SelectionItemId[], selectedItems: Set): void; } export type SelectionItemId = string | number; @@ -23,8 +23,7 @@ export function createSelectionManager( } function createMultipleSelectionManager(onSelectionChange: OnSelectionChangeCallback): SelectionManager { - const selectedItems = new Set(); - const toggleAllItems = (itemIds: SelectionItemId[]) => { + const toggleAllItems = (itemIds: SelectionItemId[], selectedItems: Set) => { const allItemsSelected = itemIds.every(itemId => selectedItems.has(itemId)); if (allItemsSelected) { @@ -36,7 +35,7 @@ function createMultipleSelectionManager(onSelectionChange: OnSelectionChangeCall onSelectionChange(new Set(selectedItems)); }; - const toggleItem = (itemId: SelectionItemId) => { + const toggleItem = (itemId: SelectionItemId, selectedItems: Set) => { if (selectedItems.has(itemId)) { selectedItems.delete(itemId); } else { @@ -46,22 +45,21 @@ function createMultipleSelectionManager(onSelectionChange: OnSelectionChangeCall onSelectionChange(new Set(selectedItems)); }; - const selectItem = (itemId: SelectionItemId) => { + const selectItem = (itemId: SelectionItemId, selectedItems: Set) => { selectedItems.add(itemId); onSelectionChange(new Set(selectedItems)); }; - const deselectItem = (itemId: SelectionItemId) => { + const deselectItem = (itemId: SelectionItemId, selectedItems: Set) => { selectedItems.delete(itemId); onSelectionChange(new Set(selectedItems)); }; const clearItems = () => { - selectedItems.clear(); - onSelectionChange(new Set(selectedItems)); + onSelectionChange(new Set()); }; - const isSelected = (itemId: SelectionItemId) => { + const isSelected = (itemId: SelectionItemId, selectedItems: Set) => { return selectedItems.has(itemId); }; @@ -76,24 +74,20 @@ function createMultipleSelectionManager(onSelectionChange: OnSelectionChangeCall } function createSingleSelectionManager(onSelectionChange: OnSelectionChangeCallback): SelectionManager { - let selectedItem: SelectionItemId | undefined = undefined; const toggleItem = (itemId: SelectionItemId) => { - selectedItem = itemId; - onSelectionChange(new Set([selectedItem])); + onSelectionChange(new Set([itemId])); }; const clearItems = () => { - selectedItem = undefined; onSelectionChange(new Set()); }; - const isSelected = (itemId: SelectionItemId) => { - return selectedItem === itemId; + const isSelected = (itemId: SelectionItemId, selectedItems: Set) => { + return selectedItems.has(itemId); }; const selectItem = (itemId: SelectionItemId) => { - selectedItem = itemId; - onSelectionChange(new Set([selectedItem])); + onSelectionChange(new Set([itemId])); }; return { diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 324ec6950eed7..256fc909ae331 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -4,6 +4,8 @@ export type RowId = string | number; export type ColumnId = string | number; export type GetRowIdInternal = (rowId: TItem, index: number) => RowId; export type SelectionMode = 'single' | 'multiselect'; +export type OnSelectionChangeCallback = (selectedItems: Set) => void; +export type OnSortChangeCallback = (state: { sortColumn: ColumnId | undefined; sortDirection: SortDirection }) => void; export interface ColumnDefinition { columnId: ColumnId; @@ -31,6 +33,36 @@ export interface UseTableOptions = RowS columns: ColumnDefinition[]; items: TItem[]; selectionMode?: SelectionMode; + /** + * Used in uncontrolled mode to set initial selected rows on mount + */ + defaultSelectedRows?: Set; + /** + * Used to control row selection + */ + selectedRows?: Set; + /** + * Called when selection changes + */ + onSelectionChange?: OnSelectionChangeCallback; + /** + * Used to control sorting + */ + sortState?: { + sortColumn: ColumnId | undefined; + sortDirection: SortDirection; + }; + /** + * Used in uncontrolled mode to set initial sort column and direction on mount + */ + defaultSortState?: { + sortColumn: ColumnId | undefined; + sortDirection: SortDirection; + }; + /** + * Called when sort changes + */ + onSortChange?: OnSortChangeCallback; getRowId?: (item: TItem) => RowId; rowEnhancer?: RowEnhancer; } diff --git a/packages/react-components/react-table/src/hooks/useSelection.test.ts b/packages/react-components/react-table/src/hooks/useSelection.test.ts index 768a8d318b6ff..79064a8b7f6c6 100644 --- a/packages/react-components/react-table/src/hooks/useSelection.test.ts +++ b/packages/react-components/react-table/src/hooks/useSelection.test.ts @@ -8,7 +8,13 @@ describe('useSelection', () => { describe('multiselect', () => { it('should use custom row id', () => { - const { result } = renderHook(() => useSelection('multiselect', items, (item: { value: string }) => item.value)); + const { result } = renderHook(() => + useSelection({ + selectionMode: 'multiselect', + items, + getRowId: (item: { value: string }) => item.value, + }), + ); act(() => { result.current.toggleAllRows(); }); @@ -18,7 +24,7 @@ describe('useSelection', () => { describe('toggleAllRows', () => { it('should select all rows', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleAllRows(); @@ -28,7 +34,7 @@ describe('useSelection', () => { }); it('should deselect all rows', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleAllRows(); @@ -43,7 +49,7 @@ describe('useSelection', () => { }); describe('clearRows', () => { it('should clear selection', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleAllRows(); @@ -58,7 +64,7 @@ describe('useSelection', () => { describe('selectRow', () => { it('should select row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.selectRow(1); @@ -68,7 +74,7 @@ describe('useSelection', () => { }); it('should select multiple rows', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.selectRow(1); @@ -86,7 +92,7 @@ describe('useSelection', () => { describe('deselectRow', () => { it('should make row unselected', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.selectRow(1); @@ -102,7 +108,7 @@ describe('useSelection', () => { describe('toggleRow', () => { it('should select unselected row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleRow(1); @@ -113,7 +119,7 @@ describe('useSelection', () => { }); it('should deselect selected row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleRow(1); @@ -128,7 +134,7 @@ describe('useSelection', () => { }); it('should select another unselected row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleRow(1); @@ -146,7 +152,7 @@ describe('useSelection', () => { describe('allRowsSelected', () => { it('should return true if all rows are selected', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleAllRows(); @@ -156,14 +162,14 @@ describe('useSelection', () => { }); it('should return false if there is no selected row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); expect(result.current.selectedRows.size).toBe(0); expect(result.current.allRowsSelected).toBe(false); }); it('should return false if not all rows are selected', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.toggleAllRows(); @@ -180,7 +186,7 @@ describe('useSelection', () => { describe('someRowsSelected', () => { it('should return true if there is a selected row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); act(() => { result.current.selectRow(1); @@ -191,7 +197,7 @@ describe('useSelection', () => { }); it('should return false if there is no selected row', () => { - const { result } = renderHook(() => useSelection('multiselect', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); expect(result.current.selectedRows.size).toBe(0); expect(result.current.someRowsSelected).toBe(false); @@ -202,7 +208,7 @@ describe('useSelection', () => { describe('single select', () => { describe('toggleAllRows', () => { it('should throw when not in production', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); expect(result.current.toggleAllRows).toThrowErrorMatchingInlineSnapshot( `"[react-table]: \`toggleAllItems\` should not be used in single selection mode"`, @@ -211,7 +217,7 @@ describe('useSelection', () => { it('should be a noop in production', () => { const nodeEnv = (process.env.NODE_ENV = 'production'); - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); result.current.toggleAllRows; expect(result.current.selectedRows.size).toBe(0); @@ -221,7 +227,7 @@ describe('useSelection', () => { }); describe('clearRows', () => { it('should clear selection', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.selectRow(1); @@ -236,7 +242,7 @@ describe('useSelection', () => { describe('selectRow', () => { it('should select row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.selectRow(1); @@ -246,7 +252,7 @@ describe('useSelection', () => { }); it('should select another row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.selectRow(1); @@ -263,7 +269,7 @@ describe('useSelection', () => { describe('deselectRow', () => { it('should make row unselected', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.selectRow(1); @@ -279,7 +285,7 @@ describe('useSelection', () => { describe('toggleRow', () => { it('should select unselected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.toggleRow(1); @@ -290,7 +296,7 @@ describe('useSelection', () => { }); it('should deselect selected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.toggleRow(1); @@ -305,7 +311,7 @@ describe('useSelection', () => { }); it('should select another unselected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.toggleRow(1); @@ -323,7 +329,7 @@ describe('useSelection', () => { describe('allRowsSelected', () => { it('should return true if there is a selected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.selectRow(1); @@ -334,7 +340,7 @@ describe('useSelection', () => { }); it('should return false if there is no selected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); expect(result.current.selectedRows.size).toBe(0); expect(result.current.allRowsSelected).toBe(false); @@ -343,7 +349,7 @@ describe('useSelection', () => { describe('someRowsSelected', () => { it('should return true if there is a selected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); act(() => { result.current.selectRow(1); @@ -354,7 +360,7 @@ describe('useSelection', () => { }); it('should return false if there is no selected row', () => { - const { result } = renderHook(() => useSelection('single', items, getRowId)); + const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); expect(result.current.selectedRows.size).toBe(0); expect(result.current.someRowsSelected).toBe(false); diff --git a/packages/react-components/react-table/src/hooks/useSelection.ts b/packages/react-components/react-table/src/hooks/useSelection.ts index cff6a47e6d2fc..dc61c61eaeddd 100644 --- a/packages/react-components/react-table/src/hooks/useSelection.ts +++ b/packages/react-components/react-table/src/hooks/useSelection.ts @@ -1,38 +1,73 @@ import * as React from 'react'; -import { useEventCallback, usePrevious } from '@fluentui/react-utilities'; +import { useControllableState, useEventCallback } from '@fluentui/react-utilities'; import { createSelectionManager } from './selectionManager'; -import { GetRowIdInternal, RowId, SelectionMode, SelectionStateInternal } from './types'; - -export function useSelection( - selectionMode: SelectionMode, - items: TItem[], - getRowId: GetRowIdInternal, -): SelectionStateInternal { - const prevSelectionMode = usePrevious(selectionMode); - const [selected, setSelected] = React.useState(() => new Set()); - const [selectionManager, setSelectionManager] = React.useState(() => - createSelectionManager(selectionMode, setSelected), - ); +import type { + GetRowIdInternal, + OnSelectionChangeCallback, + RowId, + SelectionMode, + SelectionStateInternal, +} from './types'; + +interface UseSelectionOptions { + selectionMode: SelectionMode; + items: TItem[]; + getRowId: GetRowIdInternal; + defaultSelectedItems?: Set; + selectedItems?: Set; + onSelectionChange?: OnSelectionChangeCallback; +} - React.useEffect(() => { - if (prevSelectionMode !== selectionMode) { - setSelectionManager(createSelectionManager(selectionMode, setSelected)); - } - }, [selectionMode, prevSelectionMode]); +export function useSelection(options: UseSelectionOptions): SelectionStateInternal { + const { selectionMode, items, getRowId, defaultSelectedItems, selectedItems, onSelectionChange } = options; + + const [selected, setSelected] = useControllableState({ + initialState: new Set(), + defaultState: defaultSelectedItems, + state: selectedItems, + }); + + const selectionManager = React.useMemo(() => { + return createSelectionManager(selectionMode, newSelectedItems => { + setSelected(() => { + onSelectionChange?.(newSelectedItems); + return newSelectedItems; + }); + }); + }, [onSelectionChange, selectionMode, setSelected]); const toggleAllRows: SelectionStateInternal['toggleAllRows'] = useEventCallback(() => { - selectionManager.toggleAllItems(items.map((item, i) => getRowId(item, i))); + selectionManager.toggleAllItems( + items.map((item, i) => getRowId(item, i)), + selected, + ); }); + const toggleRow: SelectionStateInternal['toggleRow'] = useEventCallback((rowId: RowId) => + selectionManager.toggleItem(rowId, selected), + ); + + const deselectRow: SelectionStateInternal['deselectRow'] = useEventCallback((rowId: RowId) => + selectionManager.deselectItem(rowId, selected), + ); + + const selectRow: SelectionStateInternal['selectRow'] = useEventCallback((rowId: RowId) => + selectionManager.selectItem(rowId, selected), + ); + + const isRowSelected: SelectionStateInternal['isRowSelected'] = useEventCallback((rowId: RowId) => + selectionManager.isSelected(rowId, selected), + ); + return { someRowsSelected: selected.size > 0, allRowsSelected: selectionMode === 'single' ? selected.size > 0 : selected.size === items.length, selectedRows: selected, - toggleRow: selectionManager.toggleItem, + toggleRow, toggleAllRows, clearRows: selectionManager.clearItems, - deselectRow: selectionManager.deselectItem, - selectRow: selectionManager.selectItem, - isRowSelected: selectionManager.isSelected, + deselectRow, + selectRow, + isRowSelected, }; } diff --git a/packages/react-components/react-table/src/hooks/useSort.test.ts b/packages/react-components/react-table/src/hooks/useSort.test.ts index 045a1bd311656..157833539b077 100644 --- a/packages/react-components/react-table/src/hooks/useSort.test.ts +++ b/packages/react-components/react-table/src/hooks/useSort.test.ts @@ -6,7 +6,7 @@ describe('useSort', () => { describe('toggleColumnSort', () => { it('should sort a new column in ascending order', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.toggleColumnSort(1); }); @@ -17,7 +17,7 @@ describe('useSort', () => { it('should toggle sort direction on a column', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.toggleColumnSort(1); }); @@ -34,7 +34,7 @@ describe('useSort', () => { describe('setColumnSort', () => { it('should sort a column in ascending order', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.setColumnSort(1, 'ascending'); }); @@ -45,7 +45,7 @@ describe('useSort', () => { it('should sort a column in descending order', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.setColumnSort(1, 'descending'); }); @@ -65,7 +65,7 @@ describe('useSort', () => { { columnId: 3, compare: createMockCompare() }, ]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.toggleColumnSort(2); }); @@ -79,7 +79,7 @@ describe('useSort', () => { { columnId: 1, compare: (a, b) => a.value - b.value }, ]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.toggleColumnSort(1); }); @@ -93,7 +93,7 @@ describe('useSort', () => { { columnId: 1, compare: (a, b) => a.value - b.value }, ]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.toggleColumnSort(1); }); @@ -110,7 +110,7 @@ describe('useSort', () => { it('should return sort direction for the sorted column', () => { const columnDefinition: ColumnDefinition<{ value: number }>[] = [{ columnId: 1 }]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.setColumnSort(1, 'descending'); }); @@ -121,7 +121,7 @@ describe('useSort', () => { it('should return undefined for unsorted column', () => { const columnDefinition: ColumnDefinition<{ value: number }>[] = [{ columnId: 1 }, { columnId: 2 }]; - const { result } = renderHook(() => useSort(columnDefinition)); + const { result } = renderHook(() => useSort({ columns: columnDefinition })); act(() => { result.current.setColumnSort(1, 'descending'); }); diff --git a/packages/react-components/react-table/src/hooks/useSort.ts b/packages/react-components/react-table/src/hooks/useSort.ts index df79f559d9e27..4b8eac10bcc96 100644 --- a/packages/react-components/react-table/src/hooks/useSort.ts +++ b/packages/react-components/react-table/src/hooks/useSort.ts @@ -1,11 +1,29 @@ -import * as React from 'react'; import { SortDirection } from '../components/Table/Table.types'; -import type { ColumnDefinition, ColumnId, SortStateInternal } from './types'; +import { useControllableState } from '@fluentui/react-utilities'; +import type { ColumnDefinition, ColumnId, OnSortChangeCallback, SortStateInternal } from './types'; -export function useSort(columns: ColumnDefinition[]): SortStateInternal { - const [sorted, setSorted] = React.useState({ - sortDirection: 'ascending' as SortDirection, - sortColumn: undefined as ColumnId | undefined, +interface SortState { + sortDirection: SortDirection; + sortColumn: ColumnId | undefined; +} + +interface UseSortOptions { + columns: ColumnDefinition[]; + sortState?: SortState; + defaultSortState?: SortState; + onSortChange?: OnSortChangeCallback; +} + +export function useSort(options: UseSortOptions): SortStateInternal { + const { columns, sortState, defaultSortState, onSortChange } = options; + + const [sorted, setSorted] = useControllableState({ + initialState: { + sortDirection: 'ascending' as SortDirection, + sortColumn: undefined as ColumnId | undefined, + }, + defaultState: defaultSortState, + state: sortState, }); const { sortColumn, sortDirection } = sorted; @@ -19,12 +37,15 @@ export function useSort(columns: ColumnDefinition[]): SortStateInt newState.sortDirection = 'ascending'; } + onSortChange?.(newState); return newState; }); }; const setColumnSort: SortStateInternal['setColumnSort'] = (nextSortColumn, nextSortDirection) => { - setSorted({ sortColumn: nextSortColumn, sortDirection: nextSortDirection }); + const newState = { sortColumn: nextSortColumn, sortDirection: nextSortDirection }; + onSortChange?.(newState); + setSorted(newState); }; const sort = (items: TItem[]) => diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index e9ddf64618112..1ddddcbcc5339 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { UseTableOptions, TableState, RowState, SelectionState, SortState } from './types'; +import type { UseTableOptions, TableState, RowState, SelectionState, SortState, GetRowIdInternal } from './types'; import { useSelection } from './useSelection'; import { useSort } from './useSort'; @@ -12,10 +12,24 @@ export function useTable = RowState undefined, selectionMode = 'multiselect', rowEnhancer = (row: RowState) => row as TRowState, + defaultSelectedRows, + selectedRows: userSelectedRows, + onSelectionChange, + sortState: userSortState, + defaultSortState, + onSortChange, } = options; - const getRowId = React.useCallback((item: TItem, index: number) => getUserRowId(item) ?? index, [getUserRowId]); - const { sortColumn, sortDirection, toggleColumnSort, setColumnSort, getSortDirection, sort } = useSort(columns); + const getRowId: GetRowIdInternal = React.useCallback( + (item: TItem, index: number) => getUserRowId(item) ?? index, + [getUserRowId], + ); + const { sortColumn, sortDirection, toggleColumnSort, setColumnSort, getSortDirection, sort } = useSort({ + columns, + sortState: userSortState, + defaultSortState, + onSortChange, + }); const sortState: SortState = React.useMemo( () => ({ sortColumn, @@ -37,7 +51,14 @@ export function useTable = RowState ({ diff --git a/packages/react-components/react-table/src/stories/Table/MultipleSelect.stories.tsx b/packages/react-components/react-table/src/stories/Table/MultipleSelect.stories.tsx index ea99ea84b03b2..95cc79b9f02f4 100644 --- a/packages/react-components/react-table/src/stories/Table/MultipleSelect.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/MultipleSelect.stories.tsx @@ -101,6 +101,7 @@ export const MultipleSelect = () => { } = useTable({ columns, items, + defaultSelectedRows: new Set([0, 1]), rowEnhancer: (row, { selection }) => ({ ...row, onClick: () => selection.toggleRow(row.rowId), diff --git a/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx b/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx new file mode 100644 index 0000000000000..052fe68b0b88e --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { + FolderRegular, + EditRegular, + OpenRegular, + DocumentRegular, + PeopleRegular, + DocumentPdfRegular, + VideoRegular, +} from '@fluentui/react-icons'; +import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; +import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell, TableSelectionCell } from '../..'; +import { useTable, ColumnDefinition, RowId } from '../../hooks'; +import { useNavigationMode } from '../../navigationModes/useNavigationMode'; + +type FileCell = { + label: string; + icon: JSX.Element; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; + icon: JSX.Element; +}; + +type AuthorCell = { + label: string; + status: PresenceBadgeStatus; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const items: Item[] = [ + { + file: { label: 'Meeting notes', icon: }, + author: { label: 'Max Mustermann', status: 'available' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + icon: , + }, + }, + { + file: { label: 'Thursday presentation', icon: }, + author: { label: 'Erika Mustermann', status: 'busy' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + icon: , + }, + }, + { + file: { label: 'Training recording', icon: }, + author: { label: 'John Doe', status: 'away' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + icon: , + }, + }, + { + file: { label: 'Purchase order', icon: }, + author: { label: 'Jane Doe', status: 'offline' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + icon: , + }, + }, +]; + +const columns: ColumnDefinition[] = [ + { + columnId: 'file', + }, + { + columnId: 'author', + }, + { + columnId: 'lastUpdated', + }, + { + columnId: 'lastUpdate', + }, +]; + +export const MultipleSelectControlled = () => { + const [selectedRows, setSelectedRows] = React.useState(new Set()); + const { + rows, + selection: { allRowsSelected, someRowsSelected, toggleAllRows }, + } = useTable({ + columns, + items, + selectedRows, + onSelectionChange: setSelectedRows, + rowEnhancer: (row, { selection }) => ({ + ...row, + onClick: () => selection.toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + selection.toggleRow(row.rowId); + } + }, + selected: selection.isRowSelected(row.rowId), + }), + }); + + // eslint-disable-next-line deprecation/deprecation + const ref = useNavigationMode('row'); + + return ( + + + + + File + Author + Last updated + Last update + + + + {rows.map(({ item, selected, onClick, onKeyDown }) => ( + + + {item.file.label} + }>{item.author.label} + {item.lastUpdated.label} + {item.lastUpdate.label} + + ))} + +
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/SingleSelect.stories.tsx b/packages/react-components/react-table/src/stories/Table/SingleSelect.stories.tsx index d6497811a5706..8f9f98fb7a73d 100644 --- a/packages/react-components/react-table/src/stories/Table/SingleSelect.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/SingleSelect.stories.tsx @@ -99,6 +99,7 @@ export const SingleSelect = () => { columns, items, selectionMode: 'single', + defaultSelectedRows: new Set([1]), rowEnhancer: (row, { selection }) => ({ ...row, selected: selection.isRowSelected(row.rowId), diff --git a/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx b/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx new file mode 100644 index 0000000000000..6f90817f93c36 --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx @@ -0,0 +1,143 @@ +import * as React from 'react'; +import { + FolderRegular, + EditRegular, + OpenRegular, + DocumentRegular, + PeopleRegular, + DocumentPdfRegular, + VideoRegular, +} from '@fluentui/react-icons'; +import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; +import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell, TableSelectionCell } from '../..'; +import { useTable, ColumnDefinition, RowId } from '../../hooks'; +import { useNavigationMode } from '../../navigationModes/useNavigationMode'; + +type FileCell = { + label: string; + icon: JSX.Element; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; + icon: JSX.Element; +}; + +type AuthorCell = { + label: string; + status: PresenceBadgeStatus; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const items: Item[] = [ + { + file: { label: 'Meeting notes', icon: }, + author: { label: 'Max Mustermann', status: 'available' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + icon: , + }, + }, + { + file: { label: 'Thursday presentation', icon: }, + author: { label: 'Erika Mustermann', status: 'busy' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + icon: , + }, + }, + { + file: { label: 'Training recording', icon: }, + author: { label: 'John Doe', status: 'away' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + icon: , + }, + }, + { + file: { label: 'Purchase order', icon: }, + author: { label: 'Jane Doe', status: 'offline' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + icon: , + }, + }, +]; + +const columns: ColumnDefinition[] = [ + { + columnId: 'file', + }, + { + columnId: 'author', + }, + { + columnId: 'lastUpdated', + }, + { + columnId: 'lastUpdate', + }, +]; + +export const SingleSelectControlled = () => { + const [selectedRows, setSelectedRows] = React.useState(new Set()); + const { rows } = useTable({ + columns, + items, + selectionMode: 'single', + selectedRows, + onSelectionChange: setSelectedRows, + rowEnhancer: (row, { selection }) => ({ + ...row, + selected: selection.isRowSelected(row.rowId), + onClick: () => selection.toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + selection.toggleRow(row.rowId); + } + }, + }), + }); + // eslint-disable-next-line deprecation/deprecation + const ref = useNavigationMode('row'); + + return ( + + + + + File + Author + Last updated + Last update + + + + {rows.map(({ item, selected, onClick, onKeyDown }) => ( + + + {item.file.label} + }>{item.author.label} + {item.lastUpdated.label} + {item.lastUpdate.label} + + ))} + +
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/Sort.stories.tsx b/packages/react-components/react-table/src/stories/Table/Sort.stories.tsx index 4196abd903ba7..61134b2301487 100644 --- a/packages/react-components/react-table/src/stories/Table/Sort.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/Sort.stories.tsx @@ -109,10 +109,12 @@ export const Sort = () => { const { rows, sort: { getSortDirection, toggleColumnSort }, - } = useTable({ columns, items }); + } = useTable({ columns, items, defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); - const headerSortProps = (columnId: ColumnId) => () => ({ - onClick: () => toggleColumnSort(columnId), + const headerSortProps = (columnId: ColumnId) => ({ + onClick: () => { + toggleColumnSort(columnId); + }, sortDirection: getSortDirection(columnId), }); diff --git a/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx b/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx new file mode 100644 index 0000000000000..c74a06c3ba383 --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { + FolderRegular, + EditRegular, + OpenRegular, + DocumentRegular, + PeopleRegular, + DocumentPdfRegular, + VideoRegular, +} from '@fluentui/react-icons'; +import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; +import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell } from '../..'; +import { useTable, ColumnDefinition, ColumnId } from '../../hooks'; +import { SortDirection } from '../../components/Table/Table.types'; + +type FileCell = { + label: string; + icon: JSX.Element; +}; + +type LastUpdatedCell = { + label: string; + timestamp: number; +}; + +type LastUpdateCell = { + label: string; + icon: JSX.Element; +}; + +type AuthorCell = { + label: string; + status: PresenceBadgeStatus; +}; + +type Item = { + file: FileCell; + author: AuthorCell; + lastUpdated: LastUpdatedCell; + lastUpdate: LastUpdateCell; +}; + +const items: Item[] = [ + { + file: { label: 'Meeting notes', icon: }, + author: { label: 'Max Mustermann', status: 'available' }, + lastUpdated: { label: '7h ago', timestamp: 3 }, + lastUpdate: { + label: 'You edited this', + icon: , + }, + }, + { + file: { label: 'Thursday presentation', icon: }, + author: { label: 'Erika Mustermann', status: 'busy' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + icon: , + }, + }, + { + file: { label: 'Training recording', icon: }, + author: { label: 'John Doe', status: 'away' }, + lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 }, + lastUpdate: { + label: 'You recently opened this', + icon: , + }, + }, + { + file: { label: 'Purchase order', icon: }, + author: { label: 'Jane Doe', status: 'offline' }, + lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 1 }, + lastUpdate: { + label: 'You shared this in a Teams chat', + icon: , + }, + }, +]; + +const columns: ColumnDefinition[] = [ + { + columnId: 'file', + compare: (a, b) => { + return a.file.label.localeCompare(b.file.label); + }, + }, + { + columnId: 'author', + compare: (a, b) => { + return a.author.label.localeCompare(b.author.label); + }, + }, + { + columnId: 'lastUpdated', + compare: (a, b) => { + return a.lastUpdated.timestamp - b.lastUpdated.timestamp; + }, + }, + { + columnId: 'lastUpdate', + compare: (a, b) => { + return a.lastUpdate.label.localeCompare(b.lastUpdate.label); + }, + }, +]; + +export const SortControlled = () => { + const [sortState, setSortState] = React.useState({ + sortDirection: 'ascending' as SortDirection, + sortColumn: 'file' as ColumnId | undefined, + }); + + const { + rows, + sort: { getSortDirection, toggleColumnSort }, + } = useTable({ columns, items, sortState, onSortChange: setSortState }); + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: () => toggleColumnSort(columnId), + sortDirection: getSortDirection(columnId), + }); + + return ( + + + + File + Author + Last updated + Last update + + + + {rows.map(({ item }) => ( + + {item.file.label} + }>{item.author.label} + {item.lastUpdated.label} + {item.lastUpdate.label} + + ))} + +
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/index.stories.tsx b/packages/react-components/react-table/src/stories/Table/index.stories.tsx index e4081a8ded3a3..5ca28f8f253ec 100644 --- a/packages/react-components/react-table/src/stories/Table/index.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/index.stories.tsx @@ -2,6 +2,7 @@ import { Table } from '../..'; export { Default } from './Default.stories'; export { Sort } from './Sort.stories'; +export { SortControlled } from './SortControlled.stories'; export { CellActions } from './CellActions.stories'; export { PrimaryCell } from './PrimaryCell.stories'; export { SizeSmall } from './SizeSmall.stories'; @@ -9,6 +10,8 @@ export { SizeSmaller } from './SizeSmaller.stories'; export { NonNativeElements } from './NonNativeElements.stories'; export { MultipleSelect } from './MultipleSelect.stories'; export { SingleSelect } from './SingleSelect.stories'; +export { MultipleSelectControlled } from './MultipleSelectControlled.stories'; +export { SingleSelectControlled } from './SingleSelectControlled.stories'; export { CellNavigationMode } from './CellNavigationMode.stories'; export { RowNavigationMode } from './RowNavigationMode.stories'; export { CompositeNavigationMode } from './CompositeNavigationMode.stories'; From 8b80d62037bd633d1378c12511b76754bf3f8ad8 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 09:38:30 +0200 Subject: [PATCH 2/8] add Table prefix to state types to avoid clash --- .../react-table/etc/react-table.api.md | 44 +++++++++---------- .../react-table/src/hooks/types.ts | 14 +++--- .../react-table/src/hooks/useSelection.ts | 17 ++++--- .../react-table/src/hooks/useSort.ts | 14 +++--- .../react-table/src/hooks/useTable.ts | 13 ++++-- .../react-components/react-table/src/index.ts | 10 ++++- 6 files changed, 63 insertions(+), 49 deletions(-) diff --git a/packages/react-components/react-table/etc/react-table.api.md b/packages/react-components/react-table/etc/react-table.api.md index 06b535f21a29a..384bb1b2ad8c4 100644 --- a/packages/react-components/react-table/etc/react-table.api.md +++ b/packages/react-components/react-table/etc/react-table.api.md @@ -67,31 +67,9 @@ export interface RowState { rowId: RowId; } -// @public (undocumented) -export interface SelectionState { - allRowsSelected: boolean; - clearRows: () => void; - deselectRow: (rowId: RowId) => void; - isRowSelected: (rowId: RowId) => boolean; - selectedRows: RowId[]; - selectRow: (rowId: RowId) => void; - someRowsSelected: boolean; - toggleAllRows: () => void; - toggleRow: (rowId: RowId) => void; -} - // @public (undocumented) export type SortDirection = 'ascending' | 'descending'; -// @public (undocumented) -export interface SortState { - getSortDirection: (columnId: ColumnId) => SortDirection | undefined; - setColumnSort: (columnId: ColumnId, sortDirection: SortDirection) => void; - sortColumn: ColumnId | undefined; - sortDirection: SortDirection; - toggleColumnSort: (columnId: ColumnId) => void; -} - // @public export const Table: ForwardRefComponent; @@ -285,11 +263,33 @@ export type TableSelectionCellSlots = { // @public export type TableSelectionCellState = ComponentState & Pick & Pick, 'type' | 'checked'>; +// @public (undocumented) +export interface TableSelectionState { + allRowsSelected: boolean; + clearRows: () => void; + deselectRow: (rowId: RowId) => void; + isRowSelected: (rowId: RowId) => boolean; + selectedRows: RowId[]; + selectRow: (rowId: RowId) => void; + someRowsSelected: boolean; + toggleAllRows: () => void; + toggleRow: (rowId: RowId) => void; +} + // @public (undocumented) export type TableSlots = { root: Slot<'table', 'div'>; }; +// @public (undocumented) +export interface TableSortState { + getSortDirection: (columnId: ColumnId) => SortDirection | undefined; + setColumnSort: (columnId: ColumnId, sortDirection: SortDirection) => void; + sortColumn: ColumnId | undefined; + sortDirection: SortDirection; + toggleColumnSort: (columnId: ColumnId) => void; +} + // @public export type TableState = ComponentState & Pick, 'size' | 'noNativeElements'> & TableContextValue; diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 256fc909ae331..6a05d6a8f4f70 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -14,10 +14,10 @@ export interface ColumnDefinition { export type RowEnhancer = RowState> = ( row: RowState, - state: { selection: SelectionState; sort: SortState }, + state: { selection: TableSelectionState; sort: TableSortState }, ) => TRowState; -export interface SortStateInternal { +export interface TableSortStateInternal { sortDirection: SortDirection; sortColumn: ColumnId | undefined; setColumnSort: (columnId: ColumnId, sortDirection: SortDirection) => void; @@ -67,7 +67,7 @@ export interface UseTableOptions = RowS rowEnhancer?: RowEnhancer; } -export interface SelectionStateInternal { +export interface TableSelectionStateInternal { clearRows: () => void; deselectRow: (rowId: RowId) => void; selectRow: (rowId: RowId) => void; @@ -79,7 +79,7 @@ export interface SelectionStateInternal { someRowsSelected: boolean; } -export interface SortState { +export interface TableSortState { /** * Current sort direction */ @@ -103,7 +103,7 @@ export interface SortState { getSortDirection: (columnId: ColumnId) => SortDirection | undefined; } -export interface SelectionState { +export interface TableSelectionState { /** * Clears all selected rows */ @@ -162,9 +162,9 @@ export interface TableState = RowState< /** * State and actions to manage row selection */ - selection: SelectionState; + selection: TableSelectionState; /** * State and actions to manage row sorting */ - sort: SortState; + sort: TableSortState; } diff --git a/packages/react-components/react-table/src/hooks/useSelection.ts b/packages/react-components/react-table/src/hooks/useSelection.ts index dc61c61eaeddd..d83d59470d637 100644 --- a/packages/react-components/react-table/src/hooks/useSelection.ts +++ b/packages/react-components/react-table/src/hooks/useSelection.ts @@ -6,7 +6,7 @@ import type { OnSelectionChangeCallback, RowId, SelectionMode, - SelectionStateInternal, + TableSelectionStateInternal, } from './types'; interface UseSelectionOptions { @@ -18,7 +18,7 @@ interface UseSelectionOptions { onSelectionChange?: OnSelectionChangeCallback; } -export function useSelection(options: UseSelectionOptions): SelectionStateInternal { +export function useSelection(options: UseSelectionOptions): TableSelectionStateInternal { const { selectionMode, items, getRowId, defaultSelectedItems, selectedItems, onSelectionChange } = options; const [selected, setSelected] = useControllableState({ @@ -36,28 +36,27 @@ export function useSelection(options: UseSelectionOptions): Select }); }, [onSelectionChange, selectionMode, setSelected]); - const toggleAllRows: SelectionStateInternal['toggleAllRows'] = useEventCallback(() => { + const toggleAllRows: TableSelectionStateInternal['toggleAllRows'] = useEventCallback(() => { selectionManager.toggleAllItems( items.map((item, i) => getRowId(item, i)), selected, ); }); - const toggleRow: SelectionStateInternal['toggleRow'] = useEventCallback((rowId: RowId) => + const toggleRow: TableSelectionStateInternal['toggleRow'] = useEventCallback((rowId: RowId) => selectionManager.toggleItem(rowId, selected), ); - const deselectRow: SelectionStateInternal['deselectRow'] = useEventCallback((rowId: RowId) => + const deselectRow: TableSelectionStateInternal['deselectRow'] = useEventCallback((rowId: RowId) => selectionManager.deselectItem(rowId, selected), ); - const selectRow: SelectionStateInternal['selectRow'] = useEventCallback((rowId: RowId) => + const selectRow: TableSelectionStateInternal['selectRow'] = useEventCallback((rowId: RowId) => selectionManager.selectItem(rowId, selected), ); - const isRowSelected: SelectionStateInternal['isRowSelected'] = useEventCallback((rowId: RowId) => - selectionManager.isSelected(rowId, selected), - ); + const isRowSelected: TableSelectionStateInternal['isRowSelected'] = (rowId: RowId) => + selectionManager.isSelected(rowId, selected); return { someRowsSelected: selected.size > 0, diff --git a/packages/react-components/react-table/src/hooks/useSort.ts b/packages/react-components/react-table/src/hooks/useSort.ts index 4b8eac10bcc96..dcc312c8d6593 100644 --- a/packages/react-components/react-table/src/hooks/useSort.ts +++ b/packages/react-components/react-table/src/hooks/useSort.ts @@ -1,20 +1,20 @@ import { SortDirection } from '../components/Table/Table.types'; import { useControllableState } from '@fluentui/react-utilities'; -import type { ColumnDefinition, ColumnId, OnSortChangeCallback, SortStateInternal } from './types'; +import type { ColumnDefinition, ColumnId, OnSortChangeCallback, TableSortStateInternal } from './types'; -interface SortState { +interface TableSortState { sortDirection: SortDirection; sortColumn: ColumnId | undefined; } interface UseSortOptions { columns: ColumnDefinition[]; - sortState?: SortState; - defaultSortState?: SortState; + sortState?: TableSortState; + defaultSortState?: TableSortState; onSortChange?: OnSortChangeCallback; } -export function useSort(options: UseSortOptions): SortStateInternal { +export function useSort(options: UseSortOptions): TableSortStateInternal { const { columns, sortState, defaultSortState, onSortChange } = options; const [sorted, setSorted] = useControllableState({ @@ -42,7 +42,7 @@ export function useSort(options: UseSortOptions): SortStateInterna }); }; - const setColumnSort: SortStateInternal['setColumnSort'] = (nextSortColumn, nextSortDirection) => { + const setColumnSort: TableSortStateInternal['setColumnSort'] = (nextSortColumn, nextSortDirection) => { const newState = { sortColumn: nextSortColumn, sortDirection: nextSortDirection }; onSortChange?.(newState); setSorted(newState); @@ -59,7 +59,7 @@ export function useSort(options: UseSortOptions): SortStateInterna return sortColumnDef.compare(a, b) * mod; }); - const getSortDirection: SortStateInternal['getSortDirection'] = (columnId: ColumnId) => { + const getSortDirection: TableSortStateInternal['getSortDirection'] = (columnId: ColumnId) => { return sortColumn === columnId ? sortDirection : undefined; }; diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index 1ddddcbcc5339..40be03b1cc9c3 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -1,5 +1,12 @@ import * as React from 'react'; -import type { UseTableOptions, TableState, RowState, SelectionState, SortState, GetRowIdInternal } from './types'; +import type { + UseTableOptions, + TableState, + RowState, + TableSelectionState, + TableSortState, + GetRowIdInternal, +} from './types'; import { useSelection } from './useSelection'; import { useSort } from './useSort'; @@ -30,7 +37,7 @@ export function useTable = RowState ({ sortColumn, sortDirection, @@ -60,7 +67,7 @@ export function useTable = RowState ({ isRowSelected, clearRows, diff --git a/packages/react-components/react-table/src/index.ts b/packages/react-components/react-table/src/index.ts index 7887435d5d671..bf062106c3fcd 100644 --- a/packages/react-components/react-table/src/index.ts +++ b/packages/react-components/react-table/src/index.ts @@ -1,5 +1,13 @@ export { useTable } from './hooks'; -export type { UseTableOptions, SelectionState, SortState, ColumnDefinition, RowState, RowId, ColumnId } from './hooks'; +export type { + UseTableOptions, + TableSelectionState, + TableSortState, + ColumnDefinition, + RowState, + RowId, + ColumnId, +} from './hooks'; export { TableCell, From 79ca6269ab8392180e04799ea451d3f36a4e31ce Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 09:49:28 +0200 Subject: [PATCH 3/8] cleanup --- .../react-table/etc/react-table.api.md | 10 ++-------- .../react-table/src/hooks/types.ts | 15 +++++++-------- .../react-table/src/hooks/useSort.ts | 18 ++++++------------ .../stories/Table/SortControlled.stories.tsx | 9 ++++----- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/packages/react-components/react-table/etc/react-table.api.md b/packages/react-components/react-table/etc/react-table.api.md index 384bb1b2ad8c4..b6cb9bf5929e1 100644 --- a/packages/react-components/react-table/etc/react-table.api.md +++ b/packages/react-components/react-table/etc/react-table.api.md @@ -337,10 +337,7 @@ export interface UseTableOptions = RowS // (undocumented) columns: ColumnDefinition[]; defaultSelectedRows?: Set; - defaultSortState?: { - sortColumn: ColumnId | undefined; - sortDirection: SortDirection; - }; + defaultSortState?: SortState; // (undocumented) getRowId?: (item: TItem) => RowId; // (undocumented) @@ -352,10 +349,7 @@ export interface UseTableOptions = RowS selectedRows?: Set; // (undocumented) selectionMode?: SelectionMode_2; - sortState?: { - sortColumn: ColumnId | undefined; - sortDirection: SortDirection; - }; + sortState?: SortState; } // @public diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 6a05d6a8f4f70..bd5f581722b31 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -7,6 +7,11 @@ export type SelectionMode = 'single' | 'multiselect'; export type OnSelectionChangeCallback = (selectedItems: Set) => void; export type OnSortChangeCallback = (state: { sortColumn: ColumnId | undefined; sortDirection: SortDirection }) => void; +export interface SortState { + sortColumn: ColumnId | undefined; + sortDirection: SortDirection; +} + export interface ColumnDefinition { columnId: ColumnId; compare?: (a: TItem, b: TItem) => number; @@ -48,17 +53,11 @@ export interface UseTableOptions = RowS /** * Used to control sorting */ - sortState?: { - sortColumn: ColumnId | undefined; - sortDirection: SortDirection; - }; + sortState?: SortState; /** * Used in uncontrolled mode to set initial sort column and direction on mount */ - defaultSortState?: { - sortColumn: ColumnId | undefined; - sortDirection: SortDirection; - }; + defaultSortState?: SortState; /** * Called when sort changes */ diff --git a/packages/react-components/react-table/src/hooks/useSort.ts b/packages/react-components/react-table/src/hooks/useSort.ts index dcc312c8d6593..f4dec5aaf9760 100644 --- a/packages/react-components/react-table/src/hooks/useSort.ts +++ b/packages/react-components/react-table/src/hooks/useSort.ts @@ -1,26 +1,20 @@ -import { SortDirection } from '../components/Table/Table.types'; import { useControllableState } from '@fluentui/react-utilities'; -import type { ColumnDefinition, ColumnId, OnSortChangeCallback, TableSortStateInternal } from './types'; - -interface TableSortState { - sortDirection: SortDirection; - sortColumn: ColumnId | undefined; -} +import type { ColumnDefinition, ColumnId, OnSortChangeCallback, SortState, TableSortStateInternal } from './types'; interface UseSortOptions { columns: ColumnDefinition[]; - sortState?: TableSortState; - defaultSortState?: TableSortState; + sortState?: SortState; + defaultSortState?: SortState; onSortChange?: OnSortChangeCallback; } export function useSort(options: UseSortOptions): TableSortStateInternal { const { columns, sortState, defaultSortState, onSortChange } = options; - const [sorted, setSorted] = useControllableState({ + const [sorted, setSorted] = useControllableState({ initialState: { - sortDirection: 'ascending' as SortDirection, - sortColumn: undefined as ColumnId | undefined, + sortDirection: 'ascending' as const, + sortColumn: undefined, }, defaultState: defaultSortState, state: sortState, diff --git a/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx b/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx index c74a06c3ba383..a2eb8791b7b86 100644 --- a/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/SortControlled.stories.tsx @@ -10,8 +10,7 @@ import { } from '@fluentui/react-icons'; import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell } from '../..'; -import { useTable, ColumnDefinition, ColumnId } from '../../hooks'; -import { SortDirection } from '../../components/Table/Table.types'; +import { useTable, ColumnDefinition, ColumnId, SortState } from '../../hooks'; type FileCell = { label: string; @@ -107,9 +106,9 @@ const columns: ColumnDefinition[] = [ ]; export const SortControlled = () => { - const [sortState, setSortState] = React.useState({ - sortDirection: 'ascending' as SortDirection, - sortColumn: 'file' as ColumnId | undefined, + const [sortState, setSortState] = React.useState({ + sortDirection: 'ascending' as const, + sortColumn: 'file', }); const { From 2293d064d91ac5f3402cb3263c6eb2ead7f747e6 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 09:51:55 +0200 Subject: [PATCH 4/8] changefile --- ...i-react-table-f0050981-47d5-4725-9d63-99142fda10f9.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-table-f0050981-47d5-4725-9d63-99142fda10f9.json diff --git a/change/@fluentui-react-table-f0050981-47d5-4725-9d63-99142fda10f9.json b/change/@fluentui-react-table-f0050981-47d5-4725-9d63-99142fda10f9.json new file mode 100644 index 0000000000000..9d4f6e74b7934 --- /dev/null +++ b/change/@fluentui-react-table-f0050981-47d5-4725-9d63-99142fda10f9.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: autocontrolled `useTable` hook", + "packageName": "@fluentui/react-table", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} From f5ac736c4c840b6acc2706c6181d9055163b0381 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 10:57:07 +0200 Subject: [PATCH 5/8] add tests for useSelection --- .../src/hooks/useSelection.test.ts | 140 +++++++++++++++--- 1 file changed, 122 insertions(+), 18 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/useSelection.test.ts b/packages/react-components/react-table/src/hooks/useSelection.test.ts index 79064a8b7f6c6..379100bbbf533 100644 --- a/packages/react-components/react-table/src/hooks/useSelection.test.ts +++ b/packages/react-components/react-table/src/hooks/useSelection.test.ts @@ -6,6 +6,22 @@ describe('useSelection', () => { const getRowId = (item: {}, index: number) => index; + it('should use default selected state', () => { + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, defaultSelectedItems: new Set([1]) }), + ); + + expect(Array.from(result.current.selectedRows)).toEqual([1]); + }); + + it('should use user selected state', () => { + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, selectedItems: new Set([1]) }), + ); + + expect(Array.from(result.current.selectedRows)).toEqual([1]); + }); + describe('multiselect', () => { it('should use custom row id', () => { const { result } = renderHook(() => @@ -24,17 +40,25 @@ describe('useSelection', () => { describe('toggleAllRows', () => { it('should select all rows', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleAllRows(); }); expect(result.current.selectedRows.size).toBe(items.length); expect(Array.from(result.current.selectedRows)).toEqual(items.map((_, i) => i)); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledWith(new Set([0, 1, 2, 3])); }); it('should deselect all rows', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleAllRows(); @@ -45,11 +69,16 @@ describe('useSelection', () => { }); expect(result.current.selectedRows.size).toBe(0); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set()); }); }); describe('clearRows', () => { it('should clear selection', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleAllRows(); @@ -59,22 +88,32 @@ describe('useSelection', () => { }); expect(result.current.selectedRows.size).toBe(0); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set()); }); }); describe('selectRow', () => { it('should select row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); }); expect(result.current.selectedRows.has(1)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledWith(new Set([1])); }); it('should select multiple rows', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); @@ -87,12 +126,17 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(2); expect(result.current.selectedRows.has(1)).toBe(true); expect(result.current.selectedRows.has(2)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set([1, 2])); }); }); describe('deselectRow', () => { it('should make row unselected', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); @@ -103,12 +147,17 @@ describe('useSelection', () => { }); expect(result.current.selectedRows.size).toBe(0); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set()); }); }); describe('toggleRow', () => { it('should select unselected row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleRow(1); @@ -116,10 +165,15 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(1); expect(result.current.selectedRows.has(1)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledWith(new Set([1])); }); it('should deselect selected row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleRow(1); @@ -131,10 +185,15 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(0); expect(result.current.selectedRows.has(1)).toBe(false); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set()); }); it('should select another unselected row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'multiselect', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'multiselect', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleRow(1); @@ -147,6 +206,8 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(2); expect(result.current.selectedRows.has(1)).toBe(true); expect(result.current.selectedRows.has(2)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set([1, 2])); }); }); @@ -208,26 +269,37 @@ describe('useSelection', () => { describe('single select', () => { describe('toggleAllRows', () => { it('should throw when not in production', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); expect(result.current.toggleAllRows).toThrowErrorMatchingInlineSnapshot( `"[react-table]: \`toggleAllItems\` should not be used in single selection mode"`, ); + expect(onSelectionChange).toHaveBeenCalledTimes(0); }); it('should be a noop in production', () => { const nodeEnv = (process.env.NODE_ENV = 'production'); - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); result.current.toggleAllRows; expect(result.current.selectedRows.size).toBe(0); + expect(onSelectionChange).toHaveBeenCalledTimes(0); process.env.NODE_ENV = nodeEnv; }); }); describe('clearRows', () => { it('should clear selection', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); @@ -237,22 +309,32 @@ describe('useSelection', () => { }); expect(result.current.selectedRows.size).toBe(0); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set()); }); }); describe('selectRow', () => { it('should select row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); }); expect(result.current.selectedRows.has(1)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledWith(new Set([1])); }); it('should select another row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); @@ -264,12 +346,17 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(1); expect(result.current.selectedRows.has(2)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set([2])); }); }); describe('deselectRow', () => { it('should make row unselected', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.selectRow(1); @@ -280,12 +367,17 @@ describe('useSelection', () => { }); expect(result.current.selectedRows.size).toBe(0); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set()); }); }); describe('toggleRow', () => { it('should select unselected row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleRow(1); @@ -293,10 +385,15 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(1); expect(result.current.selectedRows.has(1)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(1); + expect(onSelectionChange).toHaveBeenCalledWith(new Set([1])); }); it('should deselect selected row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleRow(1); @@ -308,10 +405,15 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(1); expect(result.current.selectedRows.has(1)).toBe(false); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set([2])); }); it('should select another unselected row', () => { - const { result } = renderHook(() => useSelection({ selectionMode: 'single', items, getRowId })); + const onSelectionChange = jest.fn(); + const { result } = renderHook(() => + useSelection({ selectionMode: 'single', items, getRowId, onSelectionChange }), + ); act(() => { result.current.toggleRow(1); @@ -324,6 +426,8 @@ describe('useSelection', () => { expect(result.current.selectedRows.size).toBe(1); expect(result.current.selectedRows.has(1)).toBe(false); expect(result.current.selectedRows.has(2)).toBe(true); + expect(onSelectionChange).toHaveBeenCalledTimes(2); + expect(onSelectionChange).toHaveBeenNthCalledWith(2, new Set([2])); }); }); From bbdcf2400605037a18b5cb7b6aa0b9d2cae09e26 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 11:07:12 +0200 Subject: [PATCH 6/8] add tests for useSort --- .../react-table/src/hooks/useSort.test.ts | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/useSort.test.ts b/packages/react-components/react-table/src/hooks/useSort.test.ts index 157833539b077..a28b06e5db8b9 100644 --- a/packages/react-components/react-table/src/hooks/useSort.test.ts +++ b/packages/react-components/react-table/src/hooks/useSort.test.ts @@ -3,21 +3,45 @@ import { ColumnDefinition } from './types'; import { useSort } from './useSort'; describe('useSort', () => { + it('should use default sort state', () => { + const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; + const { result } = renderHook(() => + useSort({ columns: columnDefinition, defaultSortState: { sortColumn: 2, sortDirection: 'descending' } }), + ); + + expect(result.current.getSortDirection(2)).toBe('descending'); + expect(result.current.sortColumn).toBe(2); + }); + + it('should use user sort state', () => { + const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; + const { result } = renderHook(() => + useSort({ columns: columnDefinition, sortState: { sortColumn: 2, sortDirection: 'descending' } }), + ); + + expect(result.current.getSortDirection(2)).toBe('descending'); + expect(result.current.sortColumn).toBe(2); + }); + describe('toggleColumnSort', () => { it('should sort a new column in ascending order', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort({ columns: columnDefinition })); + const onSortChange = jest.fn(); + const { result } = renderHook(() => useSort({ columns: columnDefinition, onSortChange })); act(() => { result.current.toggleColumnSort(1); }); expect(result.current.sortColumn).toBe(1); expect(result.current.sortDirection).toBe('ascending'); + expect(onSortChange).toHaveBeenCalledTimes(1); + expect(onSortChange).toHaveBeenCalledWith({ sortColumn: 1, sortDirection: 'ascending' }); }); it('should toggle sort direction on a column', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort({ columns: columnDefinition })); + const onSortChange = jest.fn(); + const { result } = renderHook(() => useSort({ columns: columnDefinition, onSortChange })); act(() => { result.current.toggleColumnSort(1); }); @@ -28,30 +52,38 @@ describe('useSort', () => { expect(result.current.sortColumn).toBe(1); expect(result.current.sortDirection).toBe('descending'); + expect(onSortChange).toHaveBeenCalledTimes(2); + expect(onSortChange).toHaveBeenNthCalledWith(2, { sortColumn: 1, sortDirection: 'descending' }); }); }); describe('setColumnSort', () => { it('should sort a column in ascending order', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort({ columns: columnDefinition })); + const onSortChange = jest.fn(); + const { result } = renderHook(() => useSort({ columns: columnDefinition, onSortChange })); act(() => { result.current.setColumnSort(1, 'ascending'); }); expect(result.current.sortColumn).toBe(1); expect(result.current.sortDirection).toBe('ascending'); + expect(onSortChange).toHaveBeenCalledTimes(1); + expect(onSortChange).toHaveBeenCalledWith({ sortColumn: 1, sortDirection: 'ascending' }); }); it('should sort a column in descending order', () => { const columnDefinition = [{ columnId: 1 }, { columnId: 2 }, { columnId: 3 }]; - const { result } = renderHook(() => useSort({ columns: columnDefinition })); + const onSortChange = jest.fn(); + const { result } = renderHook(() => useSort({ columns: columnDefinition, onSortChange })); act(() => { result.current.setColumnSort(1, 'descending'); }); expect(result.current.sortColumn).toBe(1); expect(result.current.sortDirection).toBe('descending'); + expect(onSortChange).toHaveBeenCalledTimes(1); + expect(onSortChange).toHaveBeenCalledWith({ sortColumn: 1, sortDirection: 'descending' }); }); }); From d0b3c512cc02c84d7f6d20284c478450c2810a90 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 7 Sep 2022 11:07:58 +0200 Subject: [PATCH 7/8] add suggestion --- .../react-table/src/components/Table/Table.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-table/src/components/Table/Table.types.ts b/packages/react-components/react-table/src/components/Table/Table.types.ts index 84fb43f12b276..eb5cd0b2231e8 100644 --- a/packages/react-components/react-table/src/components/Table/Table.types.ts +++ b/packages/react-components/react-table/src/components/Table/Table.types.ts @@ -21,7 +21,7 @@ export type TableContextValues = { /** * Table Props */ -export type TableProps = ComponentProps & {} & Partial; +export type TableProps = ComponentProps & Partial; /** * State used in rendering Table From 653722943ff95661080e93050309b9dd8e4b6378 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 8 Sep 2022 07:50:21 +0200 Subject: [PATCH 8/8] update md --- packages/react-components/react-table/etc/react-table.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-table/etc/react-table.api.md b/packages/react-components/react-table/etc/react-table.api.md index b6cb9bf5929e1..97d76626592cf 100644 --- a/packages/react-components/react-table/etc/react-table.api.md +++ b/packages/react-components/react-table/etc/react-table.api.md @@ -218,7 +218,7 @@ export type TablePrimaryCellSlots = { export type TablePrimaryCellState = ComponentState; // @public -export type TableProps = ComponentProps & {} & Partial; +export type TableProps = ComponentProps & Partial; // @public export const TableRow: ForwardRefComponent;