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}
-
- } appearance="subtle" />
- } appearance="subtle" />
-
-
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- )
- .addStory(
- 'always visible',
- () => (
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map(item => (
-
-
-
- {item.file.label}
-
- } appearance="subtle" />
- } appearance="subtle" />
-
-
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.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}
+
+ } appearance="subtle" />
+ } appearance="subtle" />
+
+
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
-
-
- {items.map(item => (
-
-
-
- {item.file.label}
-
- } appearance="subtle" />
- } appearance="subtle" />
-
-
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
+ ))}
+
+
+);
+
+const CellActionsAlwaysVisible: React.FC = ({ layoutType }) => (
+
+
+
+ {columns.map(column => (
+ {column.label}
))}
-
-
- ));
+
+
+
+ {items.map(item => (
+
+
+
+ {item.file.label}
+
+ } appearance="subtle" />
+ } appearance="subtle" />
+
+
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
-storiesOf('Table', module)
- .addStory(
- 'default',
- () => (
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map(item => (
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
-
- {item.lastUpdated.label}
-
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- )
- .addStory('size - small', () => (
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
+const CellActionsInHeaderCell: React.FC = ({ layoutType }) => (
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map(item => (
+
+
+
+ {item.file.label}
+
+ } appearance="subtle" />
+ } appearance="subtle" />
+
+
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
-
-
- {items.map(item => (
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
+ ))}
+
+
+);
+
+const SizeMedium: React.FC = ({ layoutType }) => (
+
+
+
+ {columns.map(column => (
+ {column.label}
))}
-
-
- ))
- .addStory('size - smaller', () => (
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
+
+
+
+ {items.map(item => (
+
+
+ {item.file.label}
+
+
+ }
+ >
+ {item.author.label}
+
+
+
+ {item.lastUpdated.label}
+
+
+ {item.lastUpdate.label}
+
-
-
- {items.map(item => (
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
+ ))}
+
+
+);
+
+const SizeSmall: React.FC = ({ layoutType }) => (
+
+
+
+ {columns.map(column => (
+ {column.label}
))}
-
-
- ))
- .addStory('primary cell', () => (
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
+
+
+
+ {items.map(item => (
+
+
+ {item.file.label}
+
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
-
-
- {items.map(item => (
-
-
-
- {item.file.label}
-
-
+ ))}
+
+
+);
+
+const SizeSmaller: React.FC = ({ layoutType }) => (
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map(item => (
+
+
+ {item.file.label}
+
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+const PrimaryCell: React.FC = ({ layoutType }) => (
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map(item => (
+
+
+
+ {item.file.label}
+
+
+ }
+ >
+ {item.author.label}
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+const Multiselect: React.FC = ({ layoutType }) => (
+
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map(item => (
+
+
+
+ {item.file.label}
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+const MultiselectChecked: React.FC = ({ layoutType }) => (
+
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map(item => (
+
+
+
+ {item.file.label}
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+const MultiselectMixed: React.FC = ({ layoutType }) => (
+
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map((item, i) => (
+
+
+
+ {item.file.label}
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+const Singleselect: React.FC = ({ layoutType }) => (
+
+
+
+
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {items.map(item => (
+
+
+
+ {item.file.label}
+
+
}
>
{item.author.label}
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+const SingleselectChecked: React.FC = ({ layoutType }) => (
+
+
+
+
+ {columns.map(column => (
+ {column.label}
))}
-
-
- ))
- .addStory(
- 'multiselect',
- () => (
-
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map(item => (
-
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- )
- .addStory(
- 'multiselect (checked)',
- () => (
-
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map(item => (
-
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- )
- .addStory(
- 'multiselect (mixed)',
- () => (
-
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map((item, i) => (
-
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- )
- .addStory(
- 'single select',
- () => (
-
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map(item => (
-
-
-
- {item.file.label}
-
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- )
- .addStory(
- 'single select (checked)',
- () => (
-
-
-
-
- {columns.map(column => (
- {column.label}
- ))}
-
-
-
- {items.map((item, i) => (
-
-
-
- {item.file.label}
-
+
+
+
+ {items.map((item, i) => (
+
+
+
+ {item.file.label}
+
+
+
+ }
+ >
+ {item.author.label}
+
+
+ {item.lastUpdated.label}
+
+ {item.lastUpdate.label}
+
+
+ ))}
+
+
+);
+
+(['native', 'flex'] as const).forEach(layoutType => {
+ storiesOf(`Table layout ${layoutType} - cell actions`, module)
+ .addDecorator(story => (
+ {story()}
+ ))
+ .addStory('default', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('always visible', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('in header cell', () => );
-
-
- }
- >
- {item.author.label}
-
-
- {item.lastUpdated.label}
-
- {item.lastUpdate.label}
-
-
- ))}
-
-
- ),
- { includeDarkMode: true, includeHighContrast: true, includeRtl: true },
- );
+ storiesOf(`Table layout ${layoutType}`, module)
+ .addStory('size - medium', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('size - small', () => )
+ .addStory('size - smaller', () => )
+ .addStory('primary cell', () => )
+ .addStory('multiselect', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('multiselect (checked)', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('multiselect (mixed)', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('single select', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ })
+ .addStory('single select (checked)', () => , {
+ includeDarkMode: true,
+ includeHighContrast: true,
+ includeRtl: true,
+ });
+});
diff --git a/change/@fluentui-react-table-69a4bbc7-8575-45e4-b339-04032ef3037f.json b/change/@fluentui-react-table-69a4bbc7-8575-45e4-b339-04032ef3037f.json
new file mode 100644
index 0000000000000..95449e38bdf3b
--- /dev/null
+++ b/change/@fluentui-react-table-69a4bbc7-8575-45e4-b339-04032ef3037f.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "feat: Adds layoutType prop to Table with flex option ",
+ "packageName": "@fluentui/react-table",
+ "email": "lingfangao@hotmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/package.json b/package.json
index b6972d0ae7772..1f31a176189b4 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
"@nrwl/js": "13.10.6",
"@nrwl/node": "13.10.6",
"@nrwl/workspace": "13.10.6",
+ "@resembli/react-virtualized-window": "0.8.6",
"@storybook/addon-a11y": "6.5.5",
"@storybook/addon-actions": "6.5.5",
"@storybook/addon-docs": "6.5.5",
@@ -114,6 +115,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 +167,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",
@@ -234,6 +237,7 @@
"react-dom": "17.0.2",
"react-is": "17.0.2",
"react-test-renderer": "17.0.2",
+ "recyclerlistview": "3.0.5",
"sass": "1.49.11",
"sass-loader": "12.4.0",
"satisfied": "^1.1.1",
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/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.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,
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;
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..150d3545e9798
--- /dev/null
+++ b/packages/react-components/react-table/src/hooks/ColumnResize.tsx
@@ -0,0 +1,177 @@
+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 tableElement: HTMLElement | null = null;
+ 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');
+ this.tableElement = table;
+ 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];
+
+ 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() {
+ if (this.tableElement) {
+ Object.assign(this.tableElement.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 f5090f5922fa7..fe760ce51ad64 100644
--- a/packages/react-components/react-table/src/hooks/index.ts
+++ b/packages/react-components/react-table/src/hooks/index.ts
@@ -1,2 +1,6 @@
export * from './types';
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 bd5f581722b31..6b466f5a54b87 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;
@@ -19,7 +20,6 @@ export interface ColumnDefinition {
export type RowEnhancer = RowState> = (
row: RowState,
- state: { selection: TableSelectionState; sort: TableSortState },
) => TRowState;
export interface TableSortStateInternal {
@@ -31,39 +31,15 @@ export interface TableSortStateInternal {
/**
* Returns a sorted **shallow** copy of original items
*/
- sort: (items: TItem[]) => TItem[];
+ sort: (rows: RowState[]) => RowState[];
}
-export interface UseTableOptions = RowState> {
+export interface UseTableOptions {
+ 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;
}
export interface TableSelectionStateInternal {
@@ -78,7 +54,11 @@ export interface TableSelectionStateInternal {
someRowsSelected: boolean;
}
-export interface TableSortState {
+export interface TableSortState {
+ /**
+ * @returns sorted rows
+ */
+ sort: >(rows: TRowState[]) => TRowState[];
/**
* Current sort direction
*/
@@ -153,11 +133,52 @@ export interface RowState {
rowId: RowId;
}
-export interface TableState = RowState> {
+export interface TablePaginationState {
+ getPageRows: (rows: RowState[]) => RowState[];
+ nextPage: () => void;
+ prevPage: () => void;
+ setPage: (page: number) => void;
+ 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;
+ /**
+ * Original user items
+ */
+ items: TItem[];
/**
* The row data for rendering
*/
- rows: TRowState[];
+ getRows: = RowState>(
+ rowEnhancer?: RowEnhancer,
+ ) => TRowState[];
+ /**
+ * Ref to the table HTML element
+ */
+ tableRef: React.RefObject;
+ /**
+ * Table columns
+ */
+ columns: ColumnDefinition[];
/**
* State and actions to manage row selection
*/
@@ -165,5 +186,16 @@ export interface TableState = RowState<
/**
* State and actions to manage row sorting
*/
- sort: TableSortState;
+ sort: TableSortState;
+ /**
+ * Pagination state
+ */
+ pagination: TablePaginationState;
+
+ /**
+ * Column sizing state
+ */
+ columnSizing: TableColumnSizingState;
}
+
+export type TableStatePlugin = (tableState: TableState) => TableState;
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..e61eb41386619
--- /dev/null
+++ b/packages/react-components/react-table/src/hooks/useColumnSizing.ts
@@ -0,0 +1,39 @@
+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, tableRef]);
+
+ 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/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/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/useSelection.ts b/packages/react-components/react-table/src/hooks/useSelection.ts
index d83d59470d637..4a256a4d8fa36 100644
--- a/packages/react-components/react-table/src/hooks/useSelection.ts
+++ b/packages/react-components/react-table/src/hooks/useSelection.ts
@@ -1,25 +1,33 @@
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 {
+ /**
+ * Can be multi or single select
+ */
selectionMode: SelectionMode;
- items: TItem[];
- getRowId: GetRowIdInternal;
+ /**
+ * 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;
}
-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(),
@@ -38,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,
);
});
@@ -59,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 f4dec5aaf9760..de7a30aa27a72 100644
--- a/packages/react-components/react-table/src/hooks/useSort.ts
+++ b/packages/react-components/react-table/src/hooks/useSort.ts
@@ -1,15 +1,25 @@
+import * as React from 'react';
import { useControllableState } from '@fluentui/react-utilities';
-import type { ColumnDefinition, ColumnId, OnSortChangeCallback, SortState, TableSortStateInternal } from './types';
+import type { ColumnId, OnSortChangeCallback, RowState, SortState, TableSortStateInternal, TableState } from './types';
-interface UseSortOptions {
- columns: ColumnDefinition[];
+interface UseSortOptions {
+ /**
+ * 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;
}
-export function useSort(options: UseSortOptions): TableSortStateInternal {
- const { columns, sortState, defaultSortState, onSortChange } = options;
+export function useSort(tableState: TableState, options: UseSortOptions): TableState {
+ const { columns } = tableState;
+ const { sortState, defaultSortState, onSortChange } = options;
const [sorted, setSorted] = useControllableState({
initialState: {
@@ -42,27 +52,33 @@ 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(
+ (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;
- });
+ const mod = sortDirection === 'ascending' ? 1 : -1;
+ return sortColumnDef.compare(a.item, b.item) * mod;
+ }),
+ [sortDirection, sortColumn, columns],
+ );
const getSortDirection: TableSortStateInternal['getSortDirection'] = (columnId: ColumnId) => {
return sortColumn === columnId ? sortDirection : undefined;
};
return {
- sortColumn,
- sortDirection,
- sort,
- setColumnSort,
- toggleColumnSort,
- getSortDirection,
+ ...tableState,
+ sort: {
+ 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 40be03b1cc9c3..60d1b3e39f7ba 100644
--- a/packages/react-components/react-table/src/hooks/useTable.ts
+++ b/packages/react-components/react-table/src/hooks/useTable.ts
@@ -3,112 +3,65 @@ import type {
UseTableOptions,
TableState,
RowState,
- TableSelectionState,
- TableSortState,
- GetRowIdInternal,
+ TableSelectionStateInternal,
+ TableSortStateInternal,
+ RowEnhancer,
+ TablePaginationState,
+ TableColumnSizingState,
+ TableStatePlugin,
} from './types';
-import { useSelection } from './useSelection';
-import { useSort } from './useSort';
+import { defaultPaginationState } from './usePagination';
-export function useTable = RowState>(
- options: UseTableOptions,
-): TableState {
- const {
- items: baseItems,
- columns,
- getRowId: getUserRowId = () => undefined,
- selectionMode = 'multiselect',
- rowEnhancer = (row: RowState) => row as TRowState,
- defaultSelectedRows,
- selectedRows: userSelectedRows,
- onSelectionChange,
- sortState: userSortState,
- defaultSortState,
- onSortChange,
- } = options;
+const noop: () => void = () => undefined;
+const defaultRowEnhancer: RowEnhancer> = row => row;
+export const defaultSelectionState: TableSelectionStateInternal = {
+ allRowsSelected: false,
+ clearRows: noop,
+ deselectRow: noop,
+ isRowSelected: () => false,
+ selectRow: noop,
+ selectedRows: new Set(),
+ someRowsSelected: false,
+ toggleAllRows: noop,
+ toggleRow: noop,
+};
- const getRowId: GetRowIdInternal = React.useCallback(
- (item: TItem, index: number) => getUserRowId(item) ?? index,
- [getUserRowId],
- );
- const { sortColumn, sortDirection, toggleColumnSort, setColumnSort, getSortDirection, sort } = useSort({
- columns,
- sortState: userSortState,
- defaultSortState,
- onSortChange,
- });
- const sortState: TableSortState = React.useMemo(
- () => ({
- sortColumn,
- sortDirection,
- setColumnSort,
- toggleColumnSort,
- getSortDirection,
- }),
- [sortColumn, sortDirection, setColumnSort, toggleColumnSort, getSortDirection],
- );
+export const defaultSortState: TableSortStateInternal = {
+ getSortDirection: () => 'ascending',
+ setColumnSort: noop,
+ sort: (rows: RowState[]) => [...rows],
+ sortColumn: undefined,
+ sortDirection: 'ascending',
+ toggleColumnSort: noop,
+};
- const {
- isRowSelected,
- toggleRow,
- toggleAllRows,
- clearRows,
- selectedRows,
- allRowsSelected,
- someRowsSelected,
- selectRow,
- deselectRow,
- } = useSelection({
- selectionMode,
- items: baseItems,
- getRowId,
- defaultSelectedItems: defaultSelectedRows,
- selectedItems: userSelectedRows,
- onSelectionChange,
- });
+export const defaultColumnSizingState: TableColumnSizingState = {
+ getColumnWidth: () => 0,
+ getColumnWidths: () => [],
+ getOnMouseDown: () => () => null,
+ getTotalWidth: () => 0,
+ setColumnWidth: () => null,
+};
- const selectionState: TableSelectionState = React.useMemo(
- () => ({
- isRowSelected,
- clearRows,
- deselectRow,
- selectRow,
- toggleAllRows,
- toggleRow,
- selectedRows: Array.from(selectedRows),
- allRowsSelected,
- someRowsSelected,
- }),
- [
- isRowSelected,
- clearRows,
- deselectRow,
- selectRow,
- toggleAllRows,
- toggleRow,
- selectedRows,
- allRowsSelected,
- someRowsSelected,
- ],
- );
+export function useTable(options: UseTableOptions, plugins: TableStatePlugin[]): TableState {
+ const { items, getRowId, columns } = options;
+ const tableRef = React.useRef(null);
- 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],
- );
+ const getRows = >(
+ rowEnhancer = defaultRowEnhancer as RowEnhancer,
+ ) => items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i }));
- return {
- rows,
- selection: selectionState,
- sort: sortState,
+ const initialState = {
+ getRowId,
+ items,
+ columns,
+ getRows,
+ pagination: defaultPaginationState as TablePaginationState,
+ selection: defaultSelectionState,
+ sort: defaultSortState,
+ columnSizing: defaultColumnSizingState,
+ tableRef,
};
+
+ return plugins.reduce((state, plugin) => 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
new file mode 100644
index 0000000000000..f8f06db4667c8
--- /dev/null
+++ b/packages/react-components/react-table/src/stories/Table/ColumnResize.stories.tsx
@@ -0,0 +1,127 @@
+import * as React from 'react';
+import { TableBody, TableCell, TableRow, Table, TableHeader, TableHeaderCell } from '../..';
+import { useTable, ColumnDefinition, useColumnSizing, ColumnId } 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(10).fill(0).map((_, i) => ({ first: i, second: i, third: i, fourth: i }));
+
+export const ColumnResize = () => {
+ const {
+ getRows,
+ columnSizing: { getColumnWidth, setColumnWidth, getOnMouseDown },
+ tableRef,
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [useColumnSizing],
+ );
+
+ const getColumnStyle = (columnId: ColumnId) => ({
+ minWidth: getColumnWidth(columnId),
+ maxWidth: getColumnWidth(columnId),
+ });
+
+ 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/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/MultipleSelect.stories.tsx b/packages/react-components/react-table/src/stories/Table/MultipleSelect.stories.tsx
index 26b03adcca052..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
@@ -19,7 +19,7 @@ import {
TableSelectionCell,
TableCellLayout,
} from '../..';
-import { useTable, ColumnDefinition } from '../../hooks';
+import { useTable, ColumnDefinition, useSelection } from '../../hooks';
import { useNavigationMode } from '../../navigationModes/useNavigationMode';
type FileCell = {
@@ -105,23 +105,31 @@ const columns: ColumnDefinition- [] = [
export const MultipleSelect = () => {
const {
- rows,
- selection: { allRowsSelected, someRowsSelected, toggleAllRows },
- } = useTable({
- columns,
- items,
- defaultSelectedRows: new Set([0, 1]),
- 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),
- }),
- });
+ getRows,
+ selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [
+ tableState =>
+ useSelection(tableState, {
+ selectionMode: 'multiselect',
+ }),
+ ],
+ );
+
+ 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),
+ }));
// 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 dbe12ff30fd27..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
@@ -19,7 +19,7 @@ import {
TableSelectionCell,
TableCellLayout,
} from '../..';
-import { useTable, ColumnDefinition, RowId } from '../../hooks';
+import { useTable, ColumnDefinition, RowId, useSelection } from '../../hooks';
import { useNavigationMode } from '../../navigationModes/useNavigationMode';
type FileCell = {
@@ -104,26 +104,35 @@ const columns: ColumnDefinition
- [] = [
];
export const MultipleSelectControlled = () => {
- const [selectedRows, setSelectedRows] = React.useState(new Set());
+ const [selectedRows, setSelectedRows] = React.useState(new Set(new Set([0, 1])));
const {
- rows,
- selection: { allRowsSelected, someRowsSelected, toggleAllRows },
- } = useTable({
- columns,
- items,
- selectedRows,
- onSelectionChange: setSelectedRows,
- rowEnhancer: (row, { selection }) => ({
- ...row,
- onClick: () => selection.toggleRow(row.rowId),
- onKeyDown: (e: React.KeyboardEvent) => {
- if (e.key === ' ' || e.key === 'Enter') {
- selection.toggleRow(row.rowId);
- }
- },
- selected: selection.isRowSelected(row.rowId),
- }),
- });
+ getRows,
+ selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [
+ tableState =>
+ useSelection(tableState, {
+ selectedItems: selectedRows,
+ onSelectionChange: setSelectedRows,
+ selectionMode: 'multiselect',
+ }),
+ ],
+ );
+
+ 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),
+ }));
// eslint-disable-next-line deprecation/deprecation
const ref = useNavigationMode('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
new file mode 100644
index 0000000000000..db8308855cb51
--- /dev/null
+++ b/packages/react-components/react-table/src/stories/Table/Pagination.stories.tsx
@@ -0,0 +1,97 @@
+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 = () => {
+ const {
+ getRows,
+ sort: { getSortDirection, toggleColumnSort, sort },
+ pagination: { getPageRows, nextPage, prevPage, pageCount, currentPage },
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [
+ tableState => useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }),
+ tableState => usePagination(tableState, { pageSize: 10 }),
+ ],
+ );
+
+ 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 c488d00e1c57d..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
@@ -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';
import { TableCellLayout } from '../../components/TableCellLayout/TableCellLayout';
@@ -96,22 +96,34 @@ const columns: ColumnDefinition- [] = [
];
export const SingleSelect = () => {
- const { rows } = useTable({
- columns,
- items,
- selectionMode: 'single',
- defaultSelectedRows: new Set([1]),
- 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);
- }
- },
- }),
- });
+ const {
+ getRows,
+ selection: { toggleRow, isRowSelected },
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [
+ tableState =>
+ useSelection(tableState, {
+ selectionMode: 'single',
+ defaultSelectedItems: new Set([1]),
+ }),
+ ],
+ );
+
+ 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),
+ }));
+
// 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 95aea34239042..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
@@ -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';
import { TableCellLayout } from '../../components/TableCellLayout/TableCellLayout';
@@ -97,23 +97,34 @@ const columns: ColumnDefinition
- [] = [
export const SingleSelectControlled = () => {
const [selectedRows, setSelectedRows] = React.useState(new Set());
- const { rows } = useTable({
- columns,
- items,
- selectionMode: 'single',
- selectedRows,
- onSelectionChange: setSelectedRows,
- rowEnhancer: (row, { selection }) => ({
- ...row,
- selected: selection.isRowSelected(row.rowId),
- onClick: () => selection.toggleRow(row.rowId),
- onKeyDown: (e: React.KeyboardEvent) => {
- if (e.key === ' ' || e.key === 'Enter') {
- selection.toggleRow(row.rowId);
- }
- },
- }),
- });
+ const {
+ getRows,
+ selection: { toggleRow, isRowSelected },
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [
+ tableState =>
+ useSelection(tableState, {
+ selectionMode: 'single',
+ selectedItems: selectedRows,
+ onSelectionChange: setSelectedRows,
+ }),
+ ],
+ );
+
+ 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),
+ }));
// 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 b8de8eb98eadf..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
@@ -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';
import { TableCellLayout } from '../../components/TableCellLayout/TableCellLayout';
type FileCell = {
@@ -108,9 +108,15 @@ const columns: ColumnDefinition
- [] = [
export const Sort = () => {
const {
- rows,
- sort: { getSortDirection, toggleColumnSort },
- } = useTable({ columns, items, defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } });
+ getRows,
+ sort: { getSortDirection, toggleColumnSort, sort },
+ } = useTable(
+ {
+ columns,
+ items,
+ },
+ [tableState => useSort(tableState, { defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } })],
+ );
const headerSortProps = (columnId: ColumnId) => ({
onClick: () => {
@@ -130,7 +136,7 @@ export const Sort = () => {
- {rows.map(({ item }) => (
+ {sort(getRows()).map(({ item }) => (
{item.file.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 22ca7cdc51aa8..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
@@ -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';
import { TableCellLayout } from '../../components/TableCellLayout/TableCellLayout';
type FileCell = {
@@ -113,9 +113,9 @@ export const SortControlled = () => {
});
const {
- rows,
- sort: { getSortDirection, toggleColumnSort },
- } = useTable({ columns, items, sortState, onSortChange: setSortState });
+ getRows,
+ sort: { getSortDirection, toggleColumnSort, sort },
+ } = useTable({ columns, items }, [tableState => useSort(tableState, { sortState, onSortChange: setSortState })]);
const headerSortProps = (columnId: ColumnId) => ({
onClick: () => toggleColumnSort(columnId),
@@ -133,7 +133,7 @@ export const SortControlled = () => {
- {rows.map(({ item }) => (
+ {sort(getRows()).map(({ item }) => (
{item.file.label}
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..a000708ebdc79
--- /dev/null
+++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactVirtual.stories.tsx
@@ -0,0 +1,147 @@
+import * as React from 'react';
+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
+ const parentRef = React.useRef(null);
+
+ // The virtualizer
+ const rowVirtualizer = useVirtualizer({
+ count: 1000,
+ getScrollElement: () => parentRef.current,
+ 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
+
+
+
+ {/* 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 => {
+ 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
new file mode 100644
index 0000000000000..c64fd9b403821
--- /dev/null
+++ b/packages/react-components/react-table/src/stories/Table/VirtualizationReactWindow.stories.tsx
@@ -0,0 +1,115 @@
+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';
+import { useSelection } from '../../hooks/useSelection';
+import { useSort } from '../../hooks/useSort';
+import { TableSelectionCell } from '../../components/TableSelectionCell/TableSelectionCell';
+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 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,
+ items,
+ },
+ [
+ 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
+
+
+
+
+ {Row}
+
+
+
+ );
+};
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 9e4dced7261fb..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
@@ -16,6 +16,12 @@ 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 { 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 df23a510368ca..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"
@@ -4956,6 +4961,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 +6038,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 +10031,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"
@@ -13243,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"
@@ -15947,6 +15995,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 +18698,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"
@@ -18821,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=
@@ -22294,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"
@@ -23277,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"
@@ -26553,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"