556 lines
16 KiB
Markdown
556 lines
16 KiB
Markdown
|
# @fluentui/react-table
|
||
|
|
||
|
This package contains two high level components and their subcomponents.
|
||
|
|
||
|
## Table
|
||
|
|
||
|
This component is considered **low-level** and should be used when there is a need for more **customization** and
|
||
|
support for **non-standard features**. Please check out the **DataGrid component**
|
||
|
if you don't need lots of customization and rely on common features. There is less work involved and you will benefit
|
||
|
from first class Microsoft design and accessibility support.
|
||
|
|
||
|
A Table displays sets of two-dimensional data. The primitive components can be fully customized to support different
|
||
|
feature sets. The library provides a default `useTableFeatures` hook that handles the business logic and state management of common
|
||
|
features. There is no obligation to use our hook with these components, we've created it for convenience.
|
||
|
|
||
|
The `useTableFeatures` hook was designed with feature composition in mind. This means that they are composed using
|
||
|
plugins hooks such as `useTableSort` and `useTableSelection` as a part of `useTableFeatures`. This means
|
||
|
that as the feature set expands, users will not need to pay the bundle size price for features that they do not intend
|
||
|
to use. Please consult the usage examples below for more details.
|
||
|
|
||
|
### Example without interactive features
|
||
|
|
||
|
```tsx
|
||
|
import * as React from 'react';
|
||
|
import {
|
||
|
FolderRegular,
|
||
|
EditRegular,
|
||
|
OpenRegular,
|
||
|
DocumentRegular,
|
||
|
PeopleRegular,
|
||
|
DocumentPdfRegular,
|
||
|
VideoRegular,
|
||
|
} from '@fluentui/react-icons';
|
||
|
import {
|
||
|
TableBody,
|
||
|
TableCell,
|
||
|
TableRow,
|
||
|
Table,
|
||
|
TableHeader,
|
||
|
TableHeaderCell,
|
||
|
TableCellLayout,
|
||
|
PresenceBadgeStatus,
|
||
|
Avatar,
|
||
|
} from '@fluentui/react-components';
|
||
|
|
||
|
const items = [
|
||
|
{
|
||
|
file: { label: 'Meeting notes', icon: <DocumentRegular /> },
|
||
|
author: { label: 'Max Mustermann', status: 'available' },
|
||
|
lastUpdated: { label: '7h ago', timestamp: 1 },
|
||
|
lastUpdate: {
|
||
|
label: 'You edited this',
|
||
|
icon: <EditRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Thursday presentation', icon: <FolderRegular /> },
|
||
|
author: { label: 'Erika Mustermann', status: 'busy' },
|
||
|
lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 },
|
||
|
lastUpdate: {
|
||
|
label: 'You recently opened this',
|
||
|
icon: <OpenRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Training recording', icon: <VideoRegular /> },
|
||
|
author: { label: 'John Doe', status: 'away' },
|
||
|
lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 },
|
||
|
lastUpdate: {
|
||
|
label: 'You recently opened this',
|
||
|
icon: <OpenRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Purchase order', icon: <DocumentPdfRegular /> },
|
||
|
author: { label: 'Jane Doe', status: 'offline' },
|
||
|
lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 3 },
|
||
|
lastUpdate: {
|
||
|
label: 'You shared this in a Teams chat',
|
||
|
icon: <PeopleRegular />,
|
||
|
},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const columns = [
|
||
|
{ columnKey: 'file', label: 'File' },
|
||
|
{ columnKey: 'author', label: 'Author' },
|
||
|
{ columnKey: 'lastUpdated', label: 'Last updated' },
|
||
|
{ columnKey: 'lastUpdate', label: 'Last update' },
|
||
|
];
|
||
|
|
||
|
export const Default = () => {
|
||
|
return (
|
||
|
<Table arial-label="Default table">
|
||
|
<TableHeader>
|
||
|
<TableRow>
|
||
|
{columns.map(column => (
|
||
|
<TableHeaderCell key={column.columnKey}>{column.label}</TableHeaderCell>
|
||
|
))}
|
||
|
</TableRow>
|
||
|
</TableHeader>
|
||
|
<TableBody>
|
||
|
{items.map(item => (
|
||
|
<TableRow key={item.file.label}>
|
||
|
<TableCell>
|
||
|
<TableCellLayout media={item.file.icon}>{item.file.label}</TableCellLayout>
|
||
|
</TableCell>
|
||
|
<TableCell>
|
||
|
<TableCellLayout
|
||
|
media={
|
||
|
<Avatar
|
||
|
aria-label={item.author.label}
|
||
|
name={item.author.label}
|
||
|
badge={{ status: item.author.status as PresenceBadgeStatus }}
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
{item.author.label}
|
||
|
</TableCellLayout>
|
||
|
</TableCell>
|
||
|
<TableCell>{item.lastUpdated.label}</TableCell>
|
||
|
<TableCell>
|
||
|
<TableCellLayout media={item.lastUpdate.icon}>{item.lastUpdate.label}</TableCellLayout>
|
||
|
</TableCell>
|
||
|
</TableRow>
|
||
|
))}
|
||
|
</TableBody>
|
||
|
</Table>
|
||
|
);
|
||
|
};
|
||
|
```
|
||
|
|
||
|
### Example with interactive features
|
||
|
|
||
|
```tsx
|
||
|
import * as React from 'react';
|
||
|
import {
|
||
|
FolderRegular,
|
||
|
EditRegular,
|
||
|
OpenRegular,
|
||
|
DocumentRegular,
|
||
|
PeopleRegular,
|
||
|
DocumentPdfRegular,
|
||
|
VideoRegular,
|
||
|
} from '@fluentui/react-icons';
|
||
|
import {
|
||
|
TableBody,
|
||
|
TableCell,
|
||
|
TableRow,
|
||
|
Table,
|
||
|
TableHeader,
|
||
|
TableHeaderCell,
|
||
|
TableSelectionCell,
|
||
|
TableCellLayout,
|
||
|
useTableFeatures,
|
||
|
TableColumnDefinition,
|
||
|
useTableSelection,
|
||
|
useTableSort,
|
||
|
createTableColumn,
|
||
|
TableColumnId,
|
||
|
PresenceBadgeStatus,
|
||
|
Avatar,
|
||
|
useArrowNavigationGroup,
|
||
|
} from '@fluentui/react-components';
|
||
|
|
||
|
type FileCell = {
|
||
|
label: string;
|
||
|
icon: JSX.Element;
|
||
|
};
|
||
|
|
||
|
type LastUpdatedCell = {
|
||
|
label: string;
|
||
|
timestamp: number;
|
||
|
};
|
||
|
|
||
|
type LastUpdateCell = {
|
||
|
label: string;
|
||
|
icon: JSX.Element;
|
||
|
};
|
||
|
|
||
|
type AuthorCell = {
|
||
|
label: string;
|
||
|
status: PresenceBadgeStatus;
|
||
|
};
|
||
|
|
||
|
type Item = {
|
||
|
file: FileCell;
|
||
|
author: AuthorCell;
|
||
|
lastUpdated: LastUpdatedCell;
|
||
|
lastUpdate: LastUpdateCell;
|
||
|
};
|
||
|
|
||
|
const items: Item[] = [
|
||
|
{
|
||
|
file: { label: 'Meeting notes', icon: <DocumentRegular /> },
|
||
|
author: { label: 'Max Mustermann', status: 'available' },
|
||
|
lastUpdated: { label: '7h ago', timestamp: 3 },
|
||
|
lastUpdate: {
|
||
|
label: 'You edited this',
|
||
|
icon: <EditRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Thursday presentation', icon: <FolderRegular /> },
|
||
|
author: { label: 'Erika Mustermann', status: 'busy' },
|
||
|
lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 },
|
||
|
lastUpdate: {
|
||
|
label: 'You recently opened this',
|
||
|
icon: <OpenRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Training recording', icon: <VideoRegular /> },
|
||
|
author: { label: 'John Doe', status: 'away' },
|
||
|
lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 },
|
||
|
lastUpdate: {
|
||
|
label: 'You recently opened this',
|
||
|
icon: <OpenRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Purchase order', icon: <DocumentPdfRegular /> },
|
||
|
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: <PeopleRegular />,
|
||
|
},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const columns: TableColumnDefinition<Item>[] = [
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'file',
|
||
|
compare: (a, b) => {
|
||
|
return a.file.label.localeCompare(b.file.label);
|
||
|
},
|
||
|
}),
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'author',
|
||
|
compare: (a, b) => {
|
||
|
return a.author.label.localeCompare(b.author.label);
|
||
|
},
|
||
|
}),
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'lastUpdated',
|
||
|
compare: (a, b) => {
|
||
|
return a.lastUpdated.timestamp - b.lastUpdated.timestamp;
|
||
|
},
|
||
|
}),
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'lastUpdate',
|
||
|
compare: (a, b) => {
|
||
|
return a.lastUpdate.label.localeCompare(b.lastUpdate.label);
|
||
|
},
|
||
|
}),
|
||
|
];
|
||
|
|
||
|
export const DataGrid = () => {
|
||
|
const {
|
||
|
getRows,
|
||
|
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||
|
sort: { getSortDirection, toggleColumnSort, sort },
|
||
|
} = useTableFeatures(
|
||
|
{
|
||
|
columns,
|
||
|
items,
|
||
|
},
|
||
|
[
|
||
|
useTableSelection({
|
||
|
selectionMode: 'multiselect',
|
||
|
defaultSelectedItems: new Set([0, 1]),
|
||
|
}),
|
||
|
useTableSort({ defaultSortState: { sortColumn: 'file', sortDirection: 'ascending' } }),
|
||
|
],
|
||
|
);
|
||
|
|
||
|
const rows = sort(
|
||
|
getRows(row => {
|
||
|
const selected = isRowSelected(row.rowId);
|
||
|
return {
|
||
|
...row,
|
||
|
onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId),
|
||
|
onKeyDown: (e: React.KeyboardEvent) => {
|
||
|
if (e.key === ' ') {
|
||
|
e.preventDefault();
|
||
|
toggleRow(e, row.rowId);
|
||
|
}
|
||
|
},
|
||
|
selected,
|
||
|
appearance: selected ? ('brand' as const) : ('none' as const),
|
||
|
};
|
||
|
}),
|
||
|
);
|
||
|
|
||
|
const headerSortProps = (columnId: TableColumnId) => ({
|
||
|
onClick: (e: React.MouseEvent) => {
|
||
|
toggleColumnSort(e, columnId);
|
||
|
},
|
||
|
sortDirection: getSortDirection(columnId),
|
||
|
});
|
||
|
|
||
|
const keyboardNavAttr = useArrowNavigationGroup({ axis: 'grid' });
|
||
|
|
||
|
return (
|
||
|
<Table {...keyboardNavAttr} role="grid" sortable aria-label="DataGrid implementation with Table primitives">
|
||
|
<TableHeader>
|
||
|
<TableRow>
|
||
|
<TableSelectionCell
|
||
|
checked={allRowsSelected ? true : someRowsSelected ? 'mixed' : false}
|
||
|
aria-checked={allRowsSelected ? true : someRowsSelected ? 'mixed' : false}
|
||
|
role="checkbox"
|
||
|
onClick={toggleAllRows}
|
||
|
checkboxIndicator={{ 'aria-label': 'Select all rows ' }}
|
||
|
/>
|
||
|
<TableHeaderCell {...headerSortProps('file')}>File</TableHeaderCell>
|
||
|
<TableHeaderCell {...headerSortProps('author')}>Author</TableHeaderCell>
|
||
|
<TableHeaderCell {...headerSortProps('lastUpdated')}>Last updated</TableHeaderCell>
|
||
|
<TableHeaderCell {...headerSortProps('lastUpdate')}>Last update</TableHeaderCell>
|
||
|
</TableRow>
|
||
|
</TableHeader>
|
||
|
<TableBody>
|
||
|
{rows.map(({ item, selected, onClick, onKeyDown, appearance }) => (
|
||
|
<TableRow
|
||
|
key={item.file.label}
|
||
|
onClick={onClick}
|
||
|
onKeyDown={onKeyDown}
|
||
|
aria-selected={selected}
|
||
|
appearance={appearance}
|
||
|
>
|
||
|
<TableSelectionCell
|
||
|
role="gridcell"
|
||
|
aria-selected={selected}
|
||
|
checked={selected}
|
||
|
checkboxIndicator={{ 'aria-label': 'Select row' }}
|
||
|
/>
|
||
|
<TableCell tabIndex={0} role="gridcell" aria-selected={selected}>
|
||
|
<TableCellLayout media={item.file.icon}>{item.file.label}</TableCellLayout>
|
||
|
</TableCell>
|
||
|
<TableCell tabIndex={0} role="gridcell">
|
||
|
<TableCellLayout
|
||
|
media={
|
||
|
<Avatar
|
||
|
aria-label={item.author.label}
|
||
|
name={item.author.label}
|
||
|
badge={{ status: item.author.status }}
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
{item.author.label}
|
||
|
</TableCellLayout>
|
||
|
</TableCell>
|
||
|
<TableCell tabIndex={0} role="gridcell">
|
||
|
{item.lastUpdated.label}
|
||
|
</TableCell>
|
||
|
<TableCell tabIndex={0} role="gridcell">
|
||
|
<TableCellLayout media={item.lastUpdate.icon}>{item.lastUpdate.label}</TableCellLayout>
|
||
|
</TableCell>
|
||
|
</TableRow>
|
||
|
))}
|
||
|
</TableBody>
|
||
|
</Table>
|
||
|
);
|
||
|
};
|
||
|
```
|
||
|
|
||
|
## DataGrid
|
||
|
|
||
|
This component is a higher level extension of the `Table` primitive components and the `useTableFeatures` hook.
|
||
|
`DataGrid` is a feature-rich component that uses `useTableFeatures` internally,
|
||
|
so there should always be full feature parity with what can be
|
||
|
achieved with primitives. This component is **opinionated** and this is intentional. If the desired scenario can
|
||
|
be achieved easily and does not vary too much from documented examples, it can be very convenient. If the desired
|
||
|
scenario varies a lot from the documented examples please use the `Table` components with `useTableFeatures` (or
|
||
|
another state management solution of choice).
|
||
|
|
||
|
### Example usage
|
||
|
|
||
|
```tsx
|
||
|
import * as React from 'react';
|
||
|
import {
|
||
|
FolderRegular,
|
||
|
EditRegular,
|
||
|
OpenRegular,
|
||
|
DocumentRegular,
|
||
|
PeopleRegular,
|
||
|
DocumentPdfRegular,
|
||
|
VideoRegular,
|
||
|
} from '@fluentui/react-icons';
|
||
|
import {
|
||
|
PresenceBadgeStatus,
|
||
|
Avatar,
|
||
|
DataGridBody,
|
||
|
DataGridRow,
|
||
|
DataGrid,
|
||
|
DataGridHeader,
|
||
|
DataGridHeaderCell,
|
||
|
DataGridCell,
|
||
|
TableCellLayout,
|
||
|
TableColumnDefinition,
|
||
|
createTableColumn,
|
||
|
} from '@fluentui/react-components';
|
||
|
|
||
|
type FileCell = {
|
||
|
label: string;
|
||
|
icon: JSX.Element;
|
||
|
};
|
||
|
|
||
|
type LastUpdatedCell = {
|
||
|
label: string;
|
||
|
timestamp: number;
|
||
|
};
|
||
|
|
||
|
type LastUpdateCell = {
|
||
|
label: string;
|
||
|
icon: JSX.Element;
|
||
|
};
|
||
|
|
||
|
type AuthorCell = {
|
||
|
label: string;
|
||
|
status: PresenceBadgeStatus;
|
||
|
};
|
||
|
|
||
|
type Item = {
|
||
|
file: FileCell;
|
||
|
author: AuthorCell;
|
||
|
lastUpdated: LastUpdatedCell;
|
||
|
lastUpdate: LastUpdateCell;
|
||
|
};
|
||
|
|
||
|
const items: Item[] = [
|
||
|
{
|
||
|
file: { label: 'Meeting notes', icon: <DocumentRegular /> },
|
||
|
author: { label: 'Max Mustermann', status: 'available' },
|
||
|
lastUpdated: { label: '7h ago', timestamp: 1 },
|
||
|
lastUpdate: {
|
||
|
label: 'You edited this',
|
||
|
icon: <EditRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Thursday presentation', icon: <FolderRegular /> },
|
||
|
author: { label: 'Erika Mustermann', status: 'busy' },
|
||
|
lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 },
|
||
|
lastUpdate: {
|
||
|
label: 'You recently opened this',
|
||
|
icon: <OpenRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Training recording', icon: <VideoRegular /> },
|
||
|
author: { label: 'John Doe', status: 'away' },
|
||
|
lastUpdated: { label: 'Yesterday at 1:45 PM', timestamp: 2 },
|
||
|
lastUpdate: {
|
||
|
label: 'You recently opened this',
|
||
|
icon: <OpenRegular />,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
file: { label: 'Purchase order', icon: <DocumentPdfRegular /> },
|
||
|
author: { label: 'Jane Doe', status: 'offline' },
|
||
|
lastUpdated: { label: 'Tue at 9:30 AM', timestamp: 3 },
|
||
|
lastUpdate: {
|
||
|
label: 'You shared this in a Teams chat',
|
||
|
icon: <PeopleRegular />,
|
||
|
},
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const columns: TableColumnDefinition<Item>[] = [
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'file',
|
||
|
compare: (a, b) => {
|
||
|
return a.file.label.localeCompare(b.file.label);
|
||
|
},
|
||
|
renderHeaderCell: () => {
|
||
|
return 'File';
|
||
|
},
|
||
|
renderCell: item => {
|
||
|
return <TableCellLayout media={item.file.icon}>{item.file.label}</TableCellLayout>;
|
||
|
},
|
||
|
}),
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'author',
|
||
|
compare: (a, b) => {
|
||
|
return a.author.label.localeCompare(b.author.label);
|
||
|
},
|
||
|
renderHeaderCell: () => {
|
||
|
return 'Author';
|
||
|
},
|
||
|
renderCell: item => {
|
||
|
return (
|
||
|
<TableCellLayout
|
||
|
media={
|
||
|
<Avatar aria-label={item.author.label} name={item.author.label} badge={{ status: item.author.status }} />
|
||
|
}
|
||
|
>
|
||
|
{item.author.label}
|
||
|
</TableCellLayout>
|
||
|
);
|
||
|
},
|
||
|
}),
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'lastUpdated',
|
||
|
compare: (a, b) => {
|
||
|
return a.lastUpdated.timestamp - b.lastUpdated.timestamp;
|
||
|
},
|
||
|
renderHeaderCell: () => {
|
||
|
return 'Last updated';
|
||
|
},
|
||
|
|
||
|
renderCell: item => {
|
||
|
return item.lastUpdated.label;
|
||
|
},
|
||
|
}),
|
||
|
createTableColumn<Item>({
|
||
|
columnId: 'lastUpdate',
|
||
|
compare: (a, b) => {
|
||
|
return a.lastUpdate.label.localeCompare(b.lastUpdate.label);
|
||
|
},
|
||
|
renderHeaderCell: () => {
|
||
|
return 'Last update';
|
||
|
},
|
||
|
renderCell: item => {
|
||
|
return <TableCellLayout media={item.lastUpdate.icon}>{item.lastUpdate.label}</TableCellLayout>;
|
||
|
},
|
||
|
}),
|
||
|
];
|
||
|
|
||
|
export const Default = () => {
|
||
|
return (
|
||
|
<DataGrid
|
||
|
items={items}
|
||
|
columns={columns}
|
||
|
sortable
|
||
|
selectionMode="multiselect"
|
||
|
getRowId={item => item.file.label}
|
||
|
onSelectionChange={(e, data) => console.log(data)}
|
||
|
>
|
||
|
<DataGridHeader>
|
||
|
<DataGridRow selectionCell={{ 'aria-label': 'Select all rows' }}>
|
||
|
{({ renderHeaderCell }) => <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>}
|
||
|
</DataGridRow>
|
||
|
</DataGridHeader>
|
||
|
<DataGridBody<Item>>
|
||
|
{({ item, rowId }) => (
|
||
|
<DataGridRow<Item> key={rowId} selectionCell={{ 'aria-label': 'Select row' }}>
|
||
|
{({ renderCell }) => <DataGridCell>{renderCell(item)}</DataGridCell>}
|
||
|
</DataGridRow>
|
||
|
)}
|
||
|
</DataGridBody>
|
||
|
</DataGrid>
|
||
|
);
|
||
|
};
|
||
|
```
|