From b8058f791ac0bd27f077cd62e988786e7720534e Mon Sep 17 00:00:00 2001 From: Lingfan Gao Date: Thu, 12 Dec 2024 15:21:38 +0000 Subject: [PATCH] reflow experiment --- .../react-table/library/src/hooks/types.ts | 2 +- .../library/src/hooks/useTableFeatures.ts | 2 +- .../stories/src/DataGrid/Default.stories.tsx | 16 +- .../stories/src/DataGrid/Reflow.stories.tsx | 657 ++++++++++++++++++ .../DataGrid/ReflowStickyColumn.stories.tsx | 479 +++++++++++++ .../stories/src/DataGrid/index.stories.tsx | 2 + 6 files changed, 1153 insertions(+), 5 deletions(-) create mode 100644 packages/react-components/react-table/stories/src/DataGrid/Reflow.stories.tsx create mode 100644 packages/react-components/react-table/stories/src/DataGrid/ReflowStickyColumn.stories.tsx diff --git a/packages/react-components/react-table/library/src/hooks/types.ts b/packages/react-components/react-table/library/src/hooks/types.ts index b1c1e1ba15d61a..b4c8fdd0a8ac5a 100644 --- a/packages/react-components/react-table/library/src/hooks/types.ts +++ b/packages/react-components/react-table/library/src/hooks/types.ts @@ -166,7 +166,7 @@ export interface UseTableFeaturesOptions { getRowId?: (item: TItem) => TableRowId; } -export type TableFeaturePlugin = (tableState: TableFeaturesState) => TableFeaturesState; +export type TableFeaturePlugin = (tableState: TableFeaturesState) => TableFeaturesState; export interface ColumnWidthState { columnId: TableColumnId; diff --git a/packages/react-components/react-table/library/src/hooks/useTableFeatures.ts b/packages/react-components/react-table/library/src/hooks/useTableFeatures.ts index 133798e72b859b..1494a82dfe2873 100644 --- a/packages/react-components/react-table/library/src/hooks/useTableFeatures.ts +++ b/packages/react-components/react-table/library/src/hooks/useTableFeatures.ts @@ -27,7 +27,7 @@ export const defaultTableState: TableFeaturesState = { export function useTableFeatures( options: UseTableFeaturesOptions, - plugins: TableFeaturePlugin[] = [], + plugins: TableFeaturePlugin[] = [], ): TableFeaturesState { const { items, getRowId, columns } = options; diff --git a/packages/react-components/react-table/stories/src/DataGrid/Default.stories.tsx b/packages/react-components/react-table/stories/src/DataGrid/Default.stories.tsx index 56daaca1cee000..e8768a264a2180 100644 --- a/packages/react-components/react-table/stories/src/DataGrid/Default.stories.tsx +++ b/packages/react-components/react-table/stories/src/DataGrid/Default.stories.tsx @@ -157,16 +157,26 @@ export const Default = () => { selectionMode="multiselect" getRowId={item => item.file.label} focusMode="composite" - style={{ minWidth: '550px' }} > - + {({ renderHeaderCell }) => {renderHeaderCell()}} > {({ item, rowId }) => ( - key={rowId} selectionCell={{ checkboxIndicator: { 'aria-label': 'Select row' } }}> + + key={rowId} + selectionCell={{ + checkboxIndicator: { 'aria-label': 'Select row' }, + style: { position: 'sticky', left: 0, zIndex: 1, background: 'white' }, + }} + > {({ renderCell }) => {renderCell(item)}} )} diff --git a/packages/react-components/react-table/stories/src/DataGrid/Reflow.stories.tsx b/packages/react-components/react-table/stories/src/DataGrid/Reflow.stories.tsx new file mode 100644 index 00000000000000..f39faa93d5e8cf --- /dev/null +++ b/packages/react-components/react-table/stories/src/DataGrid/Reflow.stories.tsx @@ -0,0 +1,657 @@ +import * as React from 'react'; +import { + FolderRegular, + EditRegular, + DocumentRegular, + PeopleRegular, + DocumentPdfRegular, + VideoRegular, + ArrowUpRegular, + ArrowDownRegular, + MoreHorizontalRegular, +} from '@fluentui/react-icons'; +import { + PresenceBadgeStatus, + Avatar, + TableCellLayout, + TableColumnDefinition, + createTableColumn, + makeStyles, + tokens, + Field, + SpinButton, + useTableFeatures, + useTableSelection, + useTableCompositeNavigation, + useTableSort, + Button, + Menu, + MenuTrigger, + MenuList, + MenuPopover, + TableCellActions, + tableCellActionsClassNames, + mergeClasses, + MenuItemRadio, + TableRowData, + TableRowId, + TableColumnId, + TableSelectionCell, + TableSelectionCellProps, + DataGrid, + DataGridHeader, + DataGridRow, + DataGridHeaderCell, + DataGridBody, + DataGridCell, +} from '@fluentui/react-components'; + +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 generateTimestamp = (): number => { + const randomHour = Math.floor(Math.random() * 72); // Up to 72 hours + return Date.now() - randomHour * 3600000; // Return timestamp in milliseconds +}; + +// Helper function to generate random status +const generateStatus = (): 'available' | 'busy' | 'away' | 'offline' => { + const statuses: ('available' | 'busy' | 'away' | 'offline')[] = ['available', 'busy', 'away', 'offline']; + return statuses[Math.floor(Math.random() * statuses.length)]; +}; + +// Helper function to generate random labels for file and author +const generateRandomLabel = (): string => { + const randomNames = ['Max Mustermann', 'Erika Mustermann', 'John Doe', 'Jane Doe', 'Alice Smith', 'Bob Johnson']; + const randomFileNames = [ + 'Meeting notes', + 'Thursday presentation', + 'Training recording', + 'Purchase order', + 'Project plan', + 'Annual report', + ]; + + return Math.random() > 0.5 + ? randomNames[Math.floor(Math.random() * randomNames.length)] + : randomFileNames[Math.floor(Math.random() * randomFileNames.length)]; +}; + +// Main function to generate test data +const generateTestData = (numItems: number): Item[] => { + const items: Item[] = []; + for (let i = 0; i < numItems; i++) { + const authorStatus = generateStatus(); + const lastUpdatedTimestamp = generateTimestamp(); + + items.push({ + file: { + label: generateRandomLabel(), + // eslint-disable-next-line react/jsx-key + icon: [, , , ][ + Math.floor(Math.random() * 4) + ], // Random icon + }, + author: { + label: generateRandomLabel(), + status: authorStatus, + }, + lastUpdated: { + label: `${Math.floor(Math.random() * 24)}h ago`, // Random label for simplicity + timestamp: lastUpdatedTimestamp, + }, + lastUpdate: { + label: Math.random() > 0.5 ? 'You edited this' : 'You shared this in a Teams chat', // Random last update label + icon: Math.random() > 0.5 ? : , // Random icon + }, + }); + } + + return items; +}; + +const items = generateTestData(30); + +// const items: Item[] = [ +// { +// file: { label: 'Meeting notes', icon: }, +// author: { label: 'Max Mustermann', status: 'available' }, +// lastUpdated: { label: '7h ago', timestamp: 1 }, +// 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: 3 }, +// lastUpdate: { +// label: 'You shared this in a Teams chat', +// icon: , +// }, +// }, +// ]; + +const columns: TableColumnDefinition[] = [ + createTableColumn({ + columnId: 'file', + compare: (a, b) => { + return a.file.label.localeCompare(b.file.label); + }, + renderHeaderCell: () => { + return 'File'; + }, + renderCell: item => { + return ( + <> + {item.file.label} + + + ); +}); + +const ReflowHeader: React.FC<{ + toggleAllRows: () => void; + toggleAllKeydown: () => void; + someRowsSelected: boolean; + allRowsSelected: boolean; + sortColumn: TableColumnId | undefined; + sortDirection: 'ascending' | 'descending' | undefined; + toggleColumnSort: (e: React.MouseEvent, columnId: TableColumnId) => void; +}> = props => { + const styles = useStyles(); + const { + toggleAllRows, + toggleAllKeydown, + someRowsSelected, + allRowsSelected, + sortColumn, + sortDirection, + toggleColumnSort, + } = props; + return ( +
+ + + + + + + + {columns.map(column => ( + toggleColumnSort(e, column.columnId)} + > + {column.renderHeaderCell()} + + ))} + + + +
+ ); +}; + +const ReflowRow: React.FC<{ + row: TableRowData & { onClick: React.MouseEventHandler }; + tableRowTabsterAttribute: { 'data-tabster': string | undefined }; + children: React.ReactNode; + isRowSelected: (rowId: TableRowId) => boolean; +}> = props => { + const styles = useStyles(); + const { children, row, tableRowTabsterAttribute, isRowSelected } = props; + return ( +
+ +
{children}
+
+ ); +}; + +const ReflowCell: React.FC<{ + className: string; + column: TableColumnDefinition; + row: TableRowData; +}> = props => { + const styles = useStyles(); + const { className, column, row } = props; + + return ( +
+
{column.renderHeaderCell(row.item)}
+
{column.renderCell(row.item)}
+
+ ); +}; diff --git a/packages/react-components/react-table/stories/src/DataGrid/ReflowStickyColumn.stories.tsx b/packages/react-components/react-table/stories/src/DataGrid/ReflowStickyColumn.stories.tsx new file mode 100644 index 00000000000000..0351b5ff345dc8 --- /dev/null +++ b/packages/react-components/react-table/stories/src/DataGrid/ReflowStickyColumn.stories.tsx @@ -0,0 +1,479 @@ +import * as React from 'react'; +import { + FolderRegular, + EditRegular, + DocumentRegular, + PeopleRegular, + DocumentPdfRegular, + VideoRegular, + MoreHorizontalRegular, +} from '@fluentui/react-icons'; +import { + PresenceBadgeStatus, + Avatar, + TableCellLayout, + TableColumnDefinition, + createTableColumn, + makeStyles, + tokens, + Field, + SpinButton, + Button, + TableCellActions, + tableCellActionsClassNames, + DataGrid, + DataGridHeader, + DataGridRow, + DataGridHeaderCell, + DataGridBody, + DataGridCell, +} from '@fluentui/react-components'; + +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 generateTimestamp = (): number => { + const randomHour = Math.floor(Math.random() * 72); // Up to 72 hours + return Date.now() - randomHour * 3600000; // Return timestamp in milliseconds +}; + +// Helper function to generate random status +const generateStatus = (): 'available' | 'busy' | 'away' | 'offline' => { + const statuses: ('available' | 'busy' | 'away' | 'offline')[] = ['available', 'busy', 'away', 'offline']; + return statuses[Math.floor(Math.random() * statuses.length)]; +}; + +// Helper function to generate random labels for file and author +const generateRandomLabel = (): string => { + const randomNames = ['Max Mustermann', 'Erika Mustermann', 'John Doe', 'Jane Doe', 'Alice Smith', 'Bob Johnson']; + const randomFileNames = [ + 'Meeting notes', + 'Thursday presentation', + 'Training recording', + 'Purchase order', + 'Project plan', + 'Annual report', + ]; + + return Math.random() > 0.5 + ? randomNames[Math.floor(Math.random() * randomNames.length)] + : randomFileNames[Math.floor(Math.random() * randomFileNames.length)]; +}; + +// Main function to generate test data +const generateTestData = (numItems: number): Item[] => { + const items: Item[] = []; + for (let i = 0; i < numItems; i++) { + const authorStatus = generateStatus(); + const lastUpdatedTimestamp = generateTimestamp(); + + items.push({ + file: { + label: generateRandomLabel(), + // eslint-disable-next-line react/jsx-key + icon: [, , , ][ + Math.floor(Math.random() * 4) + ], // Random icon + }, + author: { + label: generateRandomLabel(), + status: authorStatus, + }, + lastUpdated: { + label: `${Math.floor(Math.random() * 24)}h ago`, // Random label for simplicity + timestamp: lastUpdatedTimestamp, + }, + lastUpdate: { + label: Math.random() > 0.5 ? 'You edited this' : 'You shared this in a Teams chat', // Random last update label + icon: Math.random() > 0.5 ? : , // Random icon + }, + }); + } + + return items; +}; + +const items = generateTestData(30); + +// const items: Item[] = [ +// { +// file: { label: 'Meeting notes', icon: }, +// author: { label: 'Max Mustermann', status: 'available' }, +// lastUpdated: { label: '7h ago', timestamp: 1 }, +// 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: 3 }, +// lastUpdate: { +// label: 'You shared this in a Teams chat', +// icon: , +// }, +// }, +// ]; + +const columns: TableColumnDefinition[] = [ + createTableColumn({ + columnId: 'file', + compare: (a, b) => { + return a.file.label.localeCompare(b.file.label); + }, + renderHeaderCell: () => { + return 'File'; + }, + renderCell: item => { + return ( + <> + {item.file.label} + +