docs(table): rewrite Table component documentation
Comprehensive rewrite of the Table component documentation including: - Updated prop definitions with JSX descriptions - New interactive examples for sorting, search, pagination, selection - Expanded code snippets covering all major use cases - Added custom row and primitives examples - Improved MDX structure and styling Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -1,31 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableRoot,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
Column,
|
||||
Row,
|
||||
CellProfile,
|
||||
CellText,
|
||||
type ColumnConfig,
|
||||
useTable,
|
||||
type ColumnConfig,
|
||||
} from '../../../../../packages/ui/src/components/Table';
|
||||
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
|
||||
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
|
||||
import { RadioGroup } from '../../../../../packages/ui/src/components/RadioGroup/RadioGroup';
|
||||
import { Radio } from '../../../../../packages/ui/src/components/RadioGroup';
|
||||
import { data as data4 } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
|
||||
import { useState } from 'react';
|
||||
import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
|
||||
import {
|
||||
selectionData,
|
||||
selectionColumns,
|
||||
} from '../../../../../packages/ui/src/components/Table/stories/utils';
|
||||
RadioGroup,
|
||||
Radio,
|
||||
} from '../../../../../packages/ui/src/components/RadioGroup';
|
||||
import { data as rockBandData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
|
||||
import { data as catalogData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data1';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
type Data4Item = (typeof data4)[0];
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
const columns: ColumnConfig<Data4Item>[] = [
|
||||
type RockBandItem = (typeof rockBandData)[0];
|
||||
type CatalogItem = (typeof catalogData)[0];
|
||||
|
||||
// =============================================================================
|
||||
// Hero Example
|
||||
// =============================================================================
|
||||
|
||||
const heroColumns: ColumnConfig<RockBandItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Band name',
|
||||
isRowHeader: true,
|
||||
defaultWidth: '3fr',
|
||||
cell: item => (
|
||||
<CellProfile name={item.name} src={item.image} href={item.website} />
|
||||
),
|
||||
@@ -33,112 +48,183 @@ const columns: ColumnConfig<Data4Item>[] = [
|
||||
{
|
||||
id: 'genre',
|
||||
label: 'Genre',
|
||||
defaultWidth: '3fr',
|
||||
cell: item => <CellText title={item.genre} />,
|
||||
},
|
||||
{
|
||||
id: 'yearFormed',
|
||||
label: 'Year formed',
|
||||
defaultWidth: '1fr',
|
||||
cell: item => <CellText title={item.yearFormed.toString()} />,
|
||||
},
|
||||
{
|
||||
id: 'albums',
|
||||
label: 'Albums',
|
||||
defaultWidth: '1fr',
|
||||
cell: item => <CellText title={item.albums.toString()} />,
|
||||
},
|
||||
];
|
||||
|
||||
export const TableRockBand = () => {
|
||||
export function HeroExample() {
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data4,
|
||||
getData: () => rockBandData,
|
||||
paginationOptions: { pageSize: 5 },
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Table columnConfig={columns} {...tableProps} />
|
||||
<Table columnConfig={heroColumns} {...tableProps} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const SelectionToggleWithActions = () => {
|
||||
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
|
||||
new Set(),
|
||||
);
|
||||
// =============================================================================
|
||||
// Sorting Example
|
||||
// =============================================================================
|
||||
|
||||
const sortingColumns: ColumnConfig<CatalogItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.name} />,
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.owner.name} />,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
label: 'Type',
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.type} />,
|
||||
},
|
||||
{
|
||||
id: 'lifecycle',
|
||||
label: 'Lifecycle',
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.lifecycle} />,
|
||||
},
|
||||
];
|
||||
|
||||
export function SortingExample() {
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => selectionData,
|
||||
paginationOptions: { pageSize: 10 },
|
||||
getData: () => catalogData,
|
||||
paginationOptions: { pageSize: 5 },
|
||||
initialSort: { column: 'name', direction: 'ascending' },
|
||||
sortFn: (items, { column, direction }) => {
|
||||
return [...items].sort((a, b) => {
|
||||
let aVal: string;
|
||||
let bVal: string;
|
||||
if (column === 'owner') {
|
||||
aVal = a.owner.name;
|
||||
bVal = b.owner.name;
|
||||
} else {
|
||||
aVal = String(a[column as keyof CatalogItem]);
|
||||
bVal = String(b[column as keyof CatalogItem]);
|
||||
}
|
||||
const cmp = aVal.localeCompare(bVal);
|
||||
return direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Table
|
||||
{...tableProps}
|
||||
columnConfig={selectionColumns}
|
||||
selection={{
|
||||
mode: 'multiple',
|
||||
behavior: 'toggle',
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
rowConfig={{ onClick: item => alert(`Clicked: ${item.name}`) }}
|
||||
/>
|
||||
<Table columnConfig={sortingColumns} {...tableProps} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const SelectionModePlayground = () => {
|
||||
const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
|
||||
'multiple',
|
||||
);
|
||||
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
|
||||
new Set(),
|
||||
);
|
||||
// =============================================================================
|
||||
// Search Example
|
||||
// =============================================================================
|
||||
|
||||
const { tableProps } = useTable({
|
||||
const searchColumns: ColumnConfig<CatalogItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
cell: item => <CellText title={item.name} description={item.description} />,
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
cell: item => <CellText title={item.owner.name} />,
|
||||
},
|
||||
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
|
||||
];
|
||||
|
||||
export function SearchExample() {
|
||||
const { tableProps, search } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => selectionData,
|
||||
paginationOptions: { pageSize: 10 },
|
||||
getData: () => catalogData,
|
||||
paginationOptions: { pageSize: 5 },
|
||||
searchFn: (items, query) => {
|
||||
const lowerQuery = query.toLocaleLowerCase('en-US');
|
||||
return items.filter(
|
||||
item =>
|
||||
item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
|
||||
item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
|
||||
item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Flex direction="column" gap="8">
|
||||
<Table
|
||||
{...tableProps}
|
||||
columnConfig={selectionColumns}
|
||||
selection={{
|
||||
mode: selectionMode,
|
||||
behavior: 'toggle',
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
<Flex direction="column" gap="4">
|
||||
<SearchField
|
||||
aria-label="Search"
|
||||
placeholder="Search..."
|
||||
value={search.value}
|
||||
onChange={search.onChange}
|
||||
style={{ maxWidth: 300 }}
|
||||
/>
|
||||
<Table
|
||||
columnConfig={searchColumns}
|
||||
emptyState={
|
||||
search.value ? (
|
||||
<Text>No results found for "{search.value}"</Text>
|
||||
) : (
|
||||
<Text>No data available</Text>
|
||||
)
|
||||
}
|
||||
{...tableProps}
|
||||
/>
|
||||
<div>
|
||||
<Text as="h4" style={{ marginBottom: 'var(--bui-space-2)' }}>
|
||||
Selection mode:
|
||||
</Text>
|
||||
<RadioGroup
|
||||
aria-label="Selection mode"
|
||||
orientation="horizontal"
|
||||
value={selectionMode}
|
||||
onChange={value => {
|
||||
setSelectionMode(value as 'single' | 'multiple');
|
||||
setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
<Radio value="single">single</Radio>
|
||||
<Radio value="multiple">multiple</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Flex>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const SelectionBehaviorPlayground = () => {
|
||||
// =============================================================================
|
||||
// Selection Example
|
||||
// =============================================================================
|
||||
|
||||
const selectionColumns: ColumnConfig<CatalogItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
cell: item => <CellText title={item.name} />,
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
cell: item => <CellText title={item.owner.name} />,
|
||||
},
|
||||
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
|
||||
];
|
||||
|
||||
export function SelectionExample() {
|
||||
const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
|
||||
'multiple',
|
||||
);
|
||||
const [selectionBehavior, setSelectionBehavior] = useState<
|
||||
'toggle' | 'replace'
|
||||
>('toggle');
|
||||
@@ -148,41 +234,321 @@ export const SelectionBehaviorPlayground = () => {
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => selectionData,
|
||||
paginationOptions: { pageSize: 10 },
|
||||
getData: () => catalogData.slice(0, 5),
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Flex direction="column" gap="8">
|
||||
<Flex direction="column" gap="4">
|
||||
<Table
|
||||
{...tableProps}
|
||||
columnConfig={selectionColumns}
|
||||
selection={{
|
||||
mode: 'multiple',
|
||||
mode: selectionMode,
|
||||
behavior: selectionBehavior,
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
{...tableProps}
|
||||
/>
|
||||
<div>
|
||||
<Text as="h4" style={{ marginBottom: 'var(--bui-space-2)' }}>
|
||||
Selection behavior:
|
||||
</Text>
|
||||
<RadioGroup
|
||||
aria-label="Selection behavior"
|
||||
orientation="horizontal"
|
||||
value={selectionBehavior}
|
||||
onChange={value => {
|
||||
setSelectionBehavior(value as 'toggle' | 'replace');
|
||||
setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
<Radio value="toggle">toggle</Radio>
|
||||
<Radio value="replace">replace</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Flex gap="8">
|
||||
<div>
|
||||
<Text as="p" style={{ marginBottom: 'var(--bui-space-2)' }}>
|
||||
Selection mode:
|
||||
</Text>
|
||||
<RadioGroup
|
||||
aria-label="Selection mode"
|
||||
orientation="horizontal"
|
||||
value={selectionMode}
|
||||
onChange={value => {
|
||||
setSelectionMode(value as 'single' | 'multiple');
|
||||
setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
<Radio value="single">single</Radio>
|
||||
<Radio value="multiple">multiple</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div>
|
||||
<Text as="p" style={{ marginBottom: 'var(--bui-space-2)' }}>
|
||||
Selection behavior:
|
||||
</Text>
|
||||
<RadioGroup
|
||||
aria-label="Selection behavior"
|
||||
orientation="horizontal"
|
||||
value={selectionBehavior}
|
||||
onChange={value => {
|
||||
setSelectionBehavior(value as 'toggle' | 'replace');
|
||||
setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
<Radio value="toggle">toggle</Radio>
|
||||
<Radio value="replace">replace</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Row Actions Example
|
||||
// =============================================================================
|
||||
|
||||
export function RowActionsExample() {
|
||||
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => catalogData.slice(0, 5),
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Table
|
||||
columnConfig={selectionColumns}
|
||||
rowConfig={{
|
||||
onClick: item => alert(`Clicked: ${item.name}`),
|
||||
}}
|
||||
selection={{
|
||||
mode: 'multiple',
|
||||
behavior: 'toggle',
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
{...tableProps}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Empty State Example
|
||||
// =============================================================================
|
||||
|
||||
const emptyStateColumns: ColumnConfig<CatalogItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
cell: item => <CellText title={item.name} />,
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
cell: item => <CellText title={item.owner.name} />,
|
||||
},
|
||||
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
|
||||
];
|
||||
|
||||
export function EmptyStateExample() {
|
||||
const emptyData: CatalogItem[] = [];
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => emptyData,
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Table
|
||||
columnConfig={emptyStateColumns}
|
||||
emptyState={<Text>No items yet. Create one to get started.</Text>}
|
||||
{...tableProps}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Combined Example
|
||||
// =============================================================================
|
||||
|
||||
const combinedColumns: ColumnConfig<CatalogItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.name} description={item.description} />,
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.owner.name} />,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
label: 'Type',
|
||||
isSortable: true,
|
||||
cell: item => <CellText title={item.type} />,
|
||||
},
|
||||
];
|
||||
|
||||
export function CombinedExample() {
|
||||
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const { tableProps, search } = useTable({
|
||||
mode: 'offset',
|
||||
getData: async ({ offset, pageSize, search: query, sort }) => {
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Filter data
|
||||
let filtered = catalogData;
|
||||
if (query) {
|
||||
const lowerQuery = query.toLocaleLowerCase('en-US');
|
||||
filtered = filtered.filter(
|
||||
item =>
|
||||
item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
|
||||
item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
|
||||
item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort data
|
||||
if (sort?.column) {
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
let aVal: string;
|
||||
let bVal: string;
|
||||
if (sort.column === 'owner') {
|
||||
aVal = a.owner.name;
|
||||
bVal = b.owner.name;
|
||||
} else {
|
||||
aVal = String(a[sort.column as keyof CatalogItem]);
|
||||
bVal = String(b[sort.column as keyof CatalogItem]);
|
||||
}
|
||||
const cmp = aVal.localeCompare(bVal);
|
||||
return sort.direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
}
|
||||
|
||||
// Paginate
|
||||
const data = filtered.slice(offset, offset + pageSize);
|
||||
|
||||
return { data, totalCount: filtered.length };
|
||||
},
|
||||
paginationOptions: { pageSize: 5, pageSizeOptions: [5, 10, 20] },
|
||||
initialSort: { column: 'name', direction: 'ascending' },
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Flex direction="column" gap="4">
|
||||
<SearchField
|
||||
aria-label="Search"
|
||||
placeholder="Search..."
|
||||
value={search.value}
|
||||
onChange={search.onChange}
|
||||
style={{ maxWidth: 300 }}
|
||||
/>
|
||||
<Table
|
||||
columnConfig={combinedColumns}
|
||||
rowConfig={{
|
||||
onClick: item => alert(`Clicked: ${item.name}`),
|
||||
}}
|
||||
selection={{
|
||||
mode: 'multiple',
|
||||
behavior: 'toggle',
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
emptyState={
|
||||
search.value ? (
|
||||
<Text>No results match your search</Text>
|
||||
) : (
|
||||
<Text>No items available</Text>
|
||||
)
|
||||
}
|
||||
{...tableProps}
|
||||
/>
|
||||
</Flex>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Custom Row Example
|
||||
// =============================================================================
|
||||
|
||||
const customRowColumns: ColumnConfig<CatalogItem>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
cell: item => <CellText title={item.name} />,
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
label: 'Owner',
|
||||
cell: item => <CellText title={item.owner.name} />,
|
||||
},
|
||||
{
|
||||
id: 'lifecycle',
|
||||
label: 'Lifecycle',
|
||||
cell: item => <CellText title={item.lifecycle} />,
|
||||
},
|
||||
];
|
||||
|
||||
export function CustomRowExample() {
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => catalogData.slice(0, 5),
|
||||
});
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Table
|
||||
columnConfig={customRowColumns}
|
||||
rowConfig={({ item }) => (
|
||||
<Row
|
||||
id={String(item.id)}
|
||||
columns={customRowColumns}
|
||||
style={
|
||||
item.lifecycle === 'experimental'
|
||||
? { backgroundColor: 'var(--bui-bg-warning)' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{column => column.cell(item)}
|
||||
</Row>
|
||||
)}
|
||||
{...tableProps}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Primitives Example
|
||||
// =============================================================================
|
||||
|
||||
export function PrimitivesExample() {
|
||||
const items = catalogData.slice(0, 5);
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<TableRoot>
|
||||
<TableHeader>
|
||||
<Column isRowHeader>Name</Column>
|
||||
<Column>Owner</Column>
|
||||
<Column>Type</Column>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map(item => (
|
||||
<Row key={item.id} id={String(item.id)}>
|
||||
<CellText title={item.name} />
|
||||
<CellText title={item.owner.name} />
|
||||
<CellText title={item.type} />
|
||||
</Row>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,161 +2,294 @@ import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import {
|
||||
TableRockBand,
|
||||
SelectionModePlayground,
|
||||
SelectionBehaviorPlayground,
|
||||
SelectionToggleWithActions,
|
||||
HeroExample,
|
||||
SortingExample,
|
||||
SearchExample,
|
||||
SelectionExample,
|
||||
RowActionsExample,
|
||||
EmptyStateExample,
|
||||
CombinedExample,
|
||||
CustomRowExample,
|
||||
PrimitivesExample,
|
||||
} from './components';
|
||||
import {
|
||||
tablePropsColumns,
|
||||
tableReturnColumns,
|
||||
useTableOptionsPropDefs,
|
||||
useTableReturnPropDefs,
|
||||
tablePropDefs,
|
||||
tableHeaderPropDefs,
|
||||
tableBodyPropDefs,
|
||||
tablePaginationPropDefs,
|
||||
columnConfigPropDefs,
|
||||
cellTextPropDefs,
|
||||
cellProfilePropDefs,
|
||||
tableRootPropDefs,
|
||||
columnPropDefs,
|
||||
rowPropDefs,
|
||||
cellPropDefs,
|
||||
} from './props-definition';
|
||||
import {
|
||||
tableUsageSnippet,
|
||||
tableBasicSnippet,
|
||||
tableSelectionActionsSnippet,
|
||||
tableSelectionModeSnippet,
|
||||
tableSelectionBehaviorSnippet,
|
||||
tableHeroSnippet,
|
||||
tableConceptsSnippet,
|
||||
tableSortingSnippet,
|
||||
tablePaginationSnippet,
|
||||
tableSearchSnippet,
|
||||
tableSelectionSnippet,
|
||||
tableRowActionsHrefSnippet,
|
||||
tableRowActionsClickSnippet,
|
||||
tableRowActionsDisabledSnippet,
|
||||
tableEmptyStateSnippet,
|
||||
tableOffsetPaginationSnippet,
|
||||
tableCursorPaginationSnippet,
|
||||
tableCombinedSnippet,
|
||||
tableCustomRowSnippet,
|
||||
tablePrimitivesSnippet,
|
||||
} from './snippets';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { ReactAriaLink } from '@/components/ReactAriaLink';
|
||||
import { TableDefinition } from '../../../utils/definitions';
|
||||
|
||||
export const reactAriaUrls = {
|
||||
table: 'https://react-aria.adobe.com/Table#table',
|
||||
tableHeader: 'https://react-aria.adobe.com/Table#tableheader',
|
||||
tableBody: 'https://react-aria.adobe.com/Table#tablebody',
|
||||
column: 'https://react-aria.adobe.com/Table#column',
|
||||
row: 'https://react-aria.adobe.com/Table#row',
|
||||
cell: 'https://react-aria.adobe.com/Table#cell',
|
||||
};
|
||||
|
||||
<PageTitle
|
||||
title="Table"
|
||||
description="A flexible table component built on top of TanStack Table with built-in styling, interactions, and pagination support."
|
||||
description="A data table component with built-in pagination, sorting, and selection."
|
||||
/>
|
||||
|
||||
<Snippet py={4} preview={<TableRockBand />} code={tableBasicSnippet} />
|
||||
<Snippet py={4} preview={<HeroExample />} code={tableHeroSnippet} />
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={tableUsageSnippet} />
|
||||
|
||||
## API reference
|
||||
## Core Concepts
|
||||
|
||||
### Table
|
||||
The Table component is designed around a hook + component pattern:
|
||||
|
||||
The main table component that renders data in a structured grid format.
|
||||
- **`useTable`** manages data fetching, pagination state, sorting, and filtering. It returns `tableProps` that you spread onto the Table component.
|
||||
- **`Table`** handles rendering - columns, rows, cells, and interactions.
|
||||
|
||||
<PropsTable data={tablePropDefs} />
|
||||
The hook supports three modes for different data scenarios:
|
||||
|
||||
### TableHeader
|
||||
- **`complete`** - You have all data available upfront (client-side)
|
||||
- **`offset`** - Your API uses offset/limit pagination
|
||||
- **`cursor`** - Your API uses cursor-based pagination
|
||||
|
||||
The header section of the table that contains the column definitions.
|
||||
<CodeBlock code={tableConceptsSnippet} />
|
||||
|
||||
<PropsTable data={tableHeaderPropDefs} />
|
||||
**Controlled vs uncontrolled state**
|
||||
|
||||
### Column
|
||||
By default, `useTable` manages sort, search, and filter state internally. Pass `initialSort`, `initialSearch`, or `initialFilter` to set starting values.
|
||||
|
||||
A column definition that describes how a column should be rendered.
|
||||
For full control over state, use the controlled props instead:
|
||||
|
||||
<PropsTable data={columnPropDefs} />
|
||||
- `sort` / `onSortChange`
|
||||
- `search` / `onSearchChange`
|
||||
- `filter` / `onFilterChange`
|
||||
|
||||
### TableBody
|
||||
|
||||
The body section of the table that contains the rows.
|
||||
|
||||
<PropsTable data={tableBodyPropDefs} />
|
||||
|
||||
### Row
|
||||
|
||||
A row definition that describes how a row should be rendered.
|
||||
|
||||
<PropsTable data={rowPropDefs} />
|
||||
|
||||
### Cell
|
||||
|
||||
A cell definition that describes how a cell should be rendered.
|
||||
|
||||
<PropsTable data={cellPropDefs} />
|
||||
|
||||
### TablePagination
|
||||
|
||||
A pagination component designed to work with the Table component.
|
||||
|
||||
<PropsTable data={tablePaginationPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Table
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Row selection
|
||||
|
||||
Tables support row selection with two configuration options: selection mode and selection behavior.
|
||||
|
||||
#### Selection mode
|
||||
|
||||
Use `selectionMode` to control how many rows can be selected. With `single`, only one row can be selected at a time. With `multiple`, any number of rows can be selected, and a header checkbox provides select-all functionality.
|
||||
|
||||
<Snippet
|
||||
preview={<SelectionModePlayground />}
|
||||
code={tableSelectionModeSnippet}
|
||||
/>
|
||||
|
||||
#### Selection behavior
|
||||
|
||||
Use `selectionBehavior` to control how selection is indicated and interacted with. With `toggle`, checkboxes appear for selection. With `replace`, selection is indicated by row background color—click to select, Cmd/Ctrl+click for multiple.
|
||||
|
||||
<Snippet
|
||||
preview={<SelectionBehaviorPlayground />}
|
||||
code={tableSelectionBehaviorSnippet}
|
||||
/>
|
||||
|
||||
#### With row actions
|
||||
|
||||
With toggle behavior, clicking a row triggers its action when nothing is selected. Once any row is selected, clicking toggles selection instead.
|
||||
|
||||
With replace behavior, clicking selects the row and double-clicking triggers the action.
|
||||
|
||||
<Snippet
|
||||
preview={<SelectionToggleWithActions />}
|
||||
code={tableSelectionActionsSnippet}
|
||||
/>
|
||||
|
||||
### Row Clicks
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Pagination
|
||||
|
||||
Coming soon.
|
||||
## Common Patterns
|
||||
|
||||
### Sorting
|
||||
|
||||
Coming soon.
|
||||
Columns can be made sortable by adding `isSortable: true` to the column configuration. When sortable, clicking a column header cycles through ascending, descending, and unsorted states. A visual indicator shows the current sort direction.
|
||||
|
||||
### Asynchronous loading
|
||||
With `mode: 'complete'`, sorting happens client-side. Provide a `sortFn` that receives the full dataset and the current sort descriptor, and returns the sorted array. You can also set an `initialSort` to define the default sort when the table first renders. For server-side sorting with `offset` or `cursor` modes, see [Server-Side Data](#server-side-data).
|
||||
|
||||
Coming soon.
|
||||
<Snippet preview={<SortingExample />} code={tableSortingSnippet} />
|
||||
|
||||
### Empty state
|
||||
### Pagination
|
||||
|
||||
Coming soon.
|
||||
Configure page size and available options through `paginationOptions`. The table displays navigation controls automatically.
|
||||
|
||||
### Column resizing
|
||||
<CodeBlock code={tablePaginationSnippet} />
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
### Search
|
||||
|
||||
### Column reordering
|
||||
The `useTable` hook returns a `search` object with `value` and `onChange` properties, ready to connect to a search input. With `mode: 'complete'`, provide a `searchFn` that filters the dataset based on the search query.
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
The search state is debounced internally, so rapid typing doesn't trigger excessive re-filtering. When the search query changes, pagination resets to the first page automatically.
|
||||
|
||||
### Column pinning
|
||||
For server-side search with `offset` or `cursor` modes, the search query is passed to your `getData` function. See [Server-Side Data](#server-side-data).
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
<Snippet preview={<SearchExample />} code={tableSearchSnippet} />
|
||||
|
||||
### Column visibility
|
||||
### Row Selection
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
Tables support row selection with two configuration options: `mode` and `behavior`.
|
||||
|
||||
Selection mode controls how many rows can be selected:
|
||||
|
||||
- `single` - Only one row can be selected at a time
|
||||
- `multiple` - Any number of rows can be selected, with a header checkbox for select-all
|
||||
|
||||
Selection behavior controls the interaction style:
|
||||
|
||||
- `toggle` - Checkboxes appear for selection. Click a checkbox to select/deselect.
|
||||
- `replace` - No checkboxes. Click a row to select it (replacing previous selection). Use Cmd/Ctrl+click to select multiple rows.
|
||||
|
||||
Selection state can be managed in two ways:
|
||||
|
||||
- **Controlled** - Pass both `selected` and `onSelectionChange` to fully manage state externally
|
||||
- **Uncontrolled** - Pass only `onSelectionChange` to let the Table manage state while receiving change notifications
|
||||
|
||||
<Snippet preview={<SelectionExample />} code={tableSelectionSnippet} />
|
||||
|
||||
### Row Actions
|
||||
|
||||
Rows can respond to user interaction in two ways: navigating to a URL or triggering a callback.
|
||||
|
||||
Use `getHref` when rows should link to detail pages. The entire row becomes clickable and navigates on click. Links within cells remain independently clickable.
|
||||
|
||||
<CodeBlock code={tableRowActionsHrefSnippet} />
|
||||
|
||||
Use `onClick` for custom actions like opening a panel or triggering a dialog.
|
||||
|
||||
<CodeBlock code={tableRowActionsClickSnippet} />
|
||||
|
||||
When combining row actions with selection, the interaction depends on selection behavior:
|
||||
|
||||
- **`toggle`**: Clicking a row triggers its action when nothing is selected. Once any row is selected, clicking anywhere on the row toggles selection instead.
|
||||
- **`replace`**: Single click selects the row. Double-click triggers the row action.
|
||||
|
||||
You can also disable specific rows from being clicked using `getIsDisabled`:
|
||||
|
||||
<CodeBlock code={tableRowActionsDisabledSnippet} />
|
||||
|
||||
<Snippet preview={<RowActionsExample />} />
|
||||
|
||||
### Empty State
|
||||
|
||||
When the table has no data to display, provide an `emptyState` to show a helpful message instead of an empty table body. Consider providing different messages depending on context - an empty search result is different from a table with no data at all.
|
||||
|
||||
<Snippet preview={<EmptyStateExample />} code={tableEmptyStateSnippet} />
|
||||
|
||||
## Server-Side Data
|
||||
|
||||
When your data comes from an API with server-side pagination, use `offset` or `cursor` mode instead of `complete`. The key difference: instead of providing all data upfront, you provide a `getData` function that fetches data for the current page.
|
||||
|
||||
### Offset Pagination
|
||||
|
||||
Use `mode: 'offset'` when your API accepts `offset` and `limit` (or `skip` and `take`) parameters.
|
||||
|
||||
<CodeBlock code={tableOffsetPaginationSnippet} />
|
||||
|
||||
The `signal` parameter is an `AbortSignal` - pass it to `fetch` so in-flight requests are cancelled when the user navigates away or triggers a new query.
|
||||
|
||||
### Cursor Pagination
|
||||
|
||||
Use `mode: 'cursor'` when your API uses cursor-based pagination (common with GraphQL or APIs that return `nextCursor`/`prevCursor` tokens).
|
||||
|
||||
<CodeBlock code={tableCursorPaginationSnippet} />
|
||||
|
||||
### Loading States
|
||||
|
||||
When fetching data, the table shows a loading state. If the user triggers a new query (by paginating, sorting, or searching) while previous data is displayed, the table enters a "stale" state where it continues showing the previous data until new data arrives. This prevents jarring layout shifts.
|
||||
|
||||
You can access these states via `tableProps.loading` and `tableProps.isStale` if you need to render additional loading indicators.
|
||||
|
||||
## Combining Features
|
||||
|
||||
Real-world tables often combine multiple features. Here's an example with search, sorting, pagination, and row selection working together.
|
||||
|
||||
<Snippet preview={<CombinedExample />} code={tableCombinedSnippet} />
|
||||
|
||||
## Custom Tables
|
||||
|
||||
For most use cases, `useTable` + `Table` provides everything you need. When you need more control, there are several ways you can customize your table.
|
||||
|
||||
### Custom Row and Header Rendering
|
||||
|
||||
Use a render function for `rowConfig` when you need to customize individual rows based on their data - for example, highlighting rows that require attention. Use `header` in column config to customize header cell content.
|
||||
|
||||
<Snippet preview={<CustomRowExample />} code={tableCustomRowSnippet} />
|
||||
|
||||
### Using Primitives Directly
|
||||
|
||||
When the `Table` component doesn't support your use case, you can compose with the lower-level primitives: `TableRoot`, `TableHeader`, `TableBody`, `Column`, and `Row`.
|
||||
|
||||
<Snippet preview={<PrimitivesExample />} code={tablePrimitivesSnippet} />
|
||||
|
||||
## API Reference
|
||||
|
||||
### useTable
|
||||
|
||||
The `useTable` hook manages data fetching, pagination, sorting, and filtering.
|
||||
|
||||
**Options**
|
||||
|
||||
<PropsTable data={useTableOptionsPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
**Return Value**
|
||||
|
||||
<PropsTable data={useTableReturnPropDefs} columns={tableReturnColumns} />
|
||||
|
||||
### Table
|
||||
|
||||
The main table component.
|
||||
|
||||
<PropsTable data={tablePropDefs} columns={tablePropsColumns} />
|
||||
|
||||
### ColumnConfig
|
||||
|
||||
<PropsTable data={columnConfigPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
### CellText
|
||||
|
||||
<PropsTable data={cellTextPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
|
||||
|
||||
### CellProfile
|
||||
|
||||
<PropsTable data={cellProfilePropDefs} columns={tablePropsColumns} />
|
||||
|
||||
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
|
||||
|
||||
### TablePagination
|
||||
|
||||
<PropsTable data={tablePaginationPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
### Primitives
|
||||
|
||||
Low-level components for building custom table layouts.
|
||||
|
||||
#### TableRoot
|
||||
|
||||
<PropsTable data={tableRootPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
<ReactAriaLink component="Table" href={reactAriaUrls.table} />
|
||||
|
||||
#### TableHeader
|
||||
|
||||
<ReactAriaLink component="TableHeader" href={reactAriaUrls.tableHeader} />
|
||||
|
||||
#### TableBody
|
||||
|
||||
<ReactAriaLink component="TableBody" href={reactAriaUrls.tableBody} />
|
||||
|
||||
#### Column
|
||||
|
||||
<PropsTable data={columnPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
<ReactAriaLink component="Column" href={reactAriaUrls.column} />
|
||||
|
||||
#### Row
|
||||
|
||||
<PropsTable data={rowPropDefs} columns={tablePropsColumns} />
|
||||
|
||||
<ReactAriaLink component="Row" href={reactAriaUrls.row} />
|
||||
|
||||
#### Cell
|
||||
|
||||
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
|
||||
|
||||
<Theming definition={TableDefinition} />
|
||||
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
|
||||
export const tablePropDefs: Record<string, PropDef> = {
|
||||
selectionBehavior: {
|
||||
type: 'enum',
|
||||
values: ['toggle', 'replace'],
|
||||
default: 'toggle',
|
||||
description: 'How multiple selection should behave in the collection.',
|
||||
},
|
||||
disabledBehavior: {
|
||||
type: 'enum',
|
||||
values: ['selection', 'all'],
|
||||
default: 'selection',
|
||||
description:
|
||||
'Whether disabledKeys applies to all interactions, or only selection.',
|
||||
},
|
||||
disabledKeys: {
|
||||
type: 'enum',
|
||||
values: ['Iterable<Key>'],
|
||||
description: 'A list of row keys to disable.',
|
||||
},
|
||||
selectionMode: {
|
||||
type: 'enum',
|
||||
values: ['single', 'multiple'],
|
||||
description: 'The type of selection that is allowed in the collection.',
|
||||
},
|
||||
selectedKeys: {
|
||||
type: 'enum',
|
||||
values: ['all', 'Iterable<Key>'],
|
||||
description: 'The currently selected keys in the collection (controlled).',
|
||||
},
|
||||
defaultSelectedKeys: {
|
||||
type: 'enum',
|
||||
values: ['all', 'Iterable<Key>'],
|
||||
description: 'The initial selected keys in the collection (uncontrolled).',
|
||||
},
|
||||
onRowAction: {
|
||||
type: 'enum',
|
||||
values: ['(key: Key) => void'],
|
||||
description:
|
||||
'Handler that is called when a user performs an action on the row.',
|
||||
},
|
||||
onSelectionChange: {
|
||||
type: 'enum',
|
||||
values: ['(keys: Selection) => void'],
|
||||
description: 'Handler that is called when the selection changes.',
|
||||
},
|
||||
onSortChange: {
|
||||
type: 'enum',
|
||||
values: ['(descriptor: SortDescriptor) => any'],
|
||||
description:
|
||||
'Handler that is called when the sorted column or direction changes.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tableHeaderPropDefs: Record<string, PropDef> = {
|
||||
onHoverStart: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction starts.',
|
||||
},
|
||||
onHoverEnd: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction ends.',
|
||||
},
|
||||
onHoverChange: {
|
||||
type: 'enum',
|
||||
values: ['(isHovering: boolean) => void'],
|
||||
description: 'Handler that is called when the hover state changes.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const columnPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['Key'],
|
||||
description: 'The unique id of the column.',
|
||||
},
|
||||
allowsSorting: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the column allows sorting.',
|
||||
},
|
||||
isRowHeader: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether a column is a row header and should be announced by assistive technology during row navigation.',
|
||||
},
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description:
|
||||
"A string representation of the column's contents, used for accessibility announcements.",
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tableBodyPropDefs: Record<string, PropDef> = {
|
||||
renderEmptyState: {
|
||||
type: 'enum',
|
||||
values: ['(props) => ReactNode'],
|
||||
description:
|
||||
'Provides content to display when there are no rows in the table.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const rowPropDefs: Record<string, PropDef> = {
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description:
|
||||
"A string representation of the row's contents, used for accessibility announcements.",
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the row is disabled.',
|
||||
},
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['Key'],
|
||||
description: 'The unique id of the row.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
description: 'The URL to navigate to when the row is clicked.',
|
||||
},
|
||||
hrefLang: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The language of the URL to navigate to when the row is clicked.',
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The target of the URL to navigate to when the row is clicked.',
|
||||
},
|
||||
rel: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The relationship of the URL to navigate to when the row is clicked.',
|
||||
},
|
||||
onAction: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description:
|
||||
"Handler that is called when a user performs an action on the row. The exact user event depends on the collection's selectionBehavior prop and the interaction modality.",
|
||||
},
|
||||
onHoverStart: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction starts.',
|
||||
},
|
||||
onHoverEnd: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction ends.',
|
||||
},
|
||||
onHoverChange: {
|
||||
type: 'enum',
|
||||
values: ['(isHovering: boolean) => void'],
|
||||
description: 'Handler that is called when the hover state changes.',
|
||||
},
|
||||
onPress: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description:
|
||||
'Handler that is called when the press is released over the target.',
|
||||
},
|
||||
onPressStart: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description: 'Handler that is called when a press interaction starts.',
|
||||
},
|
||||
onPressEnd: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description:
|
||||
'Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target.',
|
||||
},
|
||||
onPressChange: {
|
||||
type: 'enum',
|
||||
values: ['(isPressed: boolean) => void'],
|
||||
description: 'Handler that is called when the press state changes.',
|
||||
},
|
||||
onPressUp: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description:
|
||||
'Handler that is called when a press is released over the target, regardless of whether it started on the target or not.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const cellPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['Key'],
|
||||
description: 'The unique id of the cell.',
|
||||
},
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description:
|
||||
"A string representation of the cell's contents, used for features like typeahead.",
|
||||
},
|
||||
leadingIcon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Optional icon to display before the cell content.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tablePaginationPropDefs: Record<string, PropDef> = {
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'The current offset (starting index) for pagination.',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
description: 'The number of items per page.',
|
||||
},
|
||||
setOffset: {
|
||||
type: 'enum',
|
||||
values: ['(offset: number) => void'],
|
||||
description: 'Handler that is called when the offset changes.',
|
||||
},
|
||||
setPageSize: {
|
||||
type: 'enum',
|
||||
values: ['(pageSize: number) => void'],
|
||||
description: 'Handler that is called when the page size changes.',
|
||||
},
|
||||
rowCount: {
|
||||
type: 'number',
|
||||
description: 'The total number of rows in the table.',
|
||||
},
|
||||
onNextPage: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Handler that is called when the next page is requested.',
|
||||
},
|
||||
onPreviousPage: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Handler that is called when the previous page is requested.',
|
||||
},
|
||||
onPageSizeChange: {
|
||||
type: 'enum',
|
||||
values: ['(pageSize: number) => void'],
|
||||
description: 'Handler that is called when the page size changes.',
|
||||
},
|
||||
showPageSizeOptions: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to show the page size options.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -0,0 +1,462 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
import { Chip } from '@/components/Chip';
|
||||
|
||||
// =============================================================================
|
||||
// PropsTable Column Configuration (Table docs use description instead of responsive)
|
||||
// =============================================================================
|
||||
|
||||
export const tablePropsColumns = [
|
||||
{ key: 'prop' as const, width: '15%' },
|
||||
{ key: 'type' as const, width: '25%' },
|
||||
{ key: 'default' as const, width: '15%' },
|
||||
{ key: 'description' as const, width: '45%' },
|
||||
];
|
||||
|
||||
// For return values (no default column)
|
||||
export const tableReturnColumns = [
|
||||
{ key: 'prop' as const, width: '15%' },
|
||||
{ key: 'type' as const, width: '25%' },
|
||||
{ key: 'description' as const, width: '60%' },
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// useTable Hook
|
||||
// =============================================================================
|
||||
|
||||
export const useTableOptionsPropDefs: Record<string, PropDef> = {
|
||||
mode: {
|
||||
type: 'enum',
|
||||
values: ['complete', 'offset', 'cursor'],
|
||||
description: (
|
||||
<>
|
||||
Data fetching strategy (required). Use <Chip>complete</Chip> for
|
||||
client-side data, <Chip>offset</Chip> for offset/limit APIs,{' '}
|
||||
<Chip>cursor</Chip> for cursor-based pagination.
|
||||
</>
|
||||
),
|
||||
},
|
||||
getData: {
|
||||
type: 'enum',
|
||||
values: ['function'],
|
||||
description:
|
||||
'Function that returns or fetches data (required). Signature varies by mode.',
|
||||
},
|
||||
paginationOptions: {
|
||||
type: 'enum',
|
||||
values: ['object'],
|
||||
description: (
|
||||
<>
|
||||
Pagination configuration including <Chip>pageSize</Chip>,{' '}
|
||||
<Chip>pageSizeOptions</Chip>, and <Chip>initialOffset</Chip>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
// Uncontrolled state
|
||||
initialSort: {
|
||||
type: 'enum',
|
||||
values: ['SortDescriptor'],
|
||||
description: 'Default sort configuration on first render (uncontrolled).',
|
||||
},
|
||||
initialSearch: {
|
||||
type: 'string',
|
||||
description: 'Default search value on first render (uncontrolled).',
|
||||
},
|
||||
initialFilter: {
|
||||
type: 'enum',
|
||||
values: ['TFilter'],
|
||||
description: 'Default filter value on first render (uncontrolled).',
|
||||
},
|
||||
// Controlled state
|
||||
sort: {
|
||||
type: 'enum',
|
||||
values: ['SortDescriptor'],
|
||||
description: 'Current sort state (controlled).',
|
||||
},
|
||||
onSortChange: {
|
||||
type: 'enum',
|
||||
values: ['(sort: SortDescriptor) => void'],
|
||||
description: 'Sort change handler (controlled).',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Current search value (controlled).',
|
||||
},
|
||||
onSearchChange: {
|
||||
type: 'enum',
|
||||
values: ['(search: string) => void'],
|
||||
description: 'Search change handler (controlled).',
|
||||
},
|
||||
filter: {
|
||||
type: 'enum',
|
||||
values: ['TFilter'],
|
||||
description: 'Current filter value (controlled).',
|
||||
},
|
||||
onFilterChange: {
|
||||
type: 'enum',
|
||||
values: ['(filter: TFilter) => void'],
|
||||
description: 'Filter change handler (controlled).',
|
||||
},
|
||||
// Client-side functions
|
||||
sortFn: {
|
||||
type: 'enum',
|
||||
values: ['(data, sort) => data'],
|
||||
description: (
|
||||
<>
|
||||
Client-side sort function. Only used with <Chip>complete</Chip> mode.
|
||||
</>
|
||||
),
|
||||
},
|
||||
searchFn: {
|
||||
type: 'enum',
|
||||
values: ['(data, query) => data'],
|
||||
description: (
|
||||
<>
|
||||
Client-side search function. Only used with <Chip>complete</Chip> mode.
|
||||
</>
|
||||
),
|
||||
},
|
||||
filterFn: {
|
||||
type: 'enum',
|
||||
values: ['(data, filter) => data'],
|
||||
description: (
|
||||
<>
|
||||
Client-side filter function. Only used with <Chip>complete</Chip> mode.
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const useTableReturnPropDefs: Record<string, PropDef> = {
|
||||
tableProps: {
|
||||
type: 'enum',
|
||||
values: ['object'],
|
||||
description: (
|
||||
<>
|
||||
Props to spread onto the <Chip>Table</Chip> component. Includes data,
|
||||
loading, error, pagination, and sort state.
|
||||
</>
|
||||
),
|
||||
},
|
||||
reload: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Function to trigger a data refetch.',
|
||||
},
|
||||
search: {
|
||||
type: 'enum',
|
||||
values: ['{ value, onChange }'],
|
||||
description: (
|
||||
<>
|
||||
Search state object for binding to a <Chip>SearchField</Chip> component.
|
||||
</>
|
||||
),
|
||||
},
|
||||
filter: {
|
||||
type: 'enum',
|
||||
values: ['{ value, onChange }'],
|
||||
description: 'Filter state object for binding to filter controls.',
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Table Component
|
||||
// =============================================================================
|
||||
|
||||
export const tablePropDefs: Record<string, PropDef> = {
|
||||
columnConfig: {
|
||||
type: 'enum',
|
||||
values: ['ColumnConfig[]'],
|
||||
description:
|
||||
'Array of column configurations defining how each column renders.',
|
||||
},
|
||||
data: {
|
||||
type: 'enum',
|
||||
values: ['T[]'],
|
||||
description: 'Array of data items to display in the table.',
|
||||
},
|
||||
loading: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Whether the table is in a loading state.',
|
||||
},
|
||||
isStale: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description:
|
||||
'Whether the displayed data is stale while new data is loading.',
|
||||
},
|
||||
error: {
|
||||
type: 'enum',
|
||||
values: ['Error'],
|
||||
description: 'Error object if data fetching failed.',
|
||||
},
|
||||
pagination: {
|
||||
type: 'enum',
|
||||
values: ['TablePaginationType'],
|
||||
description: (
|
||||
<>
|
||||
Pagination configuration (required). Use{' '}
|
||||
<Chip>{'{ type: "none" }'}</Chip> to disable or{' '}
|
||||
<Chip>{'{ type: "page", ...props }'}</Chip> for pagination.
|
||||
</>
|
||||
),
|
||||
},
|
||||
sort: {
|
||||
type: 'enum',
|
||||
values: ['SortState'],
|
||||
description: 'Sort state including current descriptor and change handler.',
|
||||
},
|
||||
rowConfig: {
|
||||
type: 'enum',
|
||||
values: ['RowConfig | RowRenderFn'],
|
||||
description: (
|
||||
<>
|
||||
Row configuration object with <Chip>getHref</Chip>, <Chip>onClick</Chip>
|
||||
, <Chip>getIsDisabled</Chip>, or a render function for custom rows.
|
||||
</>
|
||||
),
|
||||
},
|
||||
selection: {
|
||||
type: 'enum',
|
||||
values: ['TableSelection'],
|
||||
description:
|
||||
'Selection configuration including mode, behavior, selected keys, and change handler.',
|
||||
},
|
||||
emptyState: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Content to display when the table has no data.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ColumnConfig
|
||||
// =============================================================================
|
||||
|
||||
export const columnConfigPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Unique identifier for the column.',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'Display label for the column header.',
|
||||
},
|
||||
cell: {
|
||||
type: 'enum',
|
||||
values: ['(item) => ReactNode'],
|
||||
description: 'Render function for cell content.',
|
||||
},
|
||||
header: {
|
||||
type: 'enum',
|
||||
values: ['() => ReactNode'],
|
||||
description: 'Optional custom render function for the header cell.',
|
||||
},
|
||||
isSortable: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Whether the column supports sorting.',
|
||||
},
|
||||
isHidden: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Whether the column is hidden.',
|
||||
},
|
||||
isRowHeader: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Whether this column is the row header for accessibility.',
|
||||
},
|
||||
width: {
|
||||
type: 'enum',
|
||||
values: ['ColumnSize'],
|
||||
description: 'Current width of the column.',
|
||||
},
|
||||
defaultWidth: {
|
||||
type: 'enum',
|
||||
values: ['ColumnSize'],
|
||||
description: (
|
||||
<>
|
||||
Default width of the column (e.g., <Chip>1fr</Chip>, <Chip>200px</Chip>
|
||||
).
|
||||
</>
|
||||
),
|
||||
},
|
||||
minWidth: {
|
||||
type: 'enum',
|
||||
values: ['ColumnStaticSize'],
|
||||
description: 'Minimum width of the column.',
|
||||
},
|
||||
maxWidth: {
|
||||
type: 'enum',
|
||||
values: ['ColumnStaticSize'],
|
||||
description: 'Maximum width of the column.',
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// CellText
|
||||
// Note: Extends React Aria Cell props
|
||||
// =============================================================================
|
||||
|
||||
export const cellTextPropDefs: Record<string, PropDef> = {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Primary text content of the cell.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Secondary description text displayed below the title.',
|
||||
},
|
||||
color: {
|
||||
type: 'enum',
|
||||
values: ['TextColors'],
|
||||
description: 'Text color variant.',
|
||||
},
|
||||
leadingIcon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Icon displayed before the text content.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
description: 'URL to navigate to when the cell is clicked.',
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// CellProfile
|
||||
// Note: Extends React Aria Cell props
|
||||
// =============================================================================
|
||||
|
||||
export const cellProfilePropDefs: Record<string, PropDef> = {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name to display.',
|
||||
},
|
||||
src: {
|
||||
type: 'string',
|
||||
description: 'URL of the avatar image.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Secondary text displayed below the name.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
description: 'URL to navigate to when clicked.',
|
||||
},
|
||||
color: {
|
||||
type: 'enum',
|
||||
values: ['TextColors'],
|
||||
description: 'Text color variant.',
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// TablePagination
|
||||
// =============================================================================
|
||||
|
||||
export const tablePaginationPropDefs: Record<string, PropDef> = {
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
description: 'Number of items per page.',
|
||||
},
|
||||
pageSizeOptions: {
|
||||
type: 'enum',
|
||||
values: ['number[]'],
|
||||
description: 'Available page size options for the dropdown.',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Current offset (starting index) in the data.',
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of items across all pages.',
|
||||
},
|
||||
hasNextPage: {
|
||||
type: 'boolean',
|
||||
description: 'Whether there is a next page available.',
|
||||
},
|
||||
hasPreviousPage: {
|
||||
type: 'boolean',
|
||||
description: 'Whether there is a previous page available.',
|
||||
},
|
||||
onNextPage: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Handler called when navigating to the next page.',
|
||||
},
|
||||
onPreviousPage: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Handler called when navigating to the previous page.',
|
||||
},
|
||||
onPageSizeChange: {
|
||||
type: 'enum',
|
||||
values: ['(size: number) => void'],
|
||||
description: 'Handler called when the page size changes.',
|
||||
},
|
||||
showPageSizeOptions: {
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: 'Whether to show the page size dropdown.',
|
||||
},
|
||||
getLabel: {
|
||||
type: 'enum',
|
||||
values: ['(props) => string'],
|
||||
description: 'Custom function to generate the pagination label text.',
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Primitives
|
||||
// =============================================================================
|
||||
|
||||
export const tableRootPropDefs: Record<string, PropDef> = {
|
||||
stale: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: (
|
||||
<>
|
||||
Whether the table data is stale (e.g., while fetching new data). Adds{' '}
|
||||
<Chip>aria-busy</Chip> attribute.
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const columnPropDefs: Record<string, PropDef> = {
|
||||
isRowHeader: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this column is a row header for accessibility.',
|
||||
},
|
||||
children: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Column header content.',
|
||||
},
|
||||
};
|
||||
|
||||
export const rowPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['string | number'],
|
||||
description: 'Unique identifier for the row.',
|
||||
},
|
||||
children: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode | ((column) => ReactNode)'],
|
||||
description:
|
||||
'Row content. Can be a render function receiving column config.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -1,108 +1,359 @@
|
||||
export const tableUsageSnippet = `import { Cell, CellText, ..., TableHeader, TablePagination } from '@backstage/ui';
|
||||
// =============================================================================
|
||||
// Usage
|
||||
// =============================================================================
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<Column />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<Row>
|
||||
<CellText title="Example" />
|
||||
<CellProfile />
|
||||
</Row>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination />`;
|
||||
export const tableUsageSnippet = `import { Table, useTable, CellText, type ColumnConfig } from '@backstage/ui';
|
||||
|
||||
export const tableBasicSnippet = `import { Table, TableHeader, Column, TableBody, Row, CellText, CellProfile, TablePagination, useTable } from '@backstage/ui';
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'The Beatles',
|
||||
image: 'https://upload.wikimedia.org/wikipedia/en/thumb/4/42/Beatles_-...jpg',
|
||||
genre: 'Rock, Pop, Psychedelic Rock',
|
||||
yearFormed: 1960,
|
||||
albums: 13
|
||||
},
|
||||
// ... more data
|
||||
const columns: ColumnConfig<DataType>[] = [
|
||||
{ id: 'name', label: 'Name', isRowHeader: true, cell: item => <CellText title={item.name} /> },
|
||||
{ id: 'owner', label: 'Owner', cell: item => <CellText title={item.owner} /> },
|
||||
];
|
||||
|
||||
// Uncontrolled pagination (easiest)
|
||||
const { data: paginatedData, paginationProps } = useTable({
|
||||
data,
|
||||
pagination: {
|
||||
defaultPageSize: 5,
|
||||
function MyTable() {
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
});
|
||||
|
||||
return <Table columnConfig={columns} {...tableProps} />;
|
||||
}`;
|
||||
|
||||
// =============================================================================
|
||||
// Hero / Quick Start
|
||||
// =============================================================================
|
||||
|
||||
export const tableHeroSnippet = `import { Table, CellText, useTable, type ColumnConfig } from '@backstage/ui';
|
||||
|
||||
const data = [
|
||||
{ id: 1, name: 'Service A', owner: 'Team Alpha', type: 'service' },
|
||||
{ id: 2, name: 'Service B', owner: 'Team Beta', type: 'website' },
|
||||
{ id: 3, name: 'Library C', owner: 'Team Gamma', type: 'library' },
|
||||
];
|
||||
|
||||
const columns: ColumnConfig<typeof data[0]>[] = [
|
||||
{ id: 'name', label: 'Name', isRowHeader: true, cell: item => <CellText title={item.name} /> },
|
||||
{ id: 'owner', label: 'Owner', cell: item => <CellText title={item.owner} /> },
|
||||
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
|
||||
];
|
||||
|
||||
function MyTable() {
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
});
|
||||
|
||||
return <Table columnConfig={columns} {...tableProps} />;
|
||||
}`;
|
||||
|
||||
// =============================================================================
|
||||
// Core Concepts
|
||||
// =============================================================================
|
||||
|
||||
export const tableConceptsSnippet = `// What useTable returns
|
||||
const {
|
||||
tableProps, // Spread onto <Table />
|
||||
reload, // Trigger data refetch
|
||||
search, // { value, onChange } for search input
|
||||
filter, // { value, onChange } for filters
|
||||
} = useTable({ ... });`;
|
||||
|
||||
// =============================================================================
|
||||
// Common Patterns
|
||||
// =============================================================================
|
||||
|
||||
export const tableSortingSnippet = `const columns: ColumnConfig<Item>[] = [
|
||||
{ id: 'name', label: 'Name', isSortable: true, cell: item => <CellText title={item.name} /> },
|
||||
{ id: 'owner', label: 'Owner', isSortable: true, cell: item => <CellText title={item.owner} /> },
|
||||
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
|
||||
];
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
initialSort: { column: 'name', direction: 'ascending' },
|
||||
sortFn: (items, { column, direction }) => {
|
||||
return [...items].sort((a, b) => {
|
||||
const aVal = String(a[column]);
|
||||
const bVal = String(b[column]);
|
||||
const cmp = aVal.localeCompare(bVal);
|
||||
return direction === 'descending' ? -cmp : cmp;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<Column isRowHeader>Band name</Column>
|
||||
<Column>Genre</Column>
|
||||
<Column>Year formed</Column>
|
||||
<Column>Albums</Column>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedData?.map(item => (
|
||||
<Row key={item.name}>
|
||||
<CellProfile
|
||||
name={item.name}
|
||||
src={item.image}
|
||||
href={item.website}
|
||||
/>
|
||||
<CellText title={item.genre} />
|
||||
<CellText title={item.yearFormed.toString()} />
|
||||
<CellText title={item.albums.toString()} />
|
||||
</Row>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination {...paginationProps} />`;
|
||||
return <Table columnConfig={columns} {...tableProps} />;`;
|
||||
|
||||
export const tableSelectionActionsSnippet = `import { Table, TableHeader, TableBody, Column, Row, CellText } from '@backstage/ui';
|
||||
export const tablePaginationSnippet = `const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
paginationOptions: {
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
},
|
||||
});`;
|
||||
|
||||
function MyTable() {
|
||||
const [selectedKeys, setSelectedKeys] = React.useState(new Set([]));
|
||||
export const tableSearchSnippet = `const { tableProps, search } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
searchFn: (items, query) => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return items.filter(item =>
|
||||
item.name.toLowerCase().includes(lowerQuery) ||
|
||||
item.owner.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchField
|
||||
aria-label="Search"
|
||||
placeholder="Search..."
|
||||
{...search}
|
||||
/>
|
||||
<Table columnConfig={columns} {...tableProps} />
|
||||
</>
|
||||
);`;
|
||||
|
||||
export const tableSelectionSnippet = `const [selected, setSelected] = useState<Set<string | number> | 'all'>(new Set());
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
columnConfig={columns}
|
||||
selection={{
|
||||
mode: 'multiple',
|
||||
behavior: 'toggle',
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
{...tableProps}
|
||||
/>
|
||||
);`;
|
||||
|
||||
export const tableRowActionsHrefSnippet = `<Table
|
||||
columnConfig={columns}
|
||||
rowConfig={{
|
||||
getHref: item => \`/catalog/\${item.namespace}/\${item.name}\`
|
||||
}}
|
||||
{...tableProps}
|
||||
/>`;
|
||||
|
||||
export const tableRowActionsClickSnippet = `<Table
|
||||
columnConfig={columns}
|
||||
rowConfig={{
|
||||
onClick: item => openDetailPanel(item)
|
||||
}}
|
||||
{...tableProps}
|
||||
/>`;
|
||||
|
||||
export const tableRowActionsDisabledSnippet = `<Table
|
||||
columnConfig={columns}
|
||||
rowConfig={{
|
||||
onClick: item => openDetailPanel(item),
|
||||
getIsDisabled: item => item.status === 'archived',
|
||||
}}
|
||||
{...tableProps}
|
||||
/>`;
|
||||
|
||||
export const tableEmptyStateSnippet = `const { tableProps, search } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
searchFn: (items, query) => { /* ... */ },
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
columnConfig={columns}
|
||||
emptyState={
|
||||
search.value
|
||||
? <Text>No results match "{search.value}"</Text>
|
||||
: <Text>No items yet. Create one to get started.</Text>
|
||||
}
|
||||
{...tableProps}
|
||||
/>
|
||||
);`;
|
||||
|
||||
// =============================================================================
|
||||
// Server-Side Data
|
||||
// =============================================================================
|
||||
|
||||
export const tableOffsetPaginationSnippet = `const { tableProps } = useTable({
|
||||
mode: 'offset',
|
||||
getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
|
||||
const response = await fetch(
|
||||
\`/api/items?offset=\${offset}&limit=\${pageSize}&q=\${search}\`,
|
||||
{ signal }
|
||||
);
|
||||
const { items, totalCount } = await response.json();
|
||||
|
||||
return {
|
||||
data: items,
|
||||
totalCount,
|
||||
};
|
||||
},
|
||||
paginationOptions: {
|
||||
pageSize: 20,
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
},
|
||||
});`;
|
||||
|
||||
export const tableCursorPaginationSnippet = `const { tableProps } = useTable({
|
||||
mode: 'cursor',
|
||||
getData: async ({ cursor, pageSize, sort, search, signal }) => {
|
||||
const response = await fetch(
|
||||
\`/api/items?cursor=\${cursor ?? ''}&limit=\${pageSize}&q=\${search}\`,
|
||||
{ signal }
|
||||
);
|
||||
const { items, nextCursor, prevCursor, totalCount } = await response.json();
|
||||
|
||||
return {
|
||||
data: items,
|
||||
nextCursor,
|
||||
prevCursor,
|
||||
totalCount, // optional - enables "X of Y" display
|
||||
};
|
||||
},
|
||||
paginationOptions: {
|
||||
pageSize: 20,
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
},
|
||||
});`;
|
||||
|
||||
// =============================================================================
|
||||
// Combining Features
|
||||
// =============================================================================
|
||||
|
||||
export const tableCombinedSnippet = `interface TypeFilter {
|
||||
type: string | null;
|
||||
}
|
||||
|
||||
const columns: ColumnConfig<Item>[] = [
|
||||
{ id: 'name', label: 'Name', isRowHeader: true, isSortable: true,
|
||||
cell: item => <CellText title={item.name} description={item.description} /> },
|
||||
{ id: 'owner', label: 'Owner', isSortable: true,
|
||||
cell: item => <CellText title={item.owner} /> },
|
||||
{ id: 'type', label: 'Type', isSortable: true,
|
||||
cell: item => <CellText title={item.type} /> },
|
||||
];
|
||||
|
||||
function ItemsTable() {
|
||||
const [selected, setSelected] = useState<Set<string | number> | 'all'>(new Set());
|
||||
|
||||
const { tableProps, search, filter } = useTable<Item, TypeFilter>({
|
||||
mode: 'offset',
|
||||
initialSort: { column: 'name', direction: 'ascending' },
|
||||
getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
|
||||
const params = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(pageSize),
|
||||
q: search,
|
||||
...(sort && { sortBy: sort.column, sortDir: sort.direction }),
|
||||
...(filter?.type && { type: filter.type }),
|
||||
});
|
||||
|
||||
const response = await fetch(\`/api/items?\${params}\`, { signal });
|
||||
const { items, totalCount } = await response.json();
|
||||
|
||||
return { data: items, totalCount };
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
selectionMode="multiple"
|
||||
selectionBehavior="toggle"
|
||||
selectedKeys={selectedKeys}
|
||||
onSelectionChange={setSelectedKeys}
|
||||
onRowAction={(key) => console.log('Opening', key)}
|
||||
>
|
||||
<TableHeader>
|
||||
<Column isRowHeader>Name</Column>
|
||||
<Column>Status</Column>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<Row id="1">
|
||||
<CellText title="Component A" />
|
||||
<CellText title="Active" />
|
||||
</Row>
|
||||
<Row id="2">
|
||||
<CellText title="Component B" />
|
||||
<CellText title="Inactive" />
|
||||
</Row>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex gap="4" align="end">
|
||||
<SearchField
|
||||
aria-label="Search"
|
||||
label="Search"
|
||||
placeholder="Search by name or description..."
|
||||
value={search.value}
|
||||
onChange={search.onChange}
|
||||
/>
|
||||
<Select
|
||||
label="Type"
|
||||
options={typeOptions}
|
||||
value={filter.value?.type ?? ''}
|
||||
onChange={value => filter.onChange({ type: value || null })}
|
||||
/>
|
||||
</Flex>
|
||||
<Table
|
||||
columnConfig={columns}
|
||||
rowConfig={{
|
||||
onClick: item => openDetailPanel(item),
|
||||
}}
|
||||
selection={{
|
||||
mode: 'multiple',
|
||||
behavior: 'toggle',
|
||||
selected,
|
||||
onSelectionChange: setSelected,
|
||||
}}
|
||||
emptyState={
|
||||
search.value || filter.value?.type
|
||||
? <Text>No results match your filters</Text>
|
||||
: <Text>No items available</Text>
|
||||
}
|
||||
{...tableProps}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}`;
|
||||
|
||||
export const tableSelectionModeSnippet = `<Table
|
||||
selectionMode="multiple" // or "single"
|
||||
selectionBehavior="toggle"
|
||||
selectedKeys={selectedKeys}
|
||||
onSelectionChange={setSelectedKeys}
|
||||
>
|
||||
{/* ... */}
|
||||
</Table>`;
|
||||
// =============================================================================
|
||||
// Custom Tables
|
||||
// =============================================================================
|
||||
|
||||
export const tableSelectionBehaviorSnippet = `<Table
|
||||
selectionMode="multiple"
|
||||
selectionBehavior="toggle" // or "replace"
|
||||
selectedKeys={selectedKeys}
|
||||
onSelectionChange={setSelectedKeys}
|
||||
>
|
||||
{/* ... */}
|
||||
</Table>`;
|
||||
export const tableCustomRowSnippet = `import { Fragment } from 'react';
|
||||
|
||||
const columns: ColumnConfig<Item>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
isRowHeader: true,
|
||||
header: () => <Text weight="bold">Name (required)</Text>,
|
||||
cell: item => <CellText title={item.name} />,
|
||||
},
|
||||
{ id: 'owner', label: 'Owner', cell: item => <CellText title={item.owner} /> },
|
||||
{ id: 'lifecycle', label: 'Lifecycle', cell: item => <CellText title={item.lifecycle} /> },
|
||||
];
|
||||
|
||||
const { tableProps } = useTable({
|
||||
mode: 'complete',
|
||||
getData: () => data,
|
||||
});
|
||||
|
||||
<Table
|
||||
columnConfig={columns}
|
||||
rowConfig={({ item }) => (
|
||||
<Row
|
||||
id={String(item.id)}
|
||||
columns={columns}
|
||||
className={item.lifecycle === 'experimental' ? styles.experimentalRow : undefined}
|
||||
>
|
||||
{column => (
|
||||
<Fragment key={column.id}>{column.cell(item)}</Fragment>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
{...tableProps}
|
||||
/>`;
|
||||
|
||||
export const tablePrimitivesSnippet = `<TableRoot>
|
||||
<TableHeader>
|
||||
<Column isRowHeader>Name</Column>
|
||||
<Column>Owner</Column>
|
||||
<Column>Type</Column>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map(item => (
|
||||
<Row key={item.id} id={String(item.id)}>
|
||||
<CellText title={item.name} />
|
||||
<CellText title={item.owner} />
|
||||
<CellText title={item.type} />
|
||||
</Row>
|
||||
))}
|
||||
</TableBody>
|
||||
</TableRoot>`;
|
||||
|
||||
Reference in New Issue
Block a user