From 95f672bbe67bd5b9c6897b5b8c7f19969ba00dd3 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 8 Sep 2022 11:17:23 +0200 Subject: [PATCH 01/15] initial attempt at reusing hooks --- .../react-table/src/hooks/index.ts | 2 + .../react-table/src/hooks/types.ts | 27 +--------- .../react-table/src/hooks/useSelection.ts | 14 ++++- .../react-table/src/hooks/useSort.ts | 9 ++++ .../react-table/src/hooks/useTable.ts | 53 ++++++++++--------- .../stories/Table/MultipleSelect.stories.tsx | 9 +++- .../MultipleSelectControlled.stories.tsx | 12 +++-- .../stories/Table/SingleSelect.stories.tsx | 10 ++-- .../Table/SingleSelectControlled.stories.tsx | 13 +++-- .../src/stories/Table/Sort.stories.tsx | 8 ++- .../stories/Table/SortControlled.stories.tsx | 4 +- 11 files changed, 95 insertions(+), 66 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/index.ts b/packages/react-components/react-table/src/hooks/index.ts index f5090f5922fa7..1f47e3e6249ca 100644 --- a/packages/react-components/react-table/src/hooks/index.ts +++ b/packages/react-components/react-table/src/hooks/index.ts @@ -1,2 +1,4 @@ export * from './types'; export * from './useTable'; +export * from './useSelection'; +export * from './useSort'; diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index bd5f581722b31..e1072c052192e 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -35,33 +35,10 @@ export interface TableSortStateInternal { } export interface UseTableOptions = RowState> { + selection?: TableSelectionStateInternal; + sort?: TableSortStateInternal; 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?: SortState; - /** - * Used in uncontrolled mode to set initial sort column and direction on mount - */ - defaultSortState?: SortState; - /** - * Called when sort changes - */ - onSortChange?: OnSortChangeCallback; getRowId?: (item: TItem) => RowId; rowEnhancer?: RowEnhancer; } diff --git a/packages/react-components/react-table/src/hooks/useSelection.ts b/packages/react-components/react-table/src/hooks/useSelection.ts index d83d59470d637..1f1a6e31619d1 100644 --- a/packages/react-components/react-table/src/hooks/useSelection.ts +++ b/packages/react-components/react-table/src/hooks/useSelection.ts @@ -10,11 +10,23 @@ import type { } from './types'; interface UseSelectionOptions { - selectionMode: SelectionMode; items: TItem[]; getRowId: GetRowIdInternal; + /** + * Can be multi or single select + */ + selectionMode: SelectionMode; + /** + * Used in uncontrolled mode to set initial selected rows on mount + */ defaultSelectedItems?: Set; + /** + * Used to control row selection + */ selectedItems?: Set; + /** + * Called when selection changes + */ onSelectionChange?: OnSelectionChangeCallback; } diff --git a/packages/react-components/react-table/src/hooks/useSort.ts b/packages/react-components/react-table/src/hooks/useSort.ts index f4dec5aaf9760..7c983712aae02 100644 --- a/packages/react-components/react-table/src/hooks/useSort.ts +++ b/packages/react-components/react-table/src/hooks/useSort.ts @@ -3,8 +3,17 @@ import type { ColumnDefinition, ColumnId, OnSortChangeCallback, SortState, Table interface UseSortOptions { columns: ColumnDefinition[]; + /** + * Used to control sorting + */ sortState?: SortState; + /** + * Used in uncontrolled mode to set initial sort column and direction on mount + */ defaultSortState?: SortState; + /** + * Called when sort changes + */ onSortChange?: OnSortChangeCallback; } diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index 40be03b1cc9c3..3d614744262fe 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -6,37 +6,49 @@ import type { TableSelectionState, TableSortState, GetRowIdInternal, + TableSelectionStateInternal, + TableSortStateInternal, } from './types'; -import { useSelection } from './useSelection'; -import { useSort } from './useSort'; + +const noop: () => void = () => undefined; +const defaultSelectionState: TableSelectionStateInternal = { + allRowsSelected: false, + clearRows: noop, + deselectRow: noop, + isRowSelected: () => false, + selectRow: noop, + selectedRows: new Set(), + someRowsSelected: false, + toggleAllRows: noop, + toggleRow: noop, +}; + +const defaultSortState: TableSortStateInternal = { + getSortDirection: () => 'ascending', + setColumnSort: noop, + sort: (items: unknown[]) => items, + sortColumn: undefined, + sortDirection: 'ascending', + toggleColumnSort: noop, +}; export function useTable = RowState>( options: UseTableOptions, ): TableState { const { + selection = defaultSelectionState, + sort: baseSort = defaultSortState as TableSortStateInternal, items: baseItems, - columns, getRowId: getUserRowId = () => undefined, - selectionMode = 'multiselect', rowEnhancer = (row: RowState) => row as TRowState, - defaultSelectedRows, - selectedRows: userSelectedRows, - onSelectionChange, - sortState: userSortState, - defaultSortState, - onSortChange, } = options; 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 { sortColumn, sortDirection, toggleColumnSort, setColumnSort, getSortDirection, sort } = baseSort; const sortState: TableSortState = React.useMemo( () => ({ sortColumn, @@ -58,14 +70,7 @@ 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 95cc79b9f02f4..e84d4a5077261 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 @@ -10,7 +10,7 @@ import { } from '@fluentui/react-icons'; import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell, TableSelectionCell } from '../..'; -import { useTable, ColumnDefinition } from '../../hooks'; +import { useTable, ColumnDefinition, useSelection } from '../../hooks'; import { useNavigationMode } from '../../navigationModes/useNavigationMode'; type FileCell = { @@ -99,9 +99,14 @@ export const MultipleSelect = () => { rows, selection: { allRowsSelected, someRowsSelected, toggleAllRows }, } = useTable({ + selection: useSelection({ + items, + selectionMode: 'multiselect', + defaultSelectedItems: new Set([0, 1]), + getRowId: (item, index) => index, + }), 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 index 052fe68b0b88e..997bbec7b668f 100644 --- a/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx @@ -10,7 +10,7 @@ import { } 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 { useTable, ColumnDefinition, RowId, useSelection } from '../../hooks'; import { useNavigationMode } from '../../navigationModes/useNavigationMode'; type FileCell = { @@ -102,8 +102,14 @@ export const MultipleSelectControlled = () => { } = useTable({ columns, items, - selectedRows, - onSelectionChange: setSelectedRows, + selection: useSelection({ + items, + selectionMode: 'multiselect', + defaultSelectedItems: new Set([0, 1]), + getRowId: (item, index) => index, + selectedItems: selectedRows, + onSelectionChange: setSelectedRows, + }), rowEnhancer: (row, { selection }) => ({ ...row, onClick: () => selection.toggleRow(row.rowId), 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 8f9f98fb7a73d..17286c8038174 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 @@ -10,7 +10,7 @@ import { } from '@fluentui/react-icons'; import { PresenceBadgeStatus, Avatar } from '@fluentui/react-components'; import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell, TableSelectionCell } from '../..'; -import { useTable, ColumnDefinition } from '../../hooks'; +import { useTable, ColumnDefinition, useSelection } from '../../hooks'; import { useNavigationMode } from '../../navigationModes/useNavigationMode'; type FileCell = { @@ -96,10 +96,14 @@ const columns: ColumnDefinition[] = [ export const SingleSelect = () => { const { rows } = useTable({ + selection: useSelection({ + items, + selectionMode: 'single', + defaultSelectedItems: new Set([1]), + getRowId: (item, index) => index, + }), 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 index 6f90817f93c36..b980fb847eddf 100644 --- a/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx @@ -10,7 +10,7 @@ import { } 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 { useTable, ColumnDefinition, RowId, useSelection } from '../../hooks'; import { useNavigationMode } from '../../navigationModes/useNavigationMode'; type FileCell = { @@ -99,9 +99,14 @@ export const SingleSelectControlled = () => { const { rows } = useTable({ columns, items, - selectionMode: 'single', - selectedRows, - onSelectionChange: setSelectedRows, + selection: useSelection({ + items, + selectionMode: 'single', + defaultSelectedItems: new Set([1]), + getRowId: (item, index) => index, + selectedItems: selectedRows, + onSelectionChange: setSelectedRows, + }), rowEnhancer: (row, { selection }) => ({ ...row, selected: selection.isRowSelected(row.rowId), 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 61134b2301487..e3fb3e7846d9e 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 @@ -10,7 +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 { useTable, ColumnDefinition, ColumnId, useSort } from '../../hooks'; type FileCell = { label: string; @@ -109,7 +109,11 @@ export const Sort = () => { const { rows, sort: { getSortDirection, toggleColumnSort }, - } = useTable({ columns, items, defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); + } = useTable({ + columns, + items, + sort: useSort({ columns, defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }), + }); const headerSortProps = (columnId: ColumnId) => ({ onClick: () => { 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 a2eb8791b7b86..726db8df4be1d 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,7 +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, SortState } from '../../hooks'; +import { useTable, ColumnDefinition, ColumnId, SortState, useSort } from '../../hooks'; type FileCell = { label: string; @@ -114,7 +114,7 @@ export const SortControlled = () => { const { rows, sort: { getSortDirection, toggleColumnSort }, - } = useTable({ columns, items, sortState, onSortChange: setSortState }); + } = useTable({ columns, items, sort: useSort({ columns, sortState, onSortChange: setSortState }) }); const headerSortProps = (columnId: ColumnId) => ({ onClick: () => toggleColumnSort(columnId), From d6559dea1fbd6a44fc4ef6f48d5a20cd9470250c Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 8 Sep 2022 19:17:08 +0200 Subject: [PATCH 02/15] chaning state and new row enhancer --- .../react-table/src/hooks/types.ts | 19 ++-- .../react-table/src/hooks/useSelection.ts | 41 +++++---- .../react-table/src/hooks/useSort.ts | 62 ++++++++----- .../react-table/src/hooks/useTable.ts | 88 ++++--------------- .../stories/Table/MultipleSelect.stories.tsx | 42 ++++----- .../MultipleSelectControlled.stories.tsx | 47 +++++----- .../stories/Table/SingleSelect.stories.tsx | 40 +++++---- .../Table/SingleSelectControlled.stories.tsx | 42 +++++---- .../src/stories/Table/Sort.stories.tsx | 15 ++-- .../stories/Table/SortControlled.stories.tsx | 6 +- 10 files changed, 200 insertions(+), 202 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index e1072c052192e..6ad0b76030ad2 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -19,7 +19,6 @@ export interface ColumnDefinition { export type RowEnhancer = RowState> = ( row: RowState, - state: { selection: TableSelectionState; sort: TableSortState }, ) => TRowState; export interface TableSortStateInternal { @@ -34,13 +33,12 @@ export interface TableSortStateInternal { sort: (items: TItem[]) => TItem[]; } -export interface UseTableOptions = RowState> { +export interface UseTableOptions { selection?: TableSelectionStateInternal; sort?: TableSortStateInternal; columns: ColumnDefinition[]; items: TItem[]; getRowId?: (item: TItem) => RowId; - rowEnhancer?: RowEnhancer; } export interface TableSelectionStateInternal { @@ -130,11 +128,22 @@ export interface RowState { rowId: RowId; } -export interface TableState = RowState> { +export interface TableState { + getRowId?: (item: TItem) => RowId; + /** + * Original user items + */ + items: TItem[]; /** * The row data for rendering */ - rows: TRowState[]; + rows: = RowState>( + rowEnhancer?: RowEnhancer, + ) => TRowState[]; + /** + * Table columns + */ + columns: ColumnDefinition[]; /** * State and actions to manage row selection */ diff --git a/packages/react-components/react-table/src/hooks/useSelection.ts b/packages/react-components/react-table/src/hooks/useSelection.ts index 1f1a6e31619d1..4a256a4d8fa36 100644 --- a/packages/react-components/react-table/src/hooks/useSelection.ts +++ b/packages/react-components/react-table/src/hooks/useSelection.ts @@ -1,17 +1,9 @@ import * as React from 'react'; import { useControllableState, useEventCallback } from '@fluentui/react-utilities'; import { createSelectionManager } from './selectionManager'; -import type { - GetRowIdInternal, - OnSelectionChangeCallback, - RowId, - SelectionMode, - TableSelectionStateInternal, -} from './types'; +import type { OnSelectionChangeCallback, RowId, SelectionMode, TableSelectionStateInternal, TableState } from './types'; interface UseSelectionOptions { - items: TItem[]; - getRowId: GetRowIdInternal; /** * Can be multi or single select */ @@ -30,8 +22,12 @@ interface UseSelectionOptions { onSelectionChange?: OnSelectionChangeCallback; } -export function useSelection(options: UseSelectionOptions): TableSelectionStateInternal { - const { selectionMode, items, getRowId, defaultSelectedItems, selectedItems, onSelectionChange } = options; +export function useSelection( + tableState: TableState, + options: UseSelectionOptions, +): TableState { + const { items, getRowId } = tableState; + const { selectionMode, defaultSelectedItems, selectedItems, onSelectionChange } = options; const [selected, setSelected] = useControllableState({ initialState: new Set(), @@ -50,7 +46,7 @@ export function useSelection(options: UseSelectionOptions): TableS const toggleAllRows: TableSelectionStateInternal['toggleAllRows'] = useEventCallback(() => { selectionManager.toggleAllItems( - items.map((item, i) => getRowId(item, i)), + items.map((item, i) => getRowId?.(item) ?? i), selected, ); }); @@ -71,14 +67,17 @@ export function useSelection(options: UseSelectionOptions): TableS selectionManager.isSelected(rowId, selected); return { - someRowsSelected: selected.size > 0, - allRowsSelected: selectionMode === 'single' ? selected.size > 0 : selected.size === items.length, - selectedRows: selected, - toggleRow, - toggleAllRows, - clearRows: selectionManager.clearItems, - deselectRow, - selectRow, - isRowSelected, + ...tableState, + selection: { + someRowsSelected: selected.size > 0, + allRowsSelected: selectionMode === 'single' ? selected.size > 0 : selected.size === items.length, + selectedRows: Array.from(selected), + toggleRow, + toggleAllRows, + clearRows: selectionManager.clearItems, + deselectRow, + selectRow, + isRowSelected, + }, }; } diff --git a/packages/react-components/react-table/src/hooks/useSort.ts b/packages/react-components/react-table/src/hooks/useSort.ts index 7c983712aae02..5deb1670d0a37 100644 --- a/packages/react-components/react-table/src/hooks/useSort.ts +++ b/packages/react-components/react-table/src/hooks/useSort.ts @@ -1,8 +1,16 @@ +import * as React from 'react'; import { useControllableState } from '@fluentui/react-utilities'; -import type { ColumnDefinition, ColumnId, OnSortChangeCallback, SortState, TableSortStateInternal } from './types'; +import type { + ColumnId, + OnSortChangeCallback, + RowEnhancer, + RowState, + SortState, + TableSortStateInternal, + TableState, +} from './types'; -interface UseSortOptions { - columns: ColumnDefinition[]; +interface UseSortOptions { /** * Used to control sorting */ @@ -17,8 +25,10 @@ interface UseSortOptions { onSortChange?: OnSortChangeCallback; } -export function useSort(options: UseSortOptions): TableSortStateInternal { - const { columns, sortState, defaultSortState, onSortChange } = options; +const defaultRowEnhancer: RowEnhancer> = row => row; +export function useSort(tableState: TableState, options: UseSortOptions): TableState { + const { columns, items, getRowId } = tableState; + const { sortState, defaultSortState, onSortChange } = options; const [sorted, setSorted] = useControllableState({ initialState: { @@ -51,27 +61,39 @@ export function useSort(options: UseSortOptions): TableSortStateIn setSorted(newState); }; - const sort = (items: TItem[]) => - items.slice().sort((a, b) => { - const sortColumnDef = columns.find(column => column.columnId === sortColumn); - if (!sortColumnDef?.compare) { - return 0; - } + const sort = React.useCallback( + (itemsA: TItem[]) => + itemsA.slice().sort((a, b) => { + const sortColumnDef = columns.find(column => column.columnId === sortColumn); + if (!sortColumnDef?.compare) { + return 0; + } - const mod = sortDirection === 'ascending' ? 1 : -1; - return sortColumnDef.compare(a, b) * mod; - }); + const mod = sortDirection === 'ascending' ? 1 : -1; + return sortColumnDef.compare(a, b) * mod; + }), + [sortDirection, sortColumn, columns], + ); const getSortDirection: TableSortStateInternal['getSortDirection'] = (columnId: ColumnId) => { return sortColumn === columnId ? sortDirection : undefined; }; + const rows = >( + rowEnhancer = defaultRowEnhancer as RowEnhancer, + ) => { + return sort(items).map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); + }; + return { - sortColumn, - sortDirection, - sort, - setColumnSort, - toggleColumnSort, - getSortDirection, + ...tableState, + rows, + sort: { + sortColumn, + sortDirection, + setColumnSort, + toggleColumnSort, + getSortDirection, + }, }; } diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index 3d614744262fe..c6133ca50d490 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -1,16 +1,14 @@ -import * as React from 'react'; import type { UseTableOptions, TableState, RowState, - TableSelectionState, - TableSortState, - GetRowIdInternal, TableSelectionStateInternal, TableSortStateInternal, + RowEnhancer, } from './types'; const noop: () => void = () => undefined; +const defaultRowEnhancer: RowEnhancer> = row => row; const defaultSelectionState: TableSelectionStateInternal = { allRowsSelected: false, clearRows: noop, @@ -32,33 +30,8 @@ const defaultSortState: TableSortStateInternal = { toggleColumnSort: noop, }; -export function useTable = RowState>( - options: UseTableOptions, -): TableState { - const { - selection = defaultSelectionState, - sort: baseSort = defaultSortState as TableSortStateInternal, - items: baseItems, - getRowId: getUserRowId = () => undefined, - rowEnhancer = (row: RowState) => row as TRowState, - } = options; - - const getRowId: GetRowIdInternal = React.useCallback( - (item: TItem, index: number) => getUserRowId(item) ?? index, - [getUserRowId], - ); - - const { sortColumn, sortDirection, toggleColumnSort, setColumnSort, getSortDirection, sort } = baseSort; - const sortState: TableSortState = React.useMemo( - () => ({ - sortColumn, - sortDirection, - setColumnSort, - toggleColumnSort, - getSortDirection, - }), - [sortColumn, sortDirection, setColumnSort, toggleColumnSort, getSortDirection], - ); +export function useTable(options: UseTableOptions): TableState { + const { items, getRowId, columns } = options; const { isRowSelected, @@ -70,50 +43,27 @@ export function useTable = RowState ({ + const rows = >(rowEnhancer = defaultRowEnhancer as RowEnhancer) => + items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); + + return { + getRowId, + items, + columns, + rows, + selection: { isRowSelected, - clearRows, - deselectRow, - selectRow, - toggleAllRows, toggleRow, + toggleAllRows, + clearRows, selectedRows: Array.from(selectedRows), allRowsSelected, someRowsSelected, - }), - [ - isRowSelected, - clearRows, - deselectRow, selectRow, - toggleAllRows, - toggleRow, - selectedRows, - allRowsSelected, - someRowsSelected, - ], - ); - - const rows = React.useMemo( - () => - sort(baseItems).map((item, i) => { - return rowEnhancer( - { - item, - rowId: getRowId(item, i), - }, - { selection: selectionState, sort: sortState }, - ); - }), - [baseItems, getRowId, sort, rowEnhancer, selectionState, sortState], - ); - - return { - rows, - selection: selectionState, - sort: sortState, + deselectRow, + }, + sort: defaultSortState, }; } 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 e84d4a5077261..bb27d297f2b4a 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 @@ -95,30 +95,32 @@ const columns: ColumnDefinition[] = [ ]; export const MultipleSelect = () => { - const { - rows, - selection: { allRowsSelected, someRowsSelected, toggleAllRows }, - } = useTable({ - selection: useSelection({ - items, - selectionMode: 'multiselect', - defaultSelectedItems: new Set([0, 1]), - getRowId: (item, index) => index, - }), + let tableState = useTable({ columns, items, - 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), - }), }); + tableState = useSelection(tableState, { + selectionMode: 'multiselect', + defaultSelectedItems: new Set([0, 1]), + }); + + const { + rows: baseRows, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + } = tableState; + + const rows = baseRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); + // eslint-disable-next-line deprecation/deprecation const ref = useNavigationMode('row'); 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 index 997bbec7b668f..3e3942c0b8833 100644 --- a/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx @@ -95,33 +95,34 @@ const columns: ColumnDefinition[] = [ ]; export const MultipleSelectControlled = () => { - const [selectedRows, setSelectedRows] = React.useState(new Set()); - const { - rows, - selection: { allRowsSelected, someRowsSelected, toggleAllRows }, - } = useTable({ + const [selectedRows, setSelectedRows] = React.useState(new Set(new Set([0, 1]))); + let tableState = useTable({ columns, items, - selection: useSelection({ - items, - selectionMode: 'multiselect', - defaultSelectedItems: new Set([0, 1]), - getRowId: (item, index) => index, - selectedItems: 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), - }), }); + tableState = useSelection(tableState, { + selectionMode: 'multiselect', + selectedItems: selectedRows, + onSelectionChange: setSelectedRows, + }); + + const { + rows: baseRows, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + } = tableState; + + const rows = baseRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); + // eslint-disable-next-line deprecation/deprecation const ref = useNavigationMode('row'); 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 17286c8038174..ace035dfda7bd 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 @@ -95,26 +95,32 @@ const columns: ColumnDefinition[] = [ ]; export const SingleSelect = () => { - const { rows } = useTable({ - selection: useSelection({ - items, - selectionMode: 'single', - defaultSelectedItems: new Set([1]), - getRowId: (item, index) => index, - }), + let tableState = useTable({ columns, items, - 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); - } - }, - }), }); + + tableState = useSelection(tableState, { + selectionMode: 'single', + defaultSelectedItems: new Set([1]), + }); + + const { + rows: baseRows, + selection: { toggleRow, isRowSelected }, + } = tableState; + + const rows = baseRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); + // eslint-disable-next-line deprecation/deprecation const ref = useNavigationMode('row'); 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 index b980fb847eddf..a739cfa210905 100644 --- a/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx @@ -96,28 +96,32 @@ const columns: ColumnDefinition[] = [ export const SingleSelectControlled = () => { const [selectedRows, setSelectedRows] = React.useState(new Set()); - const { rows } = useTable({ + let tableState = useTable({ columns, items, - selection: useSelection({ - items, - selectionMode: 'single', - defaultSelectedItems: new Set([1]), - getRowId: (item, index) => index, - selectedItems: 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); - } - }, - }), }); + + tableState = useSelection(tableState, { + selectionMode: 'single', + selectedItems: selectedRows, + onSelectionChange: setSelectedRows, + }); + + const { + rows: baseRows, + selection: { toggleRow, isRowSelected }, + } = tableState; + + const rows = baseRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); // eslint-disable-next-line deprecation/deprecation const ref = useNavigationMode('row'); 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 e3fb3e7846d9e..8b8a2d6bf1f44 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 @@ -106,15 +106,18 @@ const columns: ColumnDefinition[] = [ ]; export const Sort = () => { - const { - rows, - sort: { getSortDirection, toggleColumnSort }, - } = useTable({ + let tableState = useTable({ columns, items, - sort: useSort({ columns, defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }), }); + tableState = useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); + + const { + rows, + sort: { getSortDirection, toggleColumnSort }, + } = tableState; + const headerSortProps = (columnId: ColumnId) => ({ onClick: () => { toggleColumnSort(columnId); @@ -133,7 +136,7 @@ export const Sort = () => { - {rows.map(({ item }) => ( + {rows().map(({ item }) => ( {item.file.label} }>{item.author.label} 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 726db8df4be1d..523884517932b 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 @@ -111,10 +111,12 @@ export const SortControlled = () => { sortColumn: 'file', }); + let tableState = useTable({ columns, items }); + tableState = useSort(tableState, { sortState, onSortChange: setSortState }); const { rows, sort: { getSortDirection, toggleColumnSort }, - } = useTable({ columns, items, sort: useSort({ columns, sortState, onSortChange: setSortState }) }); + } = tableState; const headerSortProps = (columnId: ColumnId) => ({ onClick: () => toggleColumnSort(columnId), @@ -132,7 +134,7 @@ export const SortControlled = () => { - {rows.map(({ item }) => ( + {rows().map(({ item }) => ( {item.file.label} }>{item.author.label} From 6425145fd64477bb26d3c557503de6e7f9d754f8 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 8 Sep 2022 20:13:59 +0200 Subject: [PATCH 03/15] sorting should happen in userland --- .../react-table/src/hooks/types.ts | 10 ++++--- .../react-table/src/hooks/useSort.ts | 27 +++++-------------- .../react-table/src/hooks/useTable.ts | 13 +++++++-- .../src/stories/Table/Sort.stories.tsx | 4 +-- .../stories/Table/SortControlled.stories.tsx | 4 +-- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 6ad0b76030ad2..2d18d595233c0 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -30,7 +30,7 @@ export interface TableSortStateInternal { /** * Returns a sorted **shallow** copy of original items */ - sort: (items: TItem[]) => TItem[]; + sort: (rows: RowState[]) => RowState[]; } export interface UseTableOptions { @@ -53,7 +53,11 @@ export interface TableSelectionStateInternal { someRowsSelected: boolean; } -export interface TableSortState { +export interface TableSortState { + /** + * @returns sorted rows + */ + sort: (rows: RowState[]) => RowState[]; /** * Current sort direction */ @@ -151,5 +155,5 @@ export interface TableState { /** * State and actions to manage row sorting */ - sort: TableSortState; + sort: TableSortState; } diff --git a/packages/react-components/react-table/src/hooks/useSort.ts b/packages/react-components/react-table/src/hooks/useSort.ts index 5deb1670d0a37..de7a30aa27a72 100644 --- a/packages/react-components/react-table/src/hooks/useSort.ts +++ b/packages/react-components/react-table/src/hooks/useSort.ts @@ -1,14 +1,6 @@ import * as React from 'react'; import { useControllableState } from '@fluentui/react-utilities'; -import type { - ColumnId, - OnSortChangeCallback, - RowEnhancer, - RowState, - SortState, - TableSortStateInternal, - TableState, -} from './types'; +import type { ColumnId, OnSortChangeCallback, RowState, SortState, TableSortStateInternal, TableState } from './types'; interface UseSortOptions { /** @@ -25,9 +17,8 @@ interface UseSortOptions { onSortChange?: OnSortChangeCallback; } -const defaultRowEnhancer: RowEnhancer> = row => row; export function useSort(tableState: TableState, options: UseSortOptions): TableState { - const { columns, items, getRowId } = tableState; + const { columns } = tableState; const { sortState, defaultSortState, onSortChange } = options; const [sorted, setSorted] = useControllableState({ @@ -62,15 +53,15 @@ export function useSort(tableState: TableState, options: UseSortOp }; const sort = React.useCallback( - (itemsA: TItem[]) => - itemsA.slice().sort((a, b) => { + (rows: RowState[]) => + rows.slice().sort((a, b) => { const sortColumnDef = columns.find(column => column.columnId === sortColumn); if (!sortColumnDef?.compare) { return 0; } const mod = sortDirection === 'ascending' ? 1 : -1; - return sortColumnDef.compare(a, b) * mod; + return sortColumnDef.compare(a.item, b.item) * mod; }), [sortDirection, sortColumn, columns], ); @@ -79,16 +70,10 @@ export function useSort(tableState: TableState, options: UseSortOp return sortColumn === columnId ? sortDirection : undefined; }; - const rows = >( - rowEnhancer = defaultRowEnhancer as RowEnhancer, - ) => { - return sort(items).map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); - }; - return { ...tableState, - rows, sort: { + sort, sortColumn, sortDirection, setColumnSort, diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index c6133ca50d490..b427301565126 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -24,7 +24,7 @@ const defaultSelectionState: TableSelectionStateInternal = { const defaultSortState: TableSortStateInternal = { getSortDirection: () => 'ascending', setColumnSort: noop, - sort: (items: unknown[]) => items, + sort: (rows: RowState[]) => [...rows], sortColumn: undefined, sortDirection: 'ascending', toggleColumnSort: noop, @@ -45,6 +45,8 @@ export function useTable(options: UseTableOptions): TableState>(rowEnhancer = defaultRowEnhancer as RowEnhancer) => items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); @@ -64,6 +66,13 @@ export function useTable(options: UseTableOptions): TableState[]) => RowState[], + getSortDirection, + setColumnSort, + sortColumn, + sortDirection, + toggleColumnSort, + }, }; } 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 8b8a2d6bf1f44..d2a4af782a587 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 @@ -115,7 +115,7 @@ export const Sort = () => { const { rows, - sort: { getSortDirection, toggleColumnSort }, + sort: { getSortDirection, toggleColumnSort, sort }, } = tableState; const headerSortProps = (columnId: ColumnId) => ({ @@ -136,7 +136,7 @@ export const Sort = () => { - {rows().map(({ item }) => ( + {sort(rows()).map(({ item }) => ( {item.file.label} }>{item.author.label} 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 523884517932b..ac51d83c5cb62 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 @@ -115,7 +115,7 @@ export const SortControlled = () => { tableState = useSort(tableState, { sortState, onSortChange: setSortState }); const { rows, - sort: { getSortDirection, toggleColumnSort }, + sort: { getSortDirection, toggleColumnSort, sort }, } = tableState; const headerSortProps = (columnId: ColumnId) => ({ @@ -134,7 +134,7 @@ export const SortControlled = () => { - {rows().map(({ item }) => ( + {sort(rows()).map(({ item }) => ( {item.file.label} }>{item.author.label} From c2d10bcda93a8af0bee723aa36e911c0e79f5a03 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 9 Sep 2022 19:22:40 +0200 Subject: [PATCH 04/15] usePagination --- .../react-table/src/hooks/index.ts | 1 + .../react-table/src/hooks/types.ts | 11 ++- .../react-table/src/hooks/usePagination.ts | 45 +++++++++ .../react-table/src/hooks/useTable.ts | 10 +- .../stories/Table/MultipleSelect.stories.tsx | 4 +- .../MultipleSelectControlled.stories.tsx | 4 +- .../src/stories/Table/Pagination.stories.tsx | 96 +++++++++++++++++++ .../stories/Table/SingleSelect.stories.tsx | 4 +- .../Table/SingleSelectControlled.stories.tsx | 4 +- .../src/stories/Table/Sort.stories.tsx | 4 +- .../src/stories/Table/index.stories.tsx | 1 + 11 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 packages/react-components/react-table/src/hooks/usePagination.ts create mode 100644 packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx diff --git a/packages/react-components/react-table/src/hooks/index.ts b/packages/react-components/react-table/src/hooks/index.ts index 1f47e3e6249ca..c815c8ebc634a 100644 --- a/packages/react-components/react-table/src/hooks/index.ts +++ b/packages/react-components/react-table/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './types'; export * from './useTable'; export * from './useSelection'; export * from './useSort'; +export * from './usePagination'; diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 2d18d595233c0..91f37d5bc52fc 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -132,6 +132,14 @@ export interface RowState { rowId: RowId; } +export interface TablePaginationState { + getPageRows: (rows: RowState[]) => RowState[]; + nextPage: () => void; + prevPage: () => void; + setPage: (page: number) => void; + currentPage: number; + pageCount: number; +} export interface TableState { getRowId?: (item: TItem) => RowId; /** @@ -141,7 +149,7 @@ export interface TableState { /** * The row data for rendering */ - rows: = RowState>( + getRows: = RowState>( rowEnhancer?: RowEnhancer, ) => TRowState[]; /** @@ -156,4 +164,5 @@ export interface TableState { * State and actions to manage row sorting */ sort: TableSortState; + pagination: TablePaginationState; } diff --git a/packages/react-components/react-table/src/hooks/usePagination.ts b/packages/react-components/react-table/src/hooks/usePagination.ts new file mode 100644 index 0000000000000..dea57d0b7673a --- /dev/null +++ b/packages/react-components/react-table/src/hooks/usePagination.ts @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { useControllableState } from '@fluentui/react-utilities'; +import { TablePaginationState, TableState } from './types'; + +interface UsePaginationOptions { + pageSize: number; + currentPage?: number; + defaultPage?: number; +} + +export const defaultPaginationState: TablePaginationState = { + currentPage: 0, + getPageRows: () => [], + nextPage: () => null, + pageCount: 0, + prevPage: () => null, + setPage: () => null, +}; + +export function usePagination(tableState: TableState, options: UsePaginationOptions): TableState { + const { items } = tableState; + const { currentPage, defaultPage, pageSize } = options; + const pageCount = Math.ceil(items.length / pageSize); + const [page, setPage] = useControllableState({ + initialState: 0, + defaultState: defaultPage, + state: currentPage, + }); + + React.useEffect(() => { + setPage(0); + }, [pageSize, setPage]); + + return { + ...tableState, + pagination: { + currentPage: page, + pageCount, + setPage: (newPage: number) => setPage(newPage), + nextPage: () => setPage(p => Math.min(p + 1, pageCount)), + prevPage: () => setPage(p => Math.max(p - 1, 0)), + getPageRows: rows => rows.slice(page * pageSize, (page + 1) * pageSize), + }, + }; +} diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index b427301565126..98d0c46e72559 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -5,7 +5,9 @@ import type { TableSelectionStateInternal, TableSortStateInternal, RowEnhancer, + TablePaginationState, } from './types'; +import { defaultPaginationState } from './usePagination'; const noop: () => void = () => undefined; const defaultRowEnhancer: RowEnhancer> = row => row; @@ -47,14 +49,16 @@ export function useTable(options: UseTableOptions): TableState>(rowEnhancer = defaultRowEnhancer as RowEnhancer) => - items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); + const getRows = >( + rowEnhancer = defaultRowEnhancer as RowEnhancer, + ) => items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); return { getRowId, items, columns, - rows, + getRows, + pagination: defaultPaginationState as TablePaginationState, selection: { isRowSelected, toggleRow, 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 bb27d297f2b4a..59462b5663b0b 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 @@ -106,11 +106,11 @@ export const MultipleSelect = () => { }); const { - rows: baseRows, + getRows, selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, } = tableState; - const rows = baseRows(row => ({ + const rows = getRows(row => ({ ...row, onClick: () => toggleRow(row.rowId), onKeyDown: (e: React.KeyboardEvent) => { 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 index 3e3942c0b8833..be25be69ae3a6 100644 --- a/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx @@ -108,11 +108,11 @@ export const MultipleSelectControlled = () => { }); const { - rows: baseRows, + getRows, selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, } = tableState; - const rows = baseRows(row => ({ + const rows = getRows(row => ({ ...row, onClick: () => toggleRow(row.rowId), onKeyDown: (e: React.KeyboardEvent) => { diff --git a/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx b/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx new file mode 100644 index 0000000000000..51e7df529ca48 --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell } from '../..'; +import { useTable, ColumnDefinition, ColumnId, useSort, usePagination } from '../../hooks'; + +type Item = { + first: number; + second: number; + third: number; + fourth: number; +}; + +const columns: ColumnDefinition[] = [ + { + columnId: 'first', + compare: (a, b) => { + return a.first - b.first; + }, + }, + { + columnId: 'second', + compare: (a, b) => { + return a.second - b.second; + }, + }, + { + columnId: 'third', + compare: (a, b) => { + return a.third - b.third; + }, + }, + { + columnId: 'fourth', + compare: (a, b) => { + return a.fourth - b.fourth; + }, + }, +]; + +const items: Item[] = new Array(1000).fill(0).map((_, i) => ({ first: i, second: i, third: i, fourth: i })); + +export const Pagination = () => { + let tableState = useTable({ + columns, + items, + }); + + tableState = useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); + tableState = usePagination(tableState, { pageSize: 10 }); + + const { + getRows, + sort: { getSortDirection, toggleColumnSort, sort }, + pagination: { getPageRows, nextPage, prevPage, pageCount, currentPage }, + } = tableState; + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: () => { + toggleColumnSort(columnId); + }, + sortDirection: getSortDirection(columnId), + }); + + const sortedRows = React.useMemo(() => { + return sort(getRows()); + }, [sort, getRows]); + + return ( + <> +
+ Page {currentPage + 1} of {pageCount} +
+ + + + First + Second + Third + Fourth + + + + {getPageRows(sortedRows).map(({ item }, i) => ( + + {item.first} + {item.second} + {item.third} + {item.fourth} + + ))} + +
+ + + + ); +}; 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 ace035dfda7bd..2c1d5346f6077 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 @@ -106,11 +106,11 @@ export const SingleSelect = () => { }); const { - rows: baseRows, + getRows, selection: { toggleRow, isRowSelected }, } = tableState; - const rows = baseRows(row => ({ + const rows = getRows(row => ({ ...row, onClick: () => toggleRow(row.rowId), onKeyDown: (e: React.KeyboardEvent) => { 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 index a739cfa210905..9ac2043e3543f 100644 --- a/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx @@ -108,11 +108,11 @@ export const SingleSelectControlled = () => { }); const { - rows: baseRows, + getRows, selection: { toggleRow, isRowSelected }, } = tableState; - const rows = baseRows(row => ({ + const rows = getRows(row => ({ ...row, onClick: () => toggleRow(row.rowId), onKeyDown: (e: React.KeyboardEvent) => { 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 d2a4af782a587..03dbcbb320c37 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 @@ -114,7 +114,7 @@ export const Sort = () => { tableState = useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); const { - rows, + getRows, sort: { getSortDirection, toggleColumnSort, sort }, } = tableState; @@ -136,7 +136,7 @@ export const Sort = () => {
- {sort(rows()).map(({ item }) => ( + {sort(getRows()).map(({ item }) => ( {item.file.label} }>{item.author.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 5ca28f8f253ec..7f66eba4ae68f 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 @@ -10,6 +10,7 @@ export { SizeSmaller } from './SizeSmaller.stories'; export { NonNativeElements } from './NonNativeElements.stories'; export { MultipleSelect } from './MultipleSelect.stories'; export { SingleSelect } from './SingleSelect.stories'; +export { Pagination } from './Pagination.stories'; export { MultipleSelectControlled } from './MultipleSelectControlled.stories'; export { SingleSelectControlled } from './SingleSelectControlled.stories'; export { CellNavigationMode } from './CellNavigationMode.stories'; From 487c23e84a4710b55976d4a19ce343af29272674 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 21 Sep 2022 17:30:52 +0200 Subject: [PATCH 05/15] implement column sizing --- .../react-table/src/hooks/ColumnResize.tsx | 174 ++++++++++++++++++ .../react-table/src/hooks/index.ts | 1 + .../react-table/src/hooks/types.ts | 31 ++++ .../react-table/src/hooks/useColumnSizing.ts | 38 ++++ .../src/hooks/useResizeObserver.ts | 43 +++++ .../react-table/src/hooks/useTable.ts | 13 ++ .../stories/Table/ColumnResize.stories.tsx | 123 +++++++++++++ .../src/stories/Table/index.stories.tsx | 1 + 8 files changed, 424 insertions(+) create mode 100644 packages/react-components/react-table/src/hooks/ColumnResize.tsx create mode 100644 packages/react-components/react-table/src/hooks/useColumnSizing.ts create mode 100644 packages/react-components/react-table/src/hooks/useResizeObserver.ts create mode 100644 packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx diff --git a/packages/react-components/react-table/src/hooks/ColumnResize.tsx b/packages/react-components/react-table/src/hooks/ColumnResize.tsx new file mode 100644 index 0000000000000..6beb14877e98b --- /dev/null +++ b/packages/react-components/react-table/src/hooks/ColumnResize.tsx @@ -0,0 +1,174 @@ +import * as React from 'react'; +import { ColumnId, ColumnWidthState } from './types'; + +const DEFAULT_WIDTH = 150; +const DEFAULT_MAX_WIDTH = 300; +const DEFAULT_MIN_WIDTH = DEFAULT_WIDTH; + +export interface ColumnWidthOptions + extends Partial>, + Pick {} + +export class ColumnResize { + public columns: ColumnWidthState[]; + private mouseX: number = 0; + private onColumnWidthsUpdate: () => void; + private container: HTMLElement; + private resizing: boolean = false; + private resizeObserver: ResizeObserver; + + constructor(columns: ColumnWidthOptions[], onColumnWidthsUpdate: () => void) { + this.columns = columns.map(column => { + const { + columnId, + width = DEFAULT_WIDTH, + maxWidth = DEFAULT_MAX_WIDTH, + minWidth = DEFAULT_MIN_WIDTH, + padding = 16, + } = column; + return { + columnId, + width: DEFAULT_WIDTH, + maxWidth, + minWidth, + idealWidth: width, + padding, + }; + }); + + this.onColumnWidthsUpdate = onColumnWidthsUpdate; + this.container = document.body; + this.resizeObserver = new ResizeObserver(this._handleResize); + } + + public init(table: HTMLElement) { + this.container = document.createElement('div'); + table.insertAdjacentElement('beforebegin', this.container); + this.resetLayout(); + this.resizeObserver.observe(this.container); + } + + public getColumnWidth(columnId: ColumnId) { + return this._getColumn(columnId).width; + } + + public get totalWidth() { + return this.columns.reduce((sum, column) => sum + column.width + column.padding, 0); + } + + public setColumnWidth(columnId: ColumnId, newWidth: number) { + const state = this._getColumn(columnId); + if (newWidth >= state.minWidth && newWidth <= state.maxWidth) { + const dx = state.width - newWidth; + state.width = newWidth; + + this.columns[this.columns.length - 1].width += dx; + } + + this._updateTableStyles(); + this.onColumnWidthsUpdate(); + } + + public getOnMouseDown(columnId: ColumnId) { + return (mouseDownEvent: React.MouseEvent) => { + if (mouseDownEvent.target !== mouseDownEvent.currentTarget) { + return; + } + + this.resizing = true; + + this.mouseX = mouseDownEvent.clientX; + const onMouseUp = () => { + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('mousemove', onMouseMove); + this.resizing = false; + }; + + const onMouseMove = (e: MouseEvent) => { + const dx = e.clientX - this.mouseX; + this.mouseX = e.clientX; + const currentWidth = this.getColumnWidth(columnId); + this.setColumnWidth(columnId, currentWidth + dx); + }; + + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('mousemove', onMouseMove); + }; + } + + public resetLayout() { + let { width: availableWidth } = this.container.getBoundingClientRect(); + + // first pass: columns to min width + let i = 0; + while (i < this.columns.length && availableWidth > this.columns[i].minWidth + this.columns[i].padding) { + const column = this.columns[i]; + availableWidth -= column.minWidth + column.padding; + column.width = column.minWidth; + i++; + } + + // second pass: columns to set width + i = 0; + while (i < this.columns.length && availableWidth > this.columns[i].idealWidth) { + const column = this.columns[i]; + availableWidth -= column.idealWidth - column.width; + column.width = column.idealWidth; + i++; + } + + // Last cell gets all the rest + if (availableWidth) { + this.columns[this.columns.length - 1].width += availableWidth; + } + + this._updateTableStyles(); + this.onColumnWidthsUpdate(); + } + + private _getColumn(columnId: ColumnId) { + const state = this.columns.find(column => column.columnId === columnId); + if (!state) { + throw new Error(`Column ${columnId} does not exist`); + } + + return state; + } + + private _handleResize = () => { + if (this.resizing) { + return; + } + + const { width: availableWidth } = this.container.getBoundingClientRect(); + + let totalWidth = this.totalWidth; + if (availableWidth > totalWidth) { + this.columns[this.columns.length - 1].width += availableWidth - totalWidth; + } else { + let i = this.columns.length - 1; + while (i >= 0 && totalWidth > availableWidth) { + const column = this.columns[i]; + console.log(column.width, column.minWidth); + + if (column.width > column.minWidth) { + const diffAvailableWidth = totalWidth - availableWidth; + const adjust = Math.min(column.width - column.minWidth, diffAvailableWidth); + column.width -= adjust; + totalWidth -= adjust; + } + i--; + } + } + + this._updateTableStyles(); + this.onColumnWidthsUpdate(); + }; + + private _updateTableStyles() { + Object.assign(this.container.style, { + tableLayout: 'fixed', + width: `${this.totalWidth}px`, + }); + } +} diff --git a/packages/react-components/react-table/src/hooks/index.ts b/packages/react-components/react-table/src/hooks/index.ts index c815c8ebc634a..fe760ce51ad64 100644 --- a/packages/react-components/react-table/src/hooks/index.ts +++ b/packages/react-components/react-table/src/hooks/index.ts @@ -3,3 +3,4 @@ export * from './useTable'; export * from './useSelection'; export * from './useSort'; export * from './usePagination'; +export * from './useColumnSizing'; diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 91f37d5bc52fc..c19842e26496f 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { SortDirection } from '../components/Table/Table.types'; export type RowId = string | number; @@ -140,6 +141,24 @@ export interface TablePaginationState { currentPage: number; pageCount: number; } + +export interface ColumnWidthState { + columnId: ColumnId; + width: number; + minWidth: number; + maxWidth: number; + idealWidth: number; + padding: number; +} + +export interface TableColumnSizingState { + getOnMouseDown: (columnId: ColumnId) => (e: React.MouseEvent) => void; + getColumnWidth: (columnId: ColumnId) => number; + getTotalWidth: () => number; + setColumnWidth: (columnId: ColumnId, newSize: number) => void; + getColumnWidths: () => ColumnWidthState[]; +} + export interface TableState { getRowId?: (item: TItem) => RowId; /** @@ -152,6 +171,10 @@ export interface TableState { getRows: = RowState>( rowEnhancer?: RowEnhancer, ) => TRowState[]; + /** + * Ref to the table HTML element + */ + tableRef: React.RefObject; /** * Table columns */ @@ -164,5 +187,13 @@ export interface TableState { * State and actions to manage row sorting */ sort: TableSortState; + /** + * Pagination state + */ pagination: TablePaginationState; + + /** + * Column sizing state + */ + columnSizing: TableColumnSizingState; } diff --git a/packages/react-components/react-table/src/hooks/useColumnSizing.ts b/packages/react-components/react-table/src/hooks/useColumnSizing.ts new file mode 100644 index 0000000000000..f6c75f462a359 --- /dev/null +++ b/packages/react-components/react-table/src/hooks/useColumnSizing.ts @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { ColumnResize } from './ColumnResize'; +import { ColumnId, TableState } from './types'; + +// why are there 2 layout components for cells ? -> try to consolidate to one +// column collapse priority -> verify DetailsList +// window resizing +// click + drag resize +// change requirements without users changing code + +export function useColumnSizing(tableState: TableState): TableState { + const { columns, tableRef } = tableState; + const forceUpdate = React.useReducer(() => ({}), {})[1]; + const manager = React.useState( + () => + new ColumnResize( + columns.map(({ columnId }) => ({ columnId })), + forceUpdate, + ), + )[0]; + + React.useEffect(() => { + if (tableRef.current) { + manager.init(tableRef.current); + } + }, [manager]); + + return { + ...tableState, + columnSizing: { + getOnMouseDown: (columnId: ColumnId) => manager.getOnMouseDown(columnId), + getColumnWidth: (columnId: ColumnId) => manager.getColumnWidth(columnId), + getTotalWidth: () => manager.totalWidth, + setColumnWidth: (columnId: ColumnId, newSize: number) => manager.setColumnWidth(columnId, newSize), + getColumnWidths: () => manager.columns, + }, + }; +} diff --git a/packages/react-components/react-table/src/hooks/useResizeObserver.ts b/packages/react-components/react-table/src/hooks/useResizeObserver.ts new file mode 100644 index 0000000000000..f11a16e5d5cc6 --- /dev/null +++ b/packages/react-components/react-table/src/hooks/useResizeObserver.ts @@ -0,0 +1,43 @@ +function hasResizeObserver() { + return typeof window.ResizeObserver !== 'undefined'; +} + +import * as React from 'react'; + +type UseResizeObserverOptionsType = { + ref: React.RefObject | undefined; + onResize: () => void; +}; + +export function useResizeObserver(options: UseResizeObserverOptionsType) { + const { ref, onResize } = options; + + React.useEffect(() => { + const element = ref?.current; + if (!element) { + return; + } + + if (!hasResizeObserver()) { + window.addEventListener('resize', onResize, false); + return () => { + window.removeEventListener('resize', onResize, false); + }; + } else { + const resizeObserverInstance = new window.ResizeObserver(entries => { + if (!entries.length) { + return; + } + + onResize(); + }); + resizeObserverInstance.observe(element); + + return () => { + if (element) { + resizeObserverInstance.unobserve(element); + } + }; + } + }, [onResize, ref]); +} diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index 98d0c46e72559..bebd7f68c3b7b 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import type { UseTableOptions, TableState, @@ -6,6 +7,7 @@ import type { TableSortStateInternal, RowEnhancer, TablePaginationState, + TableColumnSizingState, } from './types'; import { defaultPaginationState } from './usePagination'; @@ -32,8 +34,17 @@ const defaultSortState: TableSortStateInternal = { toggleColumnSort: noop, }; +const defaultColumnSizingState: TableColumnSizingState = { + getColumnWidth: () => 0, + getColumnWidths: () => [], + getOnMouseDown: () => () => null, + getTotalWidth: () => 0, + setColumnWidth: () => null, +}; + export function useTable(options: UseTableOptions): TableState { const { items, getRowId, columns } = options; + const tableRef = React.useRef(null); const { isRowSelected, @@ -78,5 +89,7 @@ export function useTable(options: UseTableOptions): TableState[] = [ + { + columnId: 'first', + compare: (a, b) => { + return a.first - b.first; + }, + }, + { + columnId: 'second', + compare: (a, b) => { + return a.second - b.second; + }, + }, + { + columnId: 'third', + compare: (a, b) => { + return a.third - b.third; + }, + }, + { + columnId: 'fourth', + compare: (a, b) => { + return a.fourth - b.fourth; + }, + }, +]; + +const items: Item[] = new Array(10).fill(0).map((_, i) => ({ first: i, second: i, third: i, fourth: i })); + +export const ColumnResize = () => { + let tableState = useTable({ + columns, + items, + }); + + tableState = useColumnSizing(tableState); + + const { + getRows, + columnSizing: { getColumnWidth, setColumnWidth, getOnMouseDown }, + tableRef, + } = tableState; + + return ( + <> +
+ {columns.map(column => { + return ( + <> + + setColumnWidth(column.columnId, Number(e.target.value))} + id={column.columnId as string} + type="number" + value={getColumnWidth(column.columnId)} + /> + + ); + })} +
+ + + + + First + + + + Second + + + + Third + + + + Fourth + + + + + + {getRows().map(({ item }, i) => ( + + {item.first} + {item.second} + {item.third} + {item.fourth} + + ))} + +
+ + ); +}; + +const Resizer: React.FC> = props => { + return ( +
+ ); +}; 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 e90e720f7a22b..9087635df0ccf 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 @@ -17,6 +17,7 @@ export { SingleSelectControlled } from './SingleSelectControlled.stories'; export { CellNavigationMode } from './CellNavigationMode.stories'; export { RowNavigationMode } from './RowNavigationMode.stories'; export { CompositeNavigationMode } from './CompositeNavigationMode.stories'; +export { ColumnResize } from './ColumnResize.stories'; export default { title: 'Preview Components/Table', From 9964d187b0aab937ea11b73394106f90850f7cf1 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Wed, 21 Sep 2022 18:02:39 +0200 Subject: [PATCH 06/15] improved composition --- .../react-table/src/hooks/types.ts | 2 ++ .../react-table/src/hooks/useTable.ts | 7 +++-- .../stories/Table/ColumnResize.stories.tsx | 15 +++++------ .../stories/Table/MultipleSelect.stories.tsx | 23 ++++++++-------- .../MultipleSelectControlled.stories.tsx | 26 ++++++++++--------- .../src/stories/Table/Pagination.stories.tsx | 19 +++++++------- .../stories/Table/SingleSelect.stories.tsx | 24 +++++++++-------- .../Table/SingleSelectControlled.stories.tsx | 26 ++++++++++--------- .../src/stories/Table/Sort.stories.tsx | 15 +++++------ .../stories/Table/SortControlled.stories.tsx | 4 +-- 10 files changed, 85 insertions(+), 76 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index c19842e26496f..166f1990cb6f5 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -197,3 +197,5 @@ export interface TableState { */ columnSizing: TableColumnSizingState; } + +export type TableStatePlugin = (tableState: TableState) => TableState; diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index bebd7f68c3b7b..83ec2ebf48e19 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -8,6 +8,7 @@ import type { RowEnhancer, TablePaginationState, TableColumnSizingState, + TableStatePlugin, } from './types'; import { defaultPaginationState } from './usePagination'; @@ -42,7 +43,7 @@ const defaultColumnSizingState: TableColumnSizingState = { setColumnWidth: () => null, }; -export function useTable(options: UseTableOptions): TableState { +export function useTable(options: UseTableOptions, plugins: TableStatePlugin[]): TableState { const { items, getRowId, columns } = options; const tableRef = React.useRef(null); @@ -64,7 +65,7 @@ export function useTable(options: UseTableOptions): TableState, ) => items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); - return { + const initialState = { getRowId, items, columns, @@ -92,4 +93,6 @@ export function useTable(options: UseTableOptions): TableState plugin(state), initialState); } diff --git a/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx b/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx index 348bc32760da5..d8cefe09bdea7 100644 --- a/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx @@ -39,18 +39,17 @@ const columns: ColumnDefinition[] = [ const items: Item[] = new Array(10).fill(0).map((_, i) => ({ first: i, second: i, third: i, fourth: i })); export const ColumnResize = () => { - let tableState = useTable({ - columns, - items, - }); - - tableState = useColumnSizing(tableState); - const { getRows, columnSizing: { getColumnWidth, setColumnWidth, getOnMouseDown }, tableRef, - } = tableState; + } = useTable( + { + columns, + items, + }, + [useColumnSizing], + ); return ( <> 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 6ca304228b1d7..628606168b5f5 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 @@ -104,20 +104,21 @@ const columns: ColumnDefinition[] = [ ]; export const MultipleSelect = () => { - let tableState = useTable({ - columns, - items, - }); - - tableState = useSelection(tableState, { - selectionMode: 'multiselect', - defaultSelectedItems: new Set([0, 1]), - }); - const { getRows, selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, - } = tableState; + } = useTable( + { + columns, + items, + }, + [ + tableState => + useSelection(tableState, { + selectionMode: 'multiselect', + }), + ], + ); const rows = getRows(row => ({ ...row, 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 index b9644da479285..a989bf8aa7608 100644 --- a/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/MultipleSelectControlled.stories.tsx @@ -105,21 +105,23 @@ const columns: ColumnDefinition[] = [ export const MultipleSelectControlled = () => { const [selectedRows, setSelectedRows] = React.useState(new Set(new Set([0, 1]))); - let tableState = useTable({ - columns, - items, - }); - - tableState = useSelection(tableState, { - selectionMode: 'multiselect', - selectedItems: selectedRows, - onSelectionChange: setSelectedRows, - }); - const { getRows, selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, - } = tableState; + } = useTable( + { + columns, + items, + }, + [ + tableState => + useSelection(tableState, { + selectedItems: selectedRows, + onSelectionChange: setSelectedRows, + selectionMode: 'multiselect', + }), + ], + ); const rows = getRows(row => ({ ...row, diff --git a/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx b/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx index 51e7df529ca48..db8308855cb51 100644 --- a/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx @@ -39,19 +39,20 @@ const columns: ColumnDefinition[] = [ const items: Item[] = new Array(1000).fill(0).map((_, i) => ({ first: i, second: i, third: i, fourth: i })); export const Pagination = () => { - let tableState = useTable({ - columns, - items, - }); - - tableState = useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); - tableState = usePagination(tableState, { pageSize: 10 }); - const { getRows, sort: { getSortDirection, toggleColumnSort, sort }, pagination: { getPageRows, nextPage, prevPage, pageCount, currentPage }, - } = tableState; + } = useTable( + { + columns, + items, + }, + [ + tableState => useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }), + tableState => usePagination(tableState, { pageSize: 10 }), + ], + ); const headerSortProps = (columnId: ColumnId) => ({ onClick: () => { 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 26b5fe6d49d8b..30f68a2d53f1c 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 @@ -96,20 +96,22 @@ const columns: ColumnDefinition[] = [ ]; export const SingleSelect = () => { - let tableState = useTable({ - columns, - items, - }); - - tableState = useSelection(tableState, { - selectionMode: 'single', - defaultSelectedItems: new Set([1]), - }); - const { getRows, selection: { toggleRow, isRowSelected }, - } = tableState; + } = useTable( + { + columns, + items, + }, + [ + tableState => + useSelection(tableState, { + selectionMode: 'single', + defaultSelectedItems: new Set([1]), + }), + ], + ); const rows = getRows(row => ({ ...row, 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 index 698bc67335b38..64fb3057f0763 100644 --- a/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/SingleSelectControlled.stories.tsx @@ -97,21 +97,23 @@ const columns: ColumnDefinition[] = [ export const SingleSelectControlled = () => { const [selectedRows, setSelectedRows] = React.useState(new Set()); - let tableState = useTable({ - columns, - items, - }); - - tableState = useSelection(tableState, { - selectionMode: 'single', - selectedItems: selectedRows, - onSelectionChange: setSelectedRows, - }); - const { getRows, selection: { toggleRow, isRowSelected }, - } = tableState; + } = useTable( + { + columns, + items, + }, + [ + tableState => + useSelection(tableState, { + selectionMode: 'single', + selectedItems: selectedRows, + onSelectionChange: setSelectedRows, + }), + ], + ); const rows = getRows(row => ({ ...row, 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 75f2074574d3e..7d4a01cca792c 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 @@ -107,17 +107,16 @@ const columns: ColumnDefinition[] = [ ]; export const Sort = () => { - let tableState = useTable({ - columns, - items, - }); - - tableState = useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }); - const { getRows, sort: { getSortDirection, toggleColumnSort, sort }, - } = tableState; + } = useTable( + { + columns, + items, + }, + [tableState => useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } })], + ); const headerSortProps = (columnId: ColumnId) => ({ onClick: () => { 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 192050cd68118..16cf9b6aaa145 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 @@ -112,12 +112,10 @@ export const SortControlled = () => { sortColumn: 'file', }); - let tableState = useTable({ columns, items }); - tableState = useSort(tableState, { sortState, onSortChange: setSortState }); const { getRows, sort: { getSortDirection, toggleColumnSort, sort }, - } = tableState; + } = useTable({ columns, items }, [tableState => useSort(tableState, { sortState, onSortChange: setSortState })]); const headerSortProps = (columnId: ColumnId) => ({ onClick: () => toggleColumnSort(columnId), From 0e22b3d30a0206fceee0fa213eb1b238a4e7da01 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 22 Sep 2022 09:31:29 +0200 Subject: [PATCH 07/15] update sort type so it returns enhanced rows --- packages/react-components/react-table/src/hooks/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-table/src/hooks/types.ts b/packages/react-components/react-table/src/hooks/types.ts index 166f1990cb6f5..6b466f5a54b87 100644 --- a/packages/react-components/react-table/src/hooks/types.ts +++ b/packages/react-components/react-table/src/hooks/types.ts @@ -58,7 +58,7 @@ export interface TableSortState { /** * @returns sorted rows */ - sort: (rows: RowState[]) => RowState[]; + sort: >(rows: TRowState[]) => TRowState[]; /** * Current sort direction */ From 7715ddb6cfd83d4ecd2267508f950c90fa07b080 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 22 Sep 2022 09:32:07 +0200 Subject: [PATCH 08/15] fix ColumnResize update styles --- .../react-table/src/hooks/ColumnResize.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-table/src/hooks/ColumnResize.tsx b/packages/react-components/react-table/src/hooks/ColumnResize.tsx index 6beb14877e98b..f9cdbc261a003 100644 --- a/packages/react-components/react-table/src/hooks/ColumnResize.tsx +++ b/packages/react-components/react-table/src/hooks/ColumnResize.tsx @@ -1,3 +1,4 @@ +import { NumberSymbol16Filled } from '@fluentui/react-icons'; import * as React from 'react'; import { ColumnId, ColumnWidthState } from './types'; @@ -14,6 +15,7 @@ export class ColumnResize { private mouseX: number = 0; private onColumnWidthsUpdate: () => void; private container: HTMLElement; + private tableElement: HTMLElement | null = null; private resizing: boolean = false; private resizeObserver: ResizeObserver; @@ -43,6 +45,7 @@ export class ColumnResize { public init(table: HTMLElement) { this.container = document.createElement('div'); + this.tableElement = table; table.insertAdjacentElement('beforebegin', this.container); this.resetLayout(); this.resizeObserver.observe(this.container); @@ -166,9 +169,11 @@ export class ColumnResize { }; private _updateTableStyles() { - Object.assign(this.container.style, { - tableLayout: 'fixed', - width: `${this.totalWidth}px`, - }); + if (this.tableElement) { + Object.assign(this.tableElement.style, { + tableLayout: 'fixed', + width: `${this.totalWidth}px`, + }); + } } } From d599b295054f4405fdd75521816bbe72561c471c Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 22 Sep 2022 18:07:18 +0200 Subject: [PATCH 09/15] virtualization stories --- package.json | 2 + .../react-table/src/hooks/ColumnResize.tsx | 1 - .../react-table/src/hooks/useColumnSizing.ts | 3 +- .../react-table/src/hooks/useTable.ts | 41 +--- .../stories/Table/ColumnResize.stories.tsx | 23 +- .../src/stories/Table/Everything.stories.tsx | 229 ++++++++++++++++++ ...irtualizationContentVisibility.stories.tsx | 40 +++ .../VirtualizationReactVirtual.stories.tsx | 67 +++++ .../VirtualizationReactWindow.stories.tsx | 45 ++++ .../src/stories/Table/index.stories.tsx | 5 +- yarn.lock | 47 ++++ 11 files changed, 455 insertions(+), 48 deletions(-) create mode 100644 packages/react-components/react-table/src/stories/Table/Everything.stories.tsx create mode 100644 packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx create mode 100644 packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx create mode 100644 packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx diff --git a/package.json b/package.json index b6972d0ae7772..348cc82d8f17a 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "@storybook/react": "6.5.5", "@storybook/theming": "6.5.5", "@swc/core": "1.2.220", + "@tanstack/react-virtual": "3.0.0-beta.18", "@testing-library/dom": "8.11.3", "@testing-library/jest-dom": "5.16.1", "@testing-library/react": "12.1.2", @@ -165,6 +166,7 @@ "ci-info": "3.2.0", "clean-webpack-plugin": "4.0.0", "cli-table3": "0.6.1", + "content-visibility": "1.2.2", "copy-webpack-plugin": "8.1.0", "cross-env": "^5.1.4", "css-loader": "5.0.1", diff --git a/packages/react-components/react-table/src/hooks/ColumnResize.tsx b/packages/react-components/react-table/src/hooks/ColumnResize.tsx index f9cdbc261a003..34da579102f24 100644 --- a/packages/react-components/react-table/src/hooks/ColumnResize.tsx +++ b/packages/react-components/react-table/src/hooks/ColumnResize.tsx @@ -1,4 +1,3 @@ -import { NumberSymbol16Filled } from '@fluentui/react-icons'; import * as React from 'react'; import { ColumnId, ColumnWidthState } from './types'; diff --git a/packages/react-components/react-table/src/hooks/useColumnSizing.ts b/packages/react-components/react-table/src/hooks/useColumnSizing.ts index f6c75f462a359..e61eb41386619 100644 --- a/packages/react-components/react-table/src/hooks/useColumnSizing.ts +++ b/packages/react-components/react-table/src/hooks/useColumnSizing.ts @@ -10,6 +10,7 @@ import { ColumnId, TableState } from './types'; export function useColumnSizing(tableState: TableState): TableState { const { columns, tableRef } = tableState; + const forceUpdate = React.useReducer(() => ({}), {})[1]; const manager = React.useState( () => @@ -23,7 +24,7 @@ export function useColumnSizing(tableState: TableState): TableStat if (tableRef.current) { manager.init(tableRef.current); } - }, [manager]); + }, [manager, tableRef]); return { ...tableState, diff --git a/packages/react-components/react-table/src/hooks/useTable.ts b/packages/react-components/react-table/src/hooks/useTable.ts index 83ec2ebf48e19..60d1b3e39f7ba 100644 --- a/packages/react-components/react-table/src/hooks/useTable.ts +++ b/packages/react-components/react-table/src/hooks/useTable.ts @@ -14,7 +14,7 @@ import { defaultPaginationState } from './usePagination'; const noop: () => void = () => undefined; const defaultRowEnhancer: RowEnhancer> = row => row; -const defaultSelectionState: TableSelectionStateInternal = { +export const defaultSelectionState: TableSelectionStateInternal = { allRowsSelected: false, clearRows: noop, deselectRow: noop, @@ -26,7 +26,7 @@ const defaultSelectionState: TableSelectionStateInternal = { toggleRow: noop, }; -const defaultSortState: TableSortStateInternal = { +export const defaultSortState: TableSortStateInternal = { getSortDirection: () => 'ascending', setColumnSort: noop, sort: (rows: RowState[]) => [...rows], @@ -35,7 +35,7 @@ const defaultSortState: TableSortStateInternal = { toggleColumnSort: noop, }; -const defaultColumnSizingState: TableColumnSizingState = { +export const defaultColumnSizingState: TableColumnSizingState = { getColumnWidth: () => 0, getColumnWidths: () => [], getOnMouseDown: () => () => null, @@ -47,20 +47,6 @@ export function useTable(options: UseTableOptions, plugins: TableS const { items, getRowId, columns } = options; const tableRef = React.useRef(null); - const { - isRowSelected, - toggleRow, - toggleAllRows, - clearRows, - selectedRows, - allRowsSelected, - someRowsSelected, - selectRow, - deselectRow, - } = defaultSelectionState; - - const { sort, getSortDirection, setColumnSort, sortColumn, sortDirection, toggleColumnSort } = defaultSortState; - const getRows = >( rowEnhancer = defaultRowEnhancer as RowEnhancer, ) => items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); @@ -71,25 +57,8 @@ export function useTable(options: UseTableOptions, plugins: TableS columns, getRows, pagination: defaultPaginationState as TablePaginationState, - selection: { - isRowSelected, - toggleRow, - toggleAllRows, - clearRows, - selectedRows: Array.from(selectedRows), - allRowsSelected, - someRowsSelected, - selectRow, - deselectRow, - }, - sort: { - sort: sort as (rows: RowState[]) => RowState[], - getSortDirection, - setColumnSort, - sortColumn, - sortDirection, - toggleColumnSort, - }, + selection: defaultSelectionState, + sort: defaultSortState, columnSizing: defaultColumnSizingState, tableRef, }; diff --git a/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx b/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx index d8cefe09bdea7..ab73e2f8bebff 100644 --- a/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell } from '../..'; -import { useTable, ColumnDefinition, useColumnSizing } from '../../hooks'; +import { useTable, ColumnDefinition, useColumnSizing, ColumnId } from '../../hooks'; type Item = { first: number; @@ -51,6 +51,11 @@ export const ColumnResize = () => { [useColumnSizing], ); + const getColumnStyle = (columnId: ColumnId) => ({ + minWidth: getColumnWidth(columnId), + maxWidth: getColumnWidth(columnId), + }); + return ( <>
@@ -71,19 +76,19 @@ export const ColumnResize = () => { - + First - + Second - + Third - + Fourth @@ -92,10 +97,10 @@ export const ColumnResize = () => { {getRows().map(({ item }, i) => ( - {item.first} - {item.second} - {item.third} - {item.fourth} + {item.first} + {item.second} + {item.third} + {item.fourth} ))} diff --git a/packages/react-components/react-table/src/stories/Table/Everything.stories.tsx b/packages/react-components/react-table/src/stories/Table/Everything.stories.tsx new file mode 100644 index 0000000000000..9046a4646664f --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/Everything.stories.tsx @@ -0,0 +1,229 @@ +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, + TableCellLayout, +} from '../..'; +import { useTable, ColumnDefinition, useSelection, useSort, useColumnSizing, ColumnId } 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', + 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 Everything = () => { + const { + getRows, + tableRef, + sort: { getSortDirection, toggleColumnSort, sort }, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + columnSizing: { getColumnWidth, getOnMouseDown }, + } = useTable( + { + columns, + items, + }, + [ + tableState => + // eslint-disable-next-line react-hooks/rules-of-hooks + useSelection(tableState, { + selectionMode: 'multiselect', + }), + // eslint-disable-next-line react-hooks/rules-of-hooks + tableState => useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }), + useColumnSizing, + ], + ); + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: () => { + toggleColumnSort(columnId); + }, + sortDirection: getSortDirection(columnId), + }); + + const rows = getRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); + + const getColumnStyle = (columnId: ColumnId) => ({ + minWidth: getColumnWidth(columnId), + maxWidth: getColumnWidth(columnId), + }); + + return ( +
+ + + + + File + + + + Author + + + + Last updated + + + + Last update + + + + + + {sort(rows).map(({ item, selected, onClick, onKeyDown }) => ( + + + + {item.file.label} + + + }> + {item.author.label} + + + {item.lastUpdated.label} + + {item.lastUpdate.label} + + + ))} + +
+ ); +}; + +const Resizer: React.FC> = props => { + return ( +
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx new file mode 100644 index 0000000000000..961be08b1faab --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; +import { useTable } from '../../hooks/useTable'; +import { useColumnSizing } from '../../hooks/useColumnSizing'; +import 'content-visibility'; + +export const VirtualizationContentVisibility = () => { + const { tableRef } = useTable( + { + columns: [{ columnId: 'first' }, { columnId: 'second' }, { columnId: 'third' }, { columnId: 'fourth' }], + items: [], + }, + [useColumnSizing], + ); + + return ( + + + + First + Second + Third + Fourth + + + + + {new Array(200).fill(0).map((_, i) => ( + + First {i} + Second + Third + Fourth + + ))} + + +
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx new file mode 100644 index 0000000000000..5639be3948fcc --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; +import { useVirtualizer } from '@tanstack/react-virtual'; + +export const VirtualizationReactVirtual = () => { + // The scrollable element for your list + const parentRef = React.useRef(null); + + // The virtualizer + const rowVirtualizer = useVirtualizer({ + count: 1000, + getScrollElement: () => parentRef.current, + estimateSize: () => 35, + }); + + return ( + + + + First + Second + Third + Fourth + + + + {/* The scrollable element for your list */} +
+ {/* The large inner element to hold all of the items */} +
+ {/* Only the visible items in the virtualizer, manually positioned to be in view */} + {rowVirtualizer.getVirtualItems().map(virtualItem => ( + + First {virtualItem.key} + Second + Third + Fourth + + ))} +
+
+
+
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx new file mode 100644 index 0000000000000..047cb56915fe6 --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; +import { FixedSizeList as List } from 'react-window'; +import { useTable } from '../../hooks/useTable'; +import { useColumnSizing } from '../../hooks/useColumnSizing'; + +const Row = ({ index, style }) => ( + + First {index} + Second + Third + Fourth + +); + +export const VirtualizationReactWindow = () => { + const { + columnSizing: { getTotalWidth }, + tableRef, + } = useTable( + { + columns: [{ columnId: 'first' }, { columnId: 'second' }, { columnId: 'third' }, { columnId: 'fourth' }], + items: [], + }, + [useColumnSizing], + ); + + return ( + + + + First + Second + Third + Fourth + + + + + {Row} + + +
+ ); +}; 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 9087635df0ccf..f915c3c854e60 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 @@ -11,13 +11,16 @@ export { SizeSmaller } from './SizeSmaller.stories'; export { NonNativeElements } from './NonNativeElements.stories'; export { MultipleSelect } from './MultipleSelect.stories'; export { SingleSelect } from './SingleSelect.stories'; -export { Pagination } from './Pagination.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'; export { ColumnResize } from './ColumnResize.stories'; +export { Everything } from './Everything.stories'; +export { VirtualizationReactWindow } from './VirtualizationReactWindow.stories'; +export { VirtualizationContentVisibility } from './VirtualizationContentVisibility.stories'; +export { VirtualizationReactVirtual } from './VirtualizationReactVirtual.stories'; export default { title: 'Preview Components/Table', diff --git a/yarn.lock b/yarn.lock index df23a510368ca..5b2dcdc64ab56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4956,6 +4956,18 @@ dependencies: defer-to-connect "^2.0.1" +"@tanstack/react-virtual@3.0.0-beta.18": + version "3.0.0-beta.18" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.18.tgz#b97b2019f7d6a5770fb88ee1f7591da55b9059b4" + integrity sha512-mnyCZT6htcRNw1jVb+WyfMUMbd1UmXX/JWPuMf6Bmj92DB/V7Ogk5n5rby5Y5aste7c7mlsBeMF8HtpwERRvEQ== + dependencies: + "@tanstack/virtual-core" "3.0.0-beta.18" + +"@tanstack/virtual-core@3.0.0-beta.18": + version "3.0.0-beta.18" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.18.tgz#d4b0738c1d0aada922063c899675ff4df9f696b2" + integrity sha512-tcXutY05NpN9lp3+AXI9Sn85RxSPV0EJC0XMim9oeQj/E7bjXoL0qZ4Er4wwnvIbv/hZjC91EmbIQGjgdr6nZg== + "@testim/chrome-version@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.2.tgz#092005c5b77bd3bb6576a4677110a11485e11864" @@ -6021,6 +6033,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17.0.0": + version "17.0.50" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.50.tgz#39abb4f7098f546cfcd6b51207c90c4295ee81fc" + integrity sha512-ZCBHzpDb5skMnc1zFXAXnL3l1FAdi+xZvwxK+PkglMmBrwjpp9nKaWuEvrGnSifCJmBFGxZOOFuwC6KH/s0NuA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/read-pkg-up@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@types/read-pkg-up/-/read-pkg-up-6.0.0.tgz#7926c93cf14d906e9829a859e790323d0f338684" @@ -10005,6 +10026,15 @@ content-type@^1.0.4, content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-visibility@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/content-visibility/-/content-visibility-1.2.2.tgz#12a960da7ac15066f7f4b6d833e9371c08509a33" + integrity sha512-QnBSVsaxNCya+38TY+BVD8j5axLMPCDpBsRgQ9GyQoG+5nkaTCi0WoECNcHn31wFwli4HBJM668JuDHWFwyf8w== + dependencies: + "@types/react" "^17.0.0" + intersection-observer "^0.7.0" + lit-element "^2.3.1" + conventional-changelog-angular@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.3.tgz#299fdd43df5a1f095283ac16aeedfb0a682ecab0" @@ -15947,6 +15977,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +intersection-observer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.7.0.tgz#ee16bee978db53516ead2f0a8154b09b400bbdc9" + integrity sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg== + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -18645,6 +18680,18 @@ listr2@^3.8.3: through "^2.3.8" wrap-ansi "^7.0.0" +lit-element@^2.3.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-2.5.1.tgz#3fa74b121a6cd22902409ae3859b7847d01aa6b6" + integrity sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ== + dependencies: + lit-html "^1.1.1" + +lit-html@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.4.1.tgz#0c6f3ee4ad4eb610a49831787f0478ad8e9ae5e0" + integrity sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA== + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" From bbfb2fbc0541b4a60b15a7e8ae2653a497da737c Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 22 Sep 2022 20:48:38 +0200 Subject: [PATCH 10/15] feat: Adds `layoutType` prop to Table with flex option Native `display: table` is great for column alignment and simplifies column sizing. However, native table layout is not very virtualization friendly and does cannot handle any non-compliant element in the markup. For virtualization and other cases that involve non-compliant table elements in markup, there is a `layoutType` prop with the `native`and `flex` layouts. Flex layout is what was used before #24762 with fixes to column alignment for long content. --- .../src/components/Table/Table.types.ts | 19 ++++++- .../src/components/Table/useTable.ts | 1 + .../components/Table/useTableContextValues.ts | 5 +- .../src/components/Table/useTableStyles.ts | 29 ++++++++-- .../components/TableBody/TableBody.types.ts | 3 +- .../src/components/TableBody/useTableBody.ts | 3 +- .../TableBody/useTableBodyStyles.ts | 15 ++++- .../components/TableCell/TableCell.types.ts | 3 +- .../src/components/TableCell/useTableCell.ts | 3 +- .../TableCell/useTableCellStyles.ts | 29 +++++++++- .../TableHeader/TableHeader.types.ts | 3 +- .../components/TableHeader/useTableHeader.ts | 3 +- .../TableHeader/useTableHeaderStyles.ts | 23 +++++++- .../TableHeaderCell/TableHeaderCell.types.ts | 5 +- .../TableHeaderCell/useTableHeaderCell.tsx | 3 +- .../useTableHeaderCellStyles.ts | 28 +++++++++- .../src/components/TableRow/TableRow.types.ts | 4 +- .../src/components/TableRow/useTableRow.ts | 3 +- .../components/TableRow/useTableRowStyles.ts | 55 +++++++++++++++++-- .../TableSelectionCell.types.ts | 4 +- .../useTableSelectionCell.tsx | 3 + .../useTableSelectionCellStyles.ts | 29 +++++++++- .../react-table/src/contexts/tableContext.ts | 1 + 23 files changed, 234 insertions(+), 40 deletions(-) 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 eb5cd0b2231e8..cb66c9daab117 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 @@ -5,10 +5,27 @@ export type TableSlots = { }; export type TableContextValue = { + /** + * Affects the sizes of all table subcomponents + * @default medium + */ size: 'small' | 'smaller' | 'medium'; + /** + * Render all table elements as divs intead of semantic table elements + */ noNativeElements: boolean; + /** + * Uses native browser `display: table` layout or flexbox layout. + * Recommended to use flx layout for virtualized tables + * @default native + */ + layoutType: 'native' | 'flex'; + + /** + * Whether the table is sortable + */ sortable: boolean; }; @@ -27,5 +44,5 @@ export type TableProps = ComponentProps & Partial * State used in rendering Table */ export type TableState = ComponentState & - Pick, 'size' | 'noNativeElements'> & + Pick, 'size' | 'noNativeElements' | 'layoutType'> & TableContextValue; diff --git a/packages/react-components/react-table/src/components/Table/useTable.ts b/packages/react-components/react-table/src/components/Table/useTable.ts index b520505e5da16..2b612a420f223 100644 --- a/packages/react-components/react-table/src/components/Table/useTable.ts +++ b/packages/react-components/react-table/src/components/Table/useTable.ts @@ -26,5 +26,6 @@ export const useTable_unstable = (props: TableProps, ref: React.Ref size: props.size ?? 'medium', noNativeElements: props.noNativeElements ?? false, sortable: props.sortable ?? false, + layoutType: props.layoutType ?? 'native', }; }; diff --git a/packages/react-components/react-table/src/components/Table/useTableContextValues.ts b/packages/react-components/react-table/src/components/Table/useTableContextValues.ts index e9a7bb63df310..f419716eefc91 100644 --- a/packages/react-components/react-table/src/components/Table/useTableContextValues.ts +++ b/packages/react-components/react-table/src/components/Table/useTableContextValues.ts @@ -2,15 +2,16 @@ import * as React from 'react'; import { TableContextValues, TableState } from './Table.types'; export function useTableContextValues_unstable(state: TableState): TableContextValues { - const { size, noNativeElements, sortable } = state; + const { size, noNativeElements, sortable, layoutType } = state; const tableContext = React.useMemo( () => ({ noNativeElements, size, sortable, + layoutType, }), - [noNativeElements, size, sortable], + [noNativeElements, size, sortable, layoutType], ); return { diff --git a/packages/react-components/react-table/src/components/Table/useTableStyles.ts b/packages/react-components/react-table/src/components/Table/useTableStyles.ts index c4606db905114..df4fada1b5998 100644 --- a/packages/react-components/react-table/src/components/Table/useTableStyles.ts +++ b/packages/react-components/react-table/src/components/Table/useTableStyles.ts @@ -8,15 +8,27 @@ export const tableClassNames: SlotClassNames = { root: 'fui-Table', }; +const useNativeLayoutStyles = makeStyles({ + root: { + display: 'table', + verticalAlign: 'middle', + width: '100%', + tableLayout: 'fixed', + }, +}); + +const useFlexLayoutStyles = makeStyles({ + root: { + display: 'block', + }, +}); + /** * Styles for the root slot */ const useStyles = makeStyles({ root: { - verticalAlign: 'middle', borderCollapse: 'collapse', - width: '100%', - display: 'table', backgroundColor: tokens.colorNeutralBackground1, }, }); @@ -26,7 +38,16 @@ const useStyles = makeStyles({ */ export const useTableStyles_unstable = (state: TableState): TableState => { const styles = useStyles(); - state.root.className = mergeClasses(tableClassName, styles.root, state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses( + tableClassName, + styles.root, + layoutStyles[state.layoutType].root, + state.root.className, + ); return state; }; diff --git a/packages/react-components/react-table/src/components/TableBody/TableBody.types.ts b/packages/react-components/react-table/src/components/TableBody/TableBody.types.ts index e2b732cc8225a..32e7cbf8b8eb8 100644 --- a/packages/react-components/react-table/src/components/TableBody/TableBody.types.ts +++ b/packages/react-components/react-table/src/components/TableBody/TableBody.types.ts @@ -1,4 +1,5 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { TableContextValue } from '../Table/Table.types'; export type TableBodySlots = { root: Slot<'tbody', 'div'>; @@ -12,4 +13,4 @@ export type TableBodyProps = ComponentProps; /** * State used in rendering TableBody */ -export type TableBodyState = ComponentState; +export type TableBodyState = ComponentState & Pick; diff --git a/packages/react-components/react-table/src/components/TableBody/useTableBody.ts b/packages/react-components/react-table/src/components/TableBody/useTableBody.ts index 8729e7f52bb57..409fe5e42de6b 100644 --- a/packages/react-components/react-table/src/components/TableBody/useTableBody.ts +++ b/packages/react-components/react-table/src/components/TableBody/useTableBody.ts @@ -13,7 +13,7 @@ import { useTableContext } from '../../contexts/tableContext'; * @param ref - reference to root HTMLElement of TableBody */ export const useTableBody_unstable = (props: TableBodyProps, ref: React.Ref): TableBodyState => { - const { noNativeElements } = useTableContext(); + const { noNativeElements, layoutType } = useTableContext(); const rootComponent = props.as ?? noNativeElements ? 'div' : 'tbody'; return { @@ -25,5 +25,6 @@ export const useTableBody_unstable = (props: TableBodyProps, ref: React.Ref = { root: 'fui-TableBody', @@ -17,8 +23,11 @@ export const tableBodyClassNames: SlotClassNames = { * Apply styling to the TableBody slots based on the state */ export const useTableBodyStyles_unstable = (state: TableBodyState): TableBodyState => { - const styles = useStyles(); - state.root.className = mergeClasses(tableBodyClassName, styles.root, state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses(tableBodyClassName, layoutStyles[state.layoutType].root, state.root.className); return state; }; diff --git a/packages/react-components/react-table/src/components/TableCell/TableCell.types.ts b/packages/react-components/react-table/src/components/TableCell/TableCell.types.ts index d3227e64d45e9..3c487ddb9a1ed 100644 --- a/packages/react-components/react-table/src/components/TableCell/TableCell.types.ts +++ b/packages/react-components/react-table/src/components/TableCell/TableCell.types.ts @@ -1,4 +1,5 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { TableContextValue } from '../Table/Table.types'; export type TableCellSlots = { root: Slot<'td', 'div'>; @@ -12,4 +13,4 @@ export type TableCellProps = ComponentProps & {}; /** * State used in rendering TableCell */ -export type TableCellState = ComponentState; +export type TableCellState = ComponentState & Pick; diff --git a/packages/react-components/react-table/src/components/TableCell/useTableCell.ts b/packages/react-components/react-table/src/components/TableCell/useTableCell.ts index 7bfd1a4284c5b..fd06e3d6f3b84 100644 --- a/packages/react-components/react-table/src/components/TableCell/useTableCell.ts +++ b/packages/react-components/react-table/src/components/TableCell/useTableCell.ts @@ -13,7 +13,7 @@ import { useTableContext } from '../../contexts/tableContext'; * @param ref - reference to root HTMLElement of TableCell */ export const useTableCell_unstable = (props: TableCellProps, ref: React.Ref): TableCellState => { - const { noNativeElements } = useTableContext(); + const { noNativeElements, layoutType } = useTableContext(); const rootComponent = props.as ?? noNativeElements ? 'div' : 'td'; @@ -26,5 +26,6 @@ export const useTableCell_unstable = (props: TableCellProps, ref: React.Ref = { root: tableCellClassName, }; +const useNativeLayoutStyles = makeStyles({ + root: { + display: 'table-cell', + verticalAlign: 'middle', + }, +}); + +const useFlexLayoutStyles = makeStyles({ + root: { + display: 'flex', + minWidth: '0px', + alignItems: 'center', + ...shorthands.flex(1, 1, '0px'), + }, +}); + /** * Styles for the root slot */ const useStyles = makeStyles({ root: { position: 'relative', - verticalAlign: 'middle', - display: 'table-cell', ...shorthands.padding('0px', tokens.spacingHorizontalS), }, }); @@ -25,6 +39,15 @@ const useStyles = makeStyles({ */ export const useTableCellStyles_unstable = (state: TableCellState): TableCellState => { const styles = useStyles(); - state.root.className = mergeClasses(tableCellClassNames.root, styles.root, state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses( + tableCellClassNames.root, + styles.root, + layoutStyles[state.layoutType].root, + state.root.className, + ); return state; }; diff --git a/packages/react-components/react-table/src/components/TableHeader/TableHeader.types.ts b/packages/react-components/react-table/src/components/TableHeader/TableHeader.types.ts index aa5a066fff418..8905721ade576 100644 --- a/packages/react-components/react-table/src/components/TableHeader/TableHeader.types.ts +++ b/packages/react-components/react-table/src/components/TableHeader/TableHeader.types.ts @@ -1,4 +1,5 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { TableContextValue } from '../Table/Table.types'; export type TableHeaderSlots = { root: Slot<'thead', 'div'>; @@ -12,4 +13,4 @@ export type TableHeaderProps = ComponentProps & {}; /** * State used in rendering TableHeader */ -export type TableHeaderState = ComponentState; +export type TableHeaderState = ComponentState & Pick; diff --git a/packages/react-components/react-table/src/components/TableHeader/useTableHeader.ts b/packages/react-components/react-table/src/components/TableHeader/useTableHeader.ts index 7afbe5f67ac93..1dc28c2444c2c 100644 --- a/packages/react-components/react-table/src/components/TableHeader/useTableHeader.ts +++ b/packages/react-components/react-table/src/components/TableHeader/useTableHeader.ts @@ -14,7 +14,7 @@ import { useTableContext } from '../../contexts/tableContext'; * @param ref - reference to root HTMLElement of TableHeader */ export const useTableHeader_unstable = (props: TableHeaderProps, ref: React.Ref): TableHeaderState => { - const { noNativeElements, sortable } = useTableContext(); + const { noNativeElements, sortable, layoutType } = useTableContext(); const keyboardNavAttr = useArrowNavigationGroup({ axis: 'horizontal', circular: true }); const rootComponent = props.as ?? noNativeElements ? 'div' : 'thead'; @@ -28,5 +28,6 @@ export const useTableHeader_unstable = (props: TableHeaderProps, ref: React.Ref< ...(sortable && keyboardNavAttr), ...props, }), + layoutType, }; }; diff --git a/packages/react-components/react-table/src/components/TableHeader/useTableHeaderStyles.ts b/packages/react-components/react-table/src/components/TableHeader/useTableHeaderStyles.ts index 8e58908c1d231..676f63215d901 100644 --- a/packages/react-components/react-table/src/components/TableHeader/useTableHeaderStyles.ts +++ b/packages/react-components/react-table/src/components/TableHeader/useTableHeaderStyles.ts @@ -7,7 +7,21 @@ export const tableHeaderClassNames: SlotClassNames = { root: 'fui-TableHeader', }; -const useStyles = makeStyles({ +const useFlexLayoutStyles = makeStyles({ + root: { + display: 'block', + }, + + rootNative: { + display: 'table-row-group', + }, + + rootFlex: { + display: 'block', + }, +}); + +const useNativeLayoutStyles = makeStyles({ root: { display: 'table-row-group', }, @@ -17,8 +31,11 @@ const useStyles = makeStyles({ * Apply styling to the TableHeader slots based on the state */ export const useTableHeaderStyles_unstable = (state: TableHeaderState): TableHeaderState => { - const styles = useStyles(); - state.root.className = mergeClasses(tableHeaderClassName, styles.root, state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses(tableHeaderClassName, layoutStyles[state.layoutType].root, state.root.className); return state; }; diff --git a/packages/react-components/react-table/src/components/TableHeaderCell/TableHeaderCell.types.ts b/packages/react-components/react-table/src/components/TableHeaderCell/TableHeaderCell.types.ts index 710aea8da6cd9..40ebecc88aa8d 100644 --- a/packages/react-components/react-table/src/components/TableHeaderCell/TableHeaderCell.types.ts +++ b/packages/react-components/react-table/src/components/TableHeaderCell/TableHeaderCell.types.ts @@ -1,6 +1,6 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { ARIAButtonSlotProps } from '@fluentui/react-aria'; -import { SortDirection } from '../Table/Table.types'; +import { SortDirection, TableContextValue } from '../Table/Table.types'; export type TableHeaderCellSlots = { root: Slot<'th', 'div'>; @@ -23,4 +23,5 @@ export type TableHeaderCellProps = ComponentProps> /** * State used in rendering TableHeaderCell */ -export type TableHeaderCellState = ComponentState & { sortable: boolean }; +export type TableHeaderCellState = ComponentState & + Pick; diff --git a/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCell.tsx b/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCell.tsx index 8f041ac4a0fbf..2582248471959 100644 --- a/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCell.tsx +++ b/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCell.tsx @@ -23,7 +23,7 @@ export const useTableHeaderCell_unstable = ( props: TableHeaderCellProps, ref: React.Ref, ): TableHeaderCellState => { - const { noNativeElements, sortable } = useTableContext(); + const { noNativeElements, sortable, layoutType } = useTableContext(); const rootComponent = props.as ?? noNativeElements ? 'div' : 'th'; return { @@ -55,5 +55,6 @@ export const useTableHeaderCell_unstable = ( }, }), sortable, + layoutType, }; }; diff --git a/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCellStyles.ts b/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCellStyles.ts index fd11a8a146a99..aaf4d4cf50c23 100644 --- a/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCellStyles.ts +++ b/packages/react-components/react-table/src/components/TableHeaderCell/useTableHeaderCellStyles.ts @@ -10,13 +10,26 @@ export const tableHeaderCellClassNames: SlotClassNames = { sortIcon: 'fui-TableHeaderCell__sortIcon', }; +const useNativeLayoutStyles = makeStyles({ + root: { + display: 'table-cell', + verticalAlign: 'middle', + }, +}); + +const useFlexLayoutStyles = makeStyles({ + root: { + display: 'flex', + ...shorthands.flex(1, 1, '0px'), + minWidth: '0px', + }, +}); + /** * Styles for the root slot */ const useStyles = makeStyles({ root: { - display: 'table-cell', - verticalAlign: 'middle', ...shorthands.padding('0px', tokens.spacingHorizontalS), }, @@ -60,7 +73,16 @@ const useStyles = makeStyles({ */ export const useTableHeaderCellStyles_unstable = (state: TableHeaderCellState): TableHeaderCellState => { const styles = useStyles(); - state.root.className = mergeClasses(tableHeaderCellClassNames.root, styles.root, state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses( + tableHeaderCellClassNames.root, + styles.root, + layoutStyles[state.layoutType].root, + state.root.className, + ); state.button.className = mergeClasses( tableHeaderCellClassNames.button, styles.resetButton, diff --git a/packages/react-components/react-table/src/components/TableRow/TableRow.types.ts b/packages/react-components/react-table/src/components/TableRow/TableRow.types.ts index 3b85b2edca47e..d443bee3c11b3 100644 --- a/packages/react-components/react-table/src/components/TableRow/TableRow.types.ts +++ b/packages/react-components/react-table/src/components/TableRow/TableRow.types.ts @@ -1,5 +1,5 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { TableState } from '../Table/Table.types'; +import { TableContextValue } from '../Table/Table.types'; export type TableRowSlots = { root: Slot<'tr', 'div'>; @@ -13,4 +13,4 @@ export type TableRowProps = ComponentProps & {}; /** * State used in rendering TableRow */ -export type TableRowState = ComponentState & { size: TableState['size'] }; +export type TableRowState = ComponentState & Pick; diff --git a/packages/react-components/react-table/src/components/TableRow/useTableRow.ts b/packages/react-components/react-table/src/components/TableRow/useTableRow.ts index 53f5f890aead6..897c216c6f8cb 100644 --- a/packages/react-components/react-table/src/components/TableRow/useTableRow.ts +++ b/packages/react-components/react-table/src/components/TableRow/useTableRow.ts @@ -13,7 +13,7 @@ import { useTableContext } from '../../contexts/tableContext'; * @param ref - reference to root HTMLElement of TableRow */ export const useTableRow_unstable = (props: TableRowProps, ref: React.Ref): TableRowState => { - const { noNativeElements, size } = useTableContext(); + const { noNativeElements, size, layoutType } = useTableContext(); const rootComponent = props.as ?? noNativeElements ? 'div' : 'tr'; return { @@ -26,5 +26,6 @@ export const useTableRow_unstable = (props: TableRowProps, ref: React.Ref = { root: tableRowClassName, }; +const useNativeLayoutStyles = makeStyles({ + root: { + display: 'table-row', + }, + + medium: { + height: '44px', + }, + + small: { + height: '34px', + }, + + smaller: { + height: '24px', + }, +}); + +const useFlexLayoutStyles = makeStyles({ + root: { + display: 'flex', + alignItems: 'center', + }, + + medium: { + minHeight: '44px', + }, + + small: { + minHeight: '34px', + }, + + smaller: { + minHeight: '24px', + }, +}); + /** * Styles for the root slot */ const useStyles = makeStyles({ root: { - display: 'table-row', color: tokens.colorNeutralForeground1, ':hover': { backgroundColor: tokens.colorNeutralBackground1Hover, @@ -24,20 +60,18 @@ const useStyles = makeStyles({ opacity: 1, }, }, + boxSizing: 'border-box', }, medium: { - height: '44px', ...shorthands.borderBottom(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke2), }, small: { - height: '34px', ...shorthands.borderBottom(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke2), }, smaller: { - height: '24px', fontSize: tokens.fontSizeBase200, }, }); @@ -47,7 +81,18 @@ const useStyles = makeStyles({ */ export const useTableRowStyles_unstable = (state: TableRowState): TableRowState => { const styles = useStyles(); - state.root.className = mergeClasses(tableRowClassNames.root, styles.root, styles[state.size], state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses( + tableRowClassNames.root, + styles.root, + styles[state.size], + layoutStyles[state.layoutType].root, + layoutStyles[state.layoutType][state.size], + state.root.className, + ); return state; }; diff --git a/packages/react-components/react-table/src/components/TableSelectionCell/TableSelectionCell.types.ts b/packages/react-components/react-table/src/components/TableSelectionCell/TableSelectionCell.types.ts index c8ff497d29eb3..95e2c074d6938 100644 --- a/packages/react-components/react-table/src/components/TableSelectionCell/TableSelectionCell.types.ts +++ b/packages/react-components/react-table/src/components/TableSelectionCell/TableSelectionCell.types.ts @@ -1,6 +1,7 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import type { Checkbox, CheckboxProps } from '@fluentui/react-checkbox'; import { TableCellSlots } from '../TableCell/TableCell.types'; +import { TableContextValue } from '../Table/Table.types'; export type TableSelectionCellSlots = { /** @@ -28,4 +29,5 @@ export type TableSelectionCellProps = ComponentProps & - Pick, 'type' | 'checked'>; + Pick, 'type' | 'checked'> & + Pick; diff --git a/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCell.tsx b/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCell.tsx index c66f08b6e440e..244eff5acf351 100644 --- a/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCell.tsx +++ b/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCell.tsx @@ -4,6 +4,7 @@ import { Checkbox } from '@fluentui/react-checkbox'; import { CheckmarkFilled } from '@fluentui/react-icons'; import type { TableSelectionCellProps, TableSelectionCellState } from './TableSelectionCell.types'; import { useTableCell_unstable } from '../TableCell/useTableCell'; +import { useTableContext } from '../../contexts/tableContext'; /** * Create the state required to render TableSelectionCell. @@ -19,6 +20,7 @@ export const useTableSelectionCell_unstable = ( ref: React.Ref, ): TableSelectionCellState => { const tableCellState = useTableCell_unstable(props, ref); + const { layoutType } = useTableContext(); const type = props.type ?? 'checkbox'; return { @@ -38,5 +40,6 @@ export const useTableSelectionCell_unstable = ( }), type, checked: props.checked ?? false, + layoutType, }; }; diff --git a/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCellStyles.ts b/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCellStyles.ts index f0522e8214b1b..46bbe77ba0cb6 100644 --- a/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCellStyles.ts +++ b/packages/react-components/react-table/src/components/TableSelectionCell/useTableSelectionCellStyles.ts @@ -8,16 +8,30 @@ export const tableSelectionCellClassNames: SlotClassNames { const styles = useStyles(); - state.root.className = mergeClasses(tableSelectionCellClassNames.root, styles.root, state.root.className); + const layoutStyles = { + native: useNativeLayoutStyles(), + flex: useFlexLayoutStyles(), + }; + state.root.className = mergeClasses( + tableSelectionCellClassNames.root, + styles.root, + layoutStyles[state.layoutType].root, + state.root.className, + ); if (state.checkboxIndicator) { state.checkboxIndicator.className = mergeClasses( tableSelectionCellClassNames.checkboxIndicator, diff --git a/packages/react-components/react-table/src/contexts/tableContext.ts b/packages/react-components/react-table/src/contexts/tableContext.ts index f7c87cabd0fb7..1832a2f6047c8 100644 --- a/packages/react-components/react-table/src/contexts/tableContext.ts +++ b/packages/react-components/react-table/src/contexts/tableContext.ts @@ -7,6 +7,7 @@ export const tableContextDefaultValue: TableContextValue = { size: 'medium', noNativeElements: false, sortable: false, + layoutType: 'native', }; export const TableContextProvider = tableContext.Provider; From 9bc33df9d3732a320d98ba0ea70e016406a7b468 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 23 Sep 2022 09:51:52 +0200 Subject: [PATCH 11/15] update test and md --- .../react-table/etc/react-table.api.md | 19 ++++++++----------- .../Table/useTableContextValues.test.ts | 1 + 2 files changed, 9 insertions(+), 11 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 32023ee3e96a1..4dffd4c2e4225 100644 --- a/packages/react-components/react-table/etc/react-table.api.md +++ b/packages/react-components/react-table/etc/react-table.api.md @@ -87,7 +87,7 @@ export type TableBodySlots = { }; // @public -export type TableBodyState = ComponentState; +export type TableBodyState = ComponentState & Pick; // @public export const TableCell: ForwardRefComponent; @@ -149,7 +149,7 @@ export type TableCellSlots = { }; // @public -export type TableCellState = ComponentState; +export type TableCellState = ComponentState & Pick; // @public (undocumented) export const tableClassName = "fui-Table"; @@ -164,6 +164,7 @@ export const TableContextProvider: React_2.Provider & { - sortable: boolean; -}; +export type TableHeaderCellState = ComponentState & Pick; // @public (undocumented) export const tableHeaderClassName = "fui-TableHeader"; @@ -216,7 +215,7 @@ export type TableHeaderSlots = { }; // @public -export type TableHeaderState = ComponentState; +export type TableHeaderState = ComponentState & Pick; // @public export type TableProps = ComponentProps & Partial; @@ -239,9 +238,7 @@ export type TableRowSlots = { }; // @public -export type TableRowState = ComponentState & { - size: TableState['size']; -}; +export type TableRowState = ComponentState & Pick; // @public export const TableSelectionCell: ForwardRefComponent; @@ -262,7 +259,7 @@ export type TableSelectionCellSlots = { } & Pick; // @public -export type TableSelectionCellState = ComponentState & Pick, 'type' | 'checked'>; +export type TableSelectionCellState = ComponentState & Pick, 'type' | 'checked'> & Pick; // @public (undocumented) export interface TableSelectionState { @@ -292,7 +289,7 @@ export interface TableSortState { } // @public -export type TableState = ComponentState & Pick, 'size' | 'noNativeElements'> & TableContextValue; +export type TableState = ComponentState & Pick, 'size' | 'noNativeElements' | 'layoutType'> & TableContextValue; // @public (undocumented) export function useTable = RowState>(options: UseTableOptions): TableState_2; diff --git a/packages/react-components/react-table/src/components/Table/useTableContextValues.test.ts b/packages/react-components/react-table/src/components/Table/useTableContextValues.test.ts index 3640099f95ae6..1a4566b43e3e9 100644 --- a/packages/react-components/react-table/src/components/Table/useTableContextValues.test.ts +++ b/packages/react-components/react-table/src/components/Table/useTableContextValues.test.ts @@ -13,6 +13,7 @@ describe('useTableContextValues', () => { expect(result.current).toMatchInlineSnapshot(` Object { "table": Object { + "layoutType": "native", "noNativeElements": false, "size": "medium", "sortable": false, From 52ead2399c37011beda72c47128ded0091c667cc Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 23 Sep 2022 10:18:03 +0200 Subject: [PATCH 12/15] vr tests should test both layout types --- .../src/stories/Table.stories.tsx | 925 +++++++++--------- 1 file changed, 469 insertions(+), 456 deletions(-) diff --git a/apps/vr-tests-react-components/src/stories/Table.stories.tsx b/apps/vr-tests-react-components/src/stories/Table.stories.tsx index 1cdbaef5daf7c..4f18442262a7c 100644 --- a/apps/vr-tests-react-components/src/stories/Table.stories.tsx +++ b/apps/vr-tests-react-components/src/stories/Table.stories.tsx @@ -21,6 +21,7 @@ import { TableCellLayout, TableSelectionCell, TableCellActions, + TableProps, } from '@fluentui/react-table'; import { Button } from '@fluentui/react-button'; import { storiesOf } from '@storybook/react'; @@ -72,474 +73,486 @@ const columns = [ { columnKey: 'lastUpdate', label: 'Last update' }, ]; -storiesOf('Table - cell actions', module) - .addDecorator(story => ( - {story()} - )) - .addStory( - 'default', - () => ( - - - - {columns.map(column => ( - {column.label} - ))} - - - - {items.map(item => ( - - - - {item.file.label} - -
- ), - { includeDarkMode: true, includeHighContrast: true, includeRtl: true }, - ) - .addStory( - 'always visible', - () => ( - - - - {columns.map(column => ( - {column.label} - ))} - - - - {items.map(item => ( - - - - {item.file.label} - -
- ), - { includeDarkMode: true, includeHighContrast: true, includeRtl: true }, - ) - .addStory('in header cell', () => ( - - - - {columns.map(column => ( - {column.label} - ))} +interface SharedVrTestArgs { + layoutType: TableProps['layoutType']; +} + +const CellActionsDefault: React.FC = ({ layoutType }) => ( +
+ + + {columns.map(column => ( + {column.label} + ))} + + + + {items.map(item => ( + + + + {item.file.label} + +
+); + +const CellActionsAlwaysVisible: React.FC = ({ layoutType }) => ( + + + + {columns.map(column => ( + {column.label} ))} - -
- )); + + + + {items.map(item => ( + + + + {item.file.label} + +
- +
diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx deleted file mode 100644 index 961be08b1faab..0000000000000 --- a/packages/react-components/react-table/src/stories/Table/VirtualizationContentVisibility.stories.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; -import { useTable } from '../../hooks/useTable'; -import { useColumnSizing } from '../../hooks/useColumnSizing'; -import 'content-visibility'; - -export const VirtualizationContentVisibility = () => { - const { tableRef } = useTable( - { - columns: [{ columnId: 'first' }, { columnId: 'second' }, { columnId: 'third' }, { columnId: 'fourth' }], - items: [], - }, - [useColumnSizing], - ); - - return ( -
- - - First - Second - Third - Fourth - - - - - {new Array(200).fill(0).map((_, i) => ( - - First {i} - Second - Third - Fourth - - ))} - - -
- ); -}; diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx index 5639be3948fcc..141f3dc79702c 100644 --- a/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx @@ -14,7 +14,7 @@ export const VirtualizationReactVirtual = () => { }); return ( - +
First diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx index 047cb56915fe6..a86a3794a4c34 100644 --- a/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx @@ -26,7 +26,7 @@ export const VirtualizationReactWindow = () => { ); return ( -
+
First diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationRecyclerListView.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationRecyclerListView.stories.tsx new file mode 100644 index 0000000000000..8319e357c12ef --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationRecyclerListView.stories.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; +import { RecyclerListView, LayoutProvider, DataProvider } from 'recyclerlistview/web'; +import { useTable } from '../../hooks/useTable'; + +const layoutProvider = new LayoutProvider( + () => 0, + (_, dimensions) => { + dimensions.width = window.innerWidth; + dimensions.height = 45; + }, +); + +const dataProvider = new DataProvider((r1, r2) => r1 !== r2); + +export const VirtualizationRecyclerListView = () => { + const { tableRef } = useTable( + { + columns: [{ columnId: 'first' }, { columnId: 'second' }, { columnId: 'third' }, { columnId: 'fourth' }], + items: [], + }, + [], + ); + + const items = new Array(10000).fill(0).map((_, i) => i); + + return ( +
+ + + First + Second + Third + Fourth + + + + { + return ( + + First + Second + Third + Fourth + + ); + }} + /> + +
+ ); +}; diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationResembli.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationResembli.stories.tsx new file mode 100644 index 0000000000000..5426886455b92 --- /dev/null +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationResembli.stories.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; +import { List } from '@resembli/react-virtualized-window'; + +const sampleData = Array.from({ length: 10000 }, (_, i) => i); + +const Row = ({ data, style }) => { + return ( + + First {data} + Second + Third + Fourth + + ); +}; + +export const VirtualizationResembli = () => { + return ( + + + + First + Second + Third + Fourth + + + + + + {Row} + + +
+ ); +}; 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 f915c3c854e60..f61d1d38a5a1b 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 @@ -19,8 +19,9 @@ export { CompositeNavigationMode } from './CompositeNavigationMode.stories'; export { ColumnResize } from './ColumnResize.stories'; export { Everything } from './Everything.stories'; export { VirtualizationReactWindow } from './VirtualizationReactWindow.stories'; -export { VirtualizationContentVisibility } from './VirtualizationContentVisibility.stories'; export { VirtualizationReactVirtual } from './VirtualizationReactVirtual.stories'; +export { VirtualizationRecyclerListView } from './VirtualizationRecyclerListView.stories'; +export { VirtualizationResembli } from './VirtualizationResembli.stories'; export default { title: 'Preview Components/Table', diff --git a/yarn.lock b/yarn.lock index 5b2dcdc64ab56..67e4de35b6e8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3654,6 +3654,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398" integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg== +"@resembli/react-virtualized-window@0.8.6": + version "0.8.6" + resolved "https://registry.yarnpkg.com/@resembli/react-virtualized-window/-/react-virtualized-window-0.8.6.tgz#e1f6096b805e264db46fd53b00116760f8d67241" + integrity sha512-pJwEjGHqTQHV2nssp32iBWdToWDC13W471FXIfnwebzanhQAt44gu4awmeDDe/8Kr7jUZYBbqEl4Yb5yBhJZEQ== + "@rnx-kit/eslint-plugin@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@rnx-kit/eslint-plugin/-/eslint-plugin-0.2.5.tgz#30b4398e6db4f7a81b301c5e825341c4386f6821" @@ -13273,6 +13278,19 @@ fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.18" +fbjs@^0.8.9: + version "0.8.18" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.18.tgz#9835e0addb9aca2eff53295cd79ca1cfc7c9662a" + integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA== + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.30" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -18868,7 +18886,7 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.debounce@^4.0.8: +lodash.debounce@4.0.8, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= @@ -22341,6 +22359,13 @@ promzard@^0.3.0: dependencies: read "1" +prop-types@15.5.8: + version "15.5.8" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" + integrity sha512-QiDx7s0lWoAVxmEmOYnn3rIZGduup2PZgj3rta5O5y0NfPKu3ApWi+GdMfTto7PmO/5+p4yamSLMZkj0jaTL4A== + dependencies: + fbjs "^0.8.9" + prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -23324,6 +23349,15 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +recyclerlistview@3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-3.0.5.tgz#50bf5bcaa401d56bb6bb264354083f4d424408eb" + integrity sha512-JVHz13u520faEsbVqFrJOMuJjc4mJlOXODe5QdqAJHdl5/IpyYeo83uiHrpzxyLb8QtJ0889JMlDik+Z1Ed0QQ== + dependencies: + lodash.debounce "4.0.8" + prop-types "15.5.8" + ts-object-utils "0.0.5" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -26600,6 +26634,11 @@ ts-node@~9.1.1: source-map-support "^0.5.17" yn "3.1.1" +ts-object-utils@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077" + integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA== + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" From f19cf60c159f5c2c97ffa02cb8bca63d69bbac43 Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Fri, 23 Sep 2022 17:46:00 +0200 Subject: [PATCH 15/15] add sort and selection --- .../VirtualizationReactVirtual.stories.tsx | 126 ++++++++++++++---- .../VirtualizationReactWindow.stories.tsx | 102 +++++++++++--- 2 files changed, 189 insertions(+), 39 deletions(-) diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx index 141f3dc79702c..a000708ebdc79 100644 --- a/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx @@ -1,6 +1,39 @@ import * as React from 'react'; -import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } from '../..'; +import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow, TableSelectionCell } from '../..'; import { useVirtualizer } from '@tanstack/react-virtual'; +import { useTable } from '../../hooks/useTable'; +import { useSelection } from '../../hooks/useSelection'; +import { useSort } from '../../hooks/useSort'; +import { ColumnDefinition, ColumnId } from '../../hooks/types'; + +const columns: ColumnDefinition<{ index: number }>[] = [ + { + columnId: 'first', + compare: (a, b) => { + return a.index - b.index; + }, + }, + { + columnId: 'second', + compare: (a, b) => { + return a.index - b.index; + }, + }, + { + columnId: 'third', + compare: (a, b) => { + return a.index - b.index; + }, + }, + { + columnId: 'fourth', + compare: (a, b) => { + return a.index - b.index; + }, + }, +]; + +const items = new Array(1000).fill(0).map((_, i) => ({ index: i })); export const VirtualizationReactVirtual = () => { // The scrollable element for your list @@ -13,14 +46,53 @@ export const VirtualizationReactVirtual = () => { estimateSize: () => 35, }); + const { + getRows, + sort: { getSortDirection, toggleColumnSort, sort }, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, + } = useTable( + { + columns, + items, + }, + [ + tableState => useSelection(tableState, { selectionMode: 'multiselect' }), + tableState => useSort(tableState, { defaultSortState: { sortColumn: 'first', sortDirection: 'ascending' } }), + ], + ); + + let rows = getRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); + + rows = sort(rows); + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: () => { + toggleColumnSort(columnId); + }, + sortDirection: getSortDirection(columnId), + }); + return ( - First - Second - Third - Fourth + + First + Second + Third + Fourth @@ -41,24 +113,32 @@ export const VirtualizationReactVirtual = () => { }} > {/* Only the visible items in the virtualizer, manually positioned to be in view */} - {rowVirtualizer.getVirtualItems().map(virtualItem => ( - - First {virtualItem.key} - Second - Third - Fourth - - ))} + {rowVirtualizer.getVirtualItems().map(virtualItem => { + const { + selected, + onClick, + item: { index: userIndex }, + } = rows[virtualItem.index]; + + const style = { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: `${virtualItem.size}px`, + transform: `translateY(${virtualItem.start}px)`, + }; + + return ( + + + First {userIndex} + Second {userIndex} + Third {userIndex} + Fourth {userIndex} + + ); + })} diff --git a/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx index a86a3794a4c34..c64fd9b403821 100644 --- a/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx +++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx @@ -3,40 +3,110 @@ import { Table, TableHeader, TableHeaderCell, TableCell, TableBody, TableRow } f import { FixedSizeList as List } from 'react-window'; import { useTable } from '../../hooks/useTable'; import { useColumnSizing } from '../../hooks/useColumnSizing'; +import { useSelection } from '../../hooks/useSelection'; +import { useSort } from '../../hooks/useSort'; +import { TableSelectionCell } from '../../components/TableSelectionCell/TableSelectionCell'; +import { ColumnDefinition, ColumnId } from '../../hooks/types'; -const Row = ({ index, style }) => ( - - First {index} - Second - Third - Fourth - -); +const columns: ColumnDefinition<{ index: number }>[] = [ + { + columnId: 'first', + compare: (a, b) => { + return a.index - b.index; + }, + }, + { + columnId: 'second', + compare: (a, b) => { + return a.index - b.index; + }, + }, + { + columnId: 'third', + compare: (a, b) => { + return a.index - b.index; + }, + }, + { + columnId: 'fourth', + compare: (a, b) => { + return a.index - b.index; + }, + }, +]; + +const Row = ({ index, style, data }) => { + const { + selected, + onClick, + item: { index: userIndex }, + } = data[index]; + return ( + + + First {userIndex} + Second {userIndex} + Third {userIndex} + Fourth {userIndex} + + ); +}; + +const items = new Array(1000).fill(0).map((_, i) => ({ index: i })); export const VirtualizationReactWindow = () => { const { columnSizing: { getTotalWidth }, tableRef, + getRows, + sort: { getSortDirection, toggleColumnSort, sort }, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, } = useTable( { - columns: [{ columnId: 'first' }, { columnId: 'second' }, { columnId: 'third' }, { columnId: 'fourth' }], - items: [], + columns, + items, }, - [useColumnSizing], + [ + useColumnSizing, + tableState => useSelection(tableState, { selectionMode: 'multiselect' }), + tableState => useSort(tableState, { defaultSortState: { sortColumn: 'first', sortDirection: 'ascending' } }), + ], ); + const rows = getRows(row => ({ + ...row, + onClick: () => toggleRow(row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + toggleRow(row.rowId); + } + }, + selected: isRowSelected(row.rowId), + })); + + const headerSortProps = (columnId: ColumnId) => ({ + onClick: () => { + toggleColumnSort(columnId); + }, + sortDirection: getSortDirection(columnId), + }); + return (
- First - Second - Third - Fourth + + First + Second + Third + Fourth - + {Row}