Table
Examples
The table component provides a flexible structure for displaying data, that can be combined with different data management layers to implement various features. You can optionally leverage external libraries such as TanStack Table to handle parts of data management logic.
For features like row actions, search, sorting, and selection, you can review the following code examples that demonstrate implementation using only Jutro components and examples that incorporate TanStack Table for part of the data management logic. The examples have all files needed for you to review them in your environment, such as hooks and mock data. Each example has a different set of files.
Check out the Usage section for details about how to design a table properly, and the different configuration options we provide.
Basic example
To create a basic implementation of a Table component, you must use the following components:
Tableto set a structureTableHeaderto define the header sectionTableBodyto define the body sectionHeaderRowto define a row in the headerBodyRowto define a row in the bodyHeaderCellto define a cell in the headerBodyCellto define a cell in the body
When using these components, you must follow the rules defined in the Code and Usage sections. This includes proper component nesting and adding appropriate accessibility labels, such as aria-label used in this example.
export default function BasicTableExample() {
const data = [
{
id: '73065',
product: 'Go Commercial Auto',
insured: 'Marshall Rogahn',
premium: 1236.39,
},
{
id: '80077',
product: 'Go Worker\'s Compensation',
insured: 'April Kub',
premium: 173.99,
},
{
id: '64487',
product: 'USA Personal Auto',
insured: 'Abel Rippin',
premium: 1228.69,
},
{
id: '12345',
product: 'Go Commercial Auto',
insured: 'John Smith',
premium: 892.45,
},
];
return (
<Table aria-label="Policy list">
<TableHeader>
<HeaderRow>
<HeaderCell columnIndex={0}>Product</HeaderCell>
<HeaderCell columnIndex={1}>Insured</HeaderCell>
<HeaderCell columnIndex={2}>Premium</HeaderCell>
</HeaderRow>
</TableHeader>
<TableBody>
{data.map((row, rowIndex) => (
<BodyRow key={row.id}>
<BodyCell rowIndex={rowIndex} columnIndex={0}>
{row.product}
</BodyCell>
<BodyCell rowIndex={rowIndex} columnIndex={1}>
{row.insured}
</BodyCell>
<BodyCell rowIndex={rowIndex} columnIndex={2}>
${row.premium.toLocaleString()}
</BodyCell>
</BodyRow>
))}
</TableBody>
</Table>
);
}
Table with a title
The Table component does not include any built-in titles or subtitles. It is recommended that you add a descriptive title in your implementation, and a subtitle if necessary. You can easily add them using the Jutro Typography component.
Add appropriate accessibility labels, such as aria-labelledby and aria-describedby, to the Table component to reference the title and subtitle, respectively.
export function TableWithTitleExample() {
const data = [
{
id: '73065',
product: 'Go Commercial Auto',
insured: 'Marshall Rogahn',
premium: 1236.39,
},
{
id: '80077',
product: 'Go Worker\'s Compensation',
insured: 'April Kub',
premium: 173.99,
},
{
id: '64487',
product: 'USA Personal Auto',
insured: 'Abel Rippin',
premium: 1228.69,
},
{
id: '12345',
product: 'Go Commercial Auto',
insured: 'John Smith',
premium: 892.45,
},
];
return (
<div style={{ display: 'block', width: '100%' }}>
<div
style={{
display: 'block',
width: '100%',
marginBottom: '1rem',
clear: 'both',
float: 'none',
}}>
<Typography variant="heading-2" id="policy-list-table">
Policy list
</Typography>
<Typography role="doc-subtitle" id="policy-list-subtitle">
Detailed record of individual policies
</Typography>
</div>
<div style={{ display: 'block', width: '100%', clear: 'both' }}>
<Table
aria-labelledby="policy-list-table"
aria-describedby="policy-list-subtitle">
<TableHeader>
<HeaderRow>
<HeaderCell columnIndex={0}>Product</HeaderCell>
<HeaderCell columnIndex={1}>Insured</HeaderCell>
<HeaderCell columnIndex={2}>Premium</HeaderCell>
</HeaderRow>
</TableHeader>
<TableBody>
{data.map((row, rowIndex) => (
<BodyRow key={row.id}>
<BodyCell rowIndex={rowIndex} columnIndex={0}>
{row.product}
</BodyCell>
<BodyCell rowIndex={rowIndex} columnIndex={1}>
{row.insured}
</BodyCell>
<BodyCell rowIndex={rowIndex} columnIndex={2}>
${row.premium.toLocaleString()}
</BodyCell>
</BodyRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
Row actions
To implement row actions in the Table component, combine the structure components with a data management layer that handles individual row operations and an action column that contains interactive elements for each row.
The row actions implementation requires managing action callbacks and providing them to action components placed in table cells. Create action buttons or menus in a dedicated column that perform operations on individual rows, such as edit, delete, or view details. This dedicated column is typically the last column in the table. The action components receive row data and action callbacks as parameters, and can trigger operations that modify the dataset or change the application state.
Use a data management layer (such as the useTableRowActions custom hook) to handle row operations like adding, updating, or deleting individual rows from your dataset. The action logic must provide methods for performing these operations and updating the internal state accordingly. For more complex scenarios like inline editing, combine row actions with an editing state management layer (such as the useEditableRow custom hook) that tracks which rows are in edit mode and manages temporary edit data.
Connect the action callbacks by passing functions like deleteRow and saveRow to your action column components. These callbacks are executed when users interact with action buttons and modify the dataset or trigger state changes. For inline editing scenarios, implement conditional rendering in your cells that switches between display components and input components based on the row's editing state, using functions like isRowEditing and renderCell to manage the display logic.
Alternatively, you can use TanStack Table as a data management layer, which provides flexibility for implementing custom row actions through column definitions with custom cell renderers. With TanStack Table, row actions are implemented through column configurations that define the action components and their behaviors.
Component implementation
import React, {
useCallback,
useEffect,
useId,
useMemo,
useState,
} from 'react';
import type { ReactNode } from 'react';
import {
BodyCell,
BodyRow,
Button,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableHeader,
Typography,
} from '@jutro/components';
import type { TableProps } from '@jutro/components';
import { DeleteIcon, EditIcon } from '@jutro/icons';
import { EditableActions } from './Editable';
import {
EditableProduct,
EditableInsured,
EditablePremium,
} from './Editable';
import { useEditableRow } from './RowActionsHooks';
import { useTableRowActions } from './RowActionsHooks';
import { TableMockData, tableMockData } from './table.mockdata';
type RowActions<T> = {
deleteRow?: (id: string) => void;
saveRow?: (id: string, rowData: Partial<T>) => void;
};
/** Column type definition that specifies the structure for table columns with row actions capabilities. */
/** Each column has an id, header renderer, cell renderer, and optional alignment. */
export type Column<T> = {
id: string;
header: () => ReactNode;
cell: (row: T, actions?: RowActions<T>) => ReactNode;
align?: 'left' | 'right' | 'center';
};
export type ColumnDefinition<T extends object> = Array<Column<T>>;
/** Column configuration for the table with Product, Insured, and Premium columns. */
export const genericColumns: ColumnDefinition<TableMockData> = [
{
id: 'product',
header: () => (
<Typography variant="heading-5" tag="span">
Product
</Typography>
),
cell: (row) => <Typography>{row.product.name}</Typography>,
},
{
id: 'insured',
header: () => (
<Typography variant="heading-5" tag="span">
Insured
</Typography>
),
cell: (row) => <Typography>{row.insured}</Typography>,
},
{
id: 'premium',
header: () => (
<Typography variant="heading-5" tag="span">
Premium
</Typography>
),
cell: (row) => (
<Typography>
{`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`}
</Typography>
),
align: 'right',
},
];
export const TableTitleString = 'Policy list';
/** Component that renders a table title section with heading and subtitle. */
export const TableTitle: React.FC<{ tableId: string }> = ({
tableId,
}) => (
<div>
<div>
<Typography variant="heading-2" id={tableId}>
{TableTitleString}
</Typography>
<Typography role="doc-subtitle">
Detailed record of individual policies
</Typography>
</div>
</div>
);
export function RowActionsStory(
args: TableProps
): React.ReactElement {
const tableId = useId();
const {
rowData,
setAtomicRowData,
onRowSave,
isRowEditing,
startEditing,
abortEditing,
} = useEditableRow<TableMockData>();
const [onFocusFirstField, setOnFocusFirstField] = useState(false);
const handleStartEditing = useCallback(
(rowId: string, row: TableMockData) => {
startEditing(rowId, row);
setOnFocusFirstField(true);
},
[startEditing]
);
// Reset focus trigger after it's been used
useEffect(() => {
if (onFocusFirstField) {
const timer = setTimeout(() => {
setOnFocusFirstField(false);
}, 100);
return () => clearTimeout(timer);
}
}, [onFocusFirstField]);
const columns = useMemo<ColumnDefinition<TableMockData>>(
() => [
...genericColumns,
{
id: 'actions',
align: 'right',
header: () => (
<Typography tag="span" variant="heading-5">
Actions
</Typography>
),
cell: (row, { deleteRow } = {}) => (
<div>
<Button
label={`Delete ${row.id}`}
icon={<DeleteIcon />}
variant="tertiary"
onClick={() => deleteRow?.(row.id)}
hideLabel
/>
<Button
label={`Edit ${row.id}`}
icon={<EditIcon />}
variant="tertiary"
onClick={() => handleStartEditing(row.id, row)}
hideLabel
/>
</div>
),
},
],
[handleStartEditing]
);
const { data, deleteRow, saveRow } =
useTableRowActions<TableMockData>({
data: tableMockData,
});
const editableCells = useMemo<Record<string, ReactNode>>(
() => ({
product: (
<EditableProduct
id="product"
value={rowData?.product}
onChange={setAtomicRowData}
onFocus={onFocusFirstField}
/>
),
insured: (
<EditableInsured
id="insured"
value={rowData?.insured}
onChange={setAtomicRowData}
/>
),
premium: (
<EditablePremium
id="premium"
value={rowData?.premium}
onChange={setAtomicRowData}
/>
),
actions: (
<EditableActions
onSave={() => onRowSave(saveRow)}
onAbort={abortEditing}
/>
),
}),
[
rowData,
setAtomicRowData,
onRowSave,
abortEditing,
saveRow,
onFocusFirstField,
]
);
const renderCell = (
rowId: string,
columnId: string,
cellCallback: () => ReactNode
) => {
if (isRowEditing(rowId)) {
return editableCells[columnId];
}
return cellCallback();
};
return (
<div>
<TableTitle tableId={tableId} />
<Table
className={args.className}
noStripedRows={args.noStripedRows}
aria-labelledby={tableId}
>
<TableHeader>
<HeaderRow>
{columns.map(({ id, header, align }, columnIndex) => (
<HeaderCell
key={`${id}_${columnIndex}`}
columnIndex={columnIndex}
align={align}
>
{header()}
</HeaderCell>
))}
</HeaderRow>
</TableHeader>
<TableBody>
{data.map((row, rowIndex) => (
<BodyRow key={row.id}>
{columns.map(
({ cell, id: columnId, align }, columnIndex) => (
<BodyCell
key={`${columnId}_${row.id}`}
columnIndex={columnIndex}
rowIndex={rowIndex}
align={align}
>
{renderCell(row.id, columnId, () =>
cell(row, { deleteRow, saveRow })
)}
</BodyCell>
)
)}
</BodyRow>
))}
</TableBody>
</Table>
</div>
);
}
Data management
import { useState, useCallback } from 'react';
/** Arguments for the useTableRowActions hook. */
export type UseTableRowActionsArgs<T extends { id: string }> = {
data: T[];
initialState?: {
selectedRows?: string[];
};
};
/** Return type for the useTableRowActions hook. */
export type UseTableRowActionsResult<T extends { id: string }> = {
data: T[];
selectedRows: string[];
hasAllRowsSelected: () => boolean;
hasSomeRowsSelected: () => boolean;
toggleRowsSelection: () => void;
isRowSelected: (rowId: string) => boolean;
toggleRowSelection: (rowId: string) => void;
deleteRow: (rowId: string) => void;
saveRow: (rowId: string, rowData: Partial<T>) => void;
};
/**
* Custom React hook for managing table row actions and selection.
* Provides functionality for selecting rows and performing actions on them.
*/
export function useTableRowActions<T extends { id: string }>({
data: initialData,
initialState: { selectedRows: initialSelectedRows = [] } = {},
}: UseTableRowActionsArgs<T>): UseTableRowActionsResult<T> {
const [data, setData] = useState(initialData);
const [selectedRows, setSelectedRows] = useState(
initialSelectedRows
);
const hasAllRowsSelected = () =>
selectedRows.length !== 0 && selectedRows.length === data.length;
const hasSomeRowsSelected = () => selectedRows.length > 0;
const toggleRowsSelection = () => {
setSelectedRows((alreadySelected) => {
if (alreadySelected.length > 0) {
return [];
}
return [...data.map((row) => row.id)];
});
};
const isRowSelected = (rowId: string) =>
selectedRows.includes(rowId);
const toggleRowSelection = (rowId: string) => {
setSelectedRows((alreadySelected) => {
if (alreadySelected.includes(rowId)) {
return alreadySelected.filter((id) => id !== rowId);
}
return [rowId, ...alreadySelected];
});
};
const deleteRow = (rowId: string) => {
setData((current) => current.filter((row) => row.id !== rowId));
setSelectedRows((current) =>
current.filter((id) => id !== rowId)
);
};
const saveRow = (rowId: string, rowData: Partial<T>) => {
setData((current) =>
current.map((row) => {
if (row.id !== rowId) {
return row;
}
return { ...row, ...rowData };
})
);
};
return {
data,
selectedRows,
hasAllRowsSelected,
hasSomeRowsSelected,
toggleRowsSelection,
isRowSelected,
toggleRowSelection,
deleteRow,
saveRow,
};
}
/** Callback function type for saving row data. */
type SaveCallback<T> = (id: string, rowData: Partial<T>) => void;
/** Return type for the useEditableRow hook. */
type UseEditableRowResult<T> = {
rowData?: Partial<T>;
setAtomicRowData: (key: string, value: unknown) => void;
onRowSave: (callback?: SaveCallback<T>) => void;
isRowEditing: (id?: string) => boolean;
startEditing: (id: string, row: T) => void;
abortEditing: () => void;
};
/**
* Custom React hook for managing editable table rows.
* Provides functionality for editing individual rows in a table.
*/
export function useEditableRow<T>(): UseEditableRowResult<T> {
const [editingId, setEditingId] = useState<string | undefined>(undefined);
const [rowData, setRowData] = useState<Partial<T> | undefined>(undefined);
const setAtomicRowData = useCallback(
(key: string, value: unknown) => {
setRowData(data => ({ ...data, [key]: value }));
},
[setRowData]
);
const onRowSave = useCallback(
(callback?: SaveCallback<T>) => {
if (!editingId || !rowData) {
setEditingId(undefined);
setRowData(undefined);
return;
}
callback?.(editingId, rowData);
setEditingId(undefined);
setRowData(undefined);
},
[editingId, rowData]
);
const isRowEditing = useCallback(
(id?: string) => editingId === id,
[editingId]
);
const startEditing = useCallback(
(id: string, row: T) => {
setEditingId(id);
setRowData(row);
},
[setEditingId, setRowData]
);
const abortEditing = useCallback(() => {
setEditingId(undefined);
setRowData(undefined);
}, [setEditingId, setRowData]);
return {
rowData,
setAtomicRowData,
onRowSave,
isRowEditing,
startEditing,
abortEditing,
};
}
import React, { useEffect, useRef } from 'react';
import { Button } from '@jutro/components';
import { CheckIcon, CloseIcon } from '@jutro/icons';
import {
Combobox,
ComboboxOption,
CurrencyInput,
TextInput,
} from '@jutro/components';
import type { CurrencyInputProps } from '@jutro/components';
import { tableMockData } from './table.mockdata';
import type {
TableMockDataProduct,
TableMockDataCurrency,
} from './table.mockdata';
export const EditableActions: React.FC<{
onSave: () => void;
onAbort: () => void;
}> = ({ onSave, onAbort }) => (
<div>
<Button
label="Save"
icon={<CheckIcon />}
variant="tertiary"
onClick={onSave}
hideLabel
/>
<Button
label="Abort"
icon={<CloseIcon />}
variant="tertiary"
onClick={onAbort}
hideLabel
/>
</div>
);
type CurrencyValue = CurrencyInputProps['value'];
// Get unique product names from the mock data
const availableProducts = Array.from(
new Set(tableMockData.map((item) => item.product.name))
).map((name) => ({ name }));
export const EditableProduct: React.FC<{
id: string;
value: TableMockDataProduct | undefined;
onChange: (id: string, value: TableMockDataProduct) => void;
onFocus?: boolean;
}> = ({ id, value, onChange, onFocus }) => {
const comboboxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (onFocus && comboboxRef.current) {
// Find the input element within the combobox and focus it
const input = comboboxRef.current.querySelector('input');
if (input) {
input.focus();
}
}
}, [onFocus]);
return (
<div ref={comboboxRef}>
<Combobox
label="Edit product"
value={{
id: String(value?.name),
label: String(value?.name),
}}
onChange={(_, newValue) => {
onChange(id, {
name: String(newValue?.label),
});
}}
hideLabel
>
{availableProducts.map(({ name }) => (
<ComboboxOption
key={name}
value={{ id: name, label: name }}
/>
))}
</Combobox>
</div>
);
};
export const EditableInsured: React.FC<{
id: string;
value: string | undefined;
onChange: (id: string, value: string) => void;
}> = ({ id, value, onChange }) => (
<TextInput
label="Edit insured"
value={value}
onChange={(event) => {
onChange(id, event.target.value);
}}
hideLabel
/>
);
export const EditablePremium: React.FC<{
id: string;
value: TableMockDataCurrency | undefined;
onChange: (id: string, value: TableMockDataCurrency) => void;
}> = ({ id, value, onChange }) => (
<CurrencyInput
label="Edit premium"
availableCurrencies={['USD']}
value={value}
onChange={(_, newValue) => onChange(id, newValue)}
hideLabel
/>
);
import type { CurrencyInputProps } from '@jutro/components';
export type TableMockDataProduct = {
name: string;
};
export type TableMockDataCurrency = NonNullable<CurrencyInputProps['value']>;
export type TableMockData = {
id: string;
product: TableMockDataProduct;
insured: string;
premium: TableMockDataCurrency;
};
export const tableMockData: Array<TableMockData> = [
{
id: '73065',
product: {
name: 'Go Commercial Auto',
},
insured: 'Marshall Rogahn',
premium: { currency: 'USD', amount: 1236.39 },
},
{
id: '80077',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'April Kub',
premium: { currency: 'USD', amount: 173.99 },
},
{
id: '64487',
product: {
name: 'USA Personal Auto',
},
insured: 'Abel Rippin',
premium: { currency: 'USD', amount: 1228.69 },
},
{
id: '12345',
product: {
name: 'Go Commercial Auto',
},
insured: 'John Smith',
premium: { currency: 'USD', amount: 892.45 },
},
{
id: '23456',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'Sarah Johnson',
premium: { currency: 'USD', amount: 567.23 },
},
{
id: '34567',
product: {
name: 'USA Personal Auto',
},
insured: 'Michael Brown',
premium: { currency: 'USD', amount: 445.67 },
},
{
id: '45678',
product: {
name: 'Go Commercial Auto',
},
insured: 'Emily Davis',
premium: { currency: 'USD', amount: 2134.89 },
},
{
id: '56789',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'David Wilson',
premium: { currency: 'USD', amount: 823.45 },
},
{
id: '67890',
product: {
name: 'USA Personal Auto',
},
insured: 'Jessica Miller',
premium: { currency: 'USD', amount: 298.76 },
},
{
id: '78901',
product: {
name: 'Go Commercial Auto',
},
insured: 'Christopher Taylor',
premium: { currency: 'USD', amount: 3456.78 },
},
];
Search
To implement search in the Table component, combine the structure components with a data management layer and a search input component that filters table data in real-time.
The search implementation requires managing search query state and providing search callbacks to a search input component. Create a search input component like SearchBar that allows users to enter search queries, and connect it to your table through a search handler function. The search functionality filters your dataset based on the search query by comparing it against searchable column values using accessor functions.
Use a data management layer (such as the useTableDataSearch custom hook) to handle search query state, debounce user input for performance, and filter your dataset based on the current search query. Debouncing is essential to prevent excessive filtering operations as users type, which can cause performance issues with large datasets. You can implement debouncing using techniques like setTimeout or libraries such as Lodash's debounce function to delay the search execution until the user has stopped typing.
The search logic must compare the query against specific column values using accessorFn properties defined in your column configuration. Each column that should be searchable needs an accessor function that returns the searchable text for that column. Connect the search callbacks by passing functions like onSearchQueryChange to the onChange property of your search input component.
Alternatively, you can use TanStack Table as a data management layer, which provides built-in global filtering capabilities. With TanStack Table, search functionality is handled automatically through its useReactTable hook with getFilteredRowModel function and global filter configuration.
Component implementation
import React, {
useEffect,
useRef,
useState,
useId,
type ReactNode,
} from 'react';
import { flushSync } from 'react-dom';
import {
BodyCell,
BodyRow,
Button,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableEmptyState,
TableHeader,
TextInput,
Typography,
} from '@jutro/components';
import type { TableProps } from '@jutro/components';
import { SearchIcon } from '@jutro/icons';
import type { IntlMessageShape } from '@jutro/prop-types';
import { useTableDataSearch } from './useTableDataSearch';
import { TableMockData, tableMockData } from './table.mockdata';
type SearchBarProps = {
/**
* Label for the search input, supports internationalized messages.
*/
label?: IntlMessageShape;
/**
* Callback function triggered when the search input value changes.
*/
onChange: (value: string) => void;
/**
* Placeholder text for the search input, supports internationalized messages.
*/
placeholder?: IntlMessageShape;
/**
* The current value of the search input.
*/
value?: string;
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>;
/** SearchBar component that provides a collapsible search input with icon toggle functionality. */
export const SearchBar: React.FC<SearchBarProps> = ({
onChange,
value: externalValue,
label = 'Search table - enter text to update the table data below',
placeholder = 'Search table',
...htmlProps
}) => {
const searchInput = useRef<HTMLInputElement>(null);
const [active, setActive] = useState(Boolean(externalValue));
const [value, setValue] = useState(externalValue);
useEffect(() => {
setValue(externalValue);
}, [externalValue]);
const onSearchChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setValue(event.target.value);
onChange(event.target.value);
};
const onIconClick = () => {
flushSync(() => {
setActive(true);
});
searchInput.current?.focus();
};
const onInputBlur = () => {
if (value) {
return;
}
setActive(false);
};
return (
<div {...htmlProps}>
{active && (
<TextInput
ref={searchInput}
label={label}
hideLabel
placeholder={placeholder}
value={value}
onChange={onSearchChange}
onBlur={onInputBlur}
/>
)}
{!active && (
<Button
label={label}
variant="neutral"
icon={<SearchIcon />}
hideLabel
onClick={onIconClick}
/>
)}
</div>
);
};
SearchBar.displayName = 'SearchBar';
/** Column type definition that specifies the structure for table columns with search capabilities. */
/** Each column has an id, header renderer, cell renderer, optional alignment, and accessor for search functionality. */
export type Column<T> = {
id: string;
header: () => ReactNode;
cell: (row: T) => ReactNode;
align?: 'left' | 'right' | 'center';
accessorFn?: (row: T) => string;
};
export type ColumnDefinition<T extends object> = Array<Column<T>>;
/** Column configuration for the table with Product, Insured, and Premium columns for search functionality. */
export const columns: ColumnDefinition<TableMockData> = [
{
id: 'product',
header: () => (
<Typography variant="heading-5" tag="span">
Product
</Typography>
),
cell: (row) => <Typography>{row.product.name}</Typography>,
accessorFn: (row) => row.product.name,
},
{
id: 'insured',
header: () => (
<Typography variant="heading-5" tag="span">
Insured
</Typography>
),
cell: (row) => <Typography>{row.insured}</Typography>,
accessorFn: (row) => row.insured,
},
{
id: 'premium',
header: () => (
<Typography variant="heading-5" tag="span">
Premium
</Typography>
),
cell: (row) => (
<Typography>
{`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`}
</Typography>
),
align: 'right',
accessorFn: (row) =>
`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`,
},
];
export const TableTitleString = 'Policy list';
/** Component that renders a table title section with heading and subtitle. */
export const TableTitle: React.FC<{
tableId: string;
children?: ReactNode;
}> = ({ tableId, children }) => (
<div>
<div>
<Typography variant="heading-2" id={tableId}>
{TableTitleString}
</Typography>
<Typography role="doc-subtitle">
Detailed record of individual policies
</Typography>
</div>
{children}
</div>
);
export function SearchStory(args: TableProps): React.ReactElement {
const tableId = useId();
const { data, searchQuery, onSearchQueryChange } =
useTableDataSearch<TableMockData>({
columns,
data: tableMockData,
});
const renderTableBody = () => {
if (!data.length) {
return (
<TableEmptyState detailedMessageContent="Your search returned no matching policies. Try a different query." />
);
}
return data.map((row, rowIndex) => (
<BodyRow key={row.id}>
{columns.map(({ cell, align }, columnIndex) => (
<BodyCell
key={`${columnIndex}_${row.id}`}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={align}
>
{cell(row)}
</BodyCell>
))}
</BodyRow>
));
};
return (
<div>
<TableTitle tableId={tableId}>
<div>
<SearchBar
value={searchQuery}
onChange={onSearchQueryChange}
/>
</div>
</TableTitle>
<Table
aria-labelledby={tableId}
className={args.className}
noStripedRows={args.noStripedRows}
>
<TableHeader>
<HeaderRow>
{columns.map(({ header, align }, columnIndex) => (
<HeaderCell
key={columnIndex}
columnIndex={columnIndex}
align={align}
>
{header()}
</HeaderCell>
))}
</HeaderRow>
</TableHeader>
<TableBody>{renderTableBody()}</TableBody>
</Table>
</div>
);
}
Data management
import { useMemo, useEffect, useState } from 'react';
import type { ColumnDefinition } from './Search';
/** Custom hook to debounce a value over a specified delay period. */
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
/** Arguments for the useTableDataSearch hook. */
export type UseTableDataSearchArgs<T extends object> = {
columns: ColumnDefinition<T>;
data: T[];
initialState?: {
searchQuery?: string;
};
};
/** Return type for the useTableDataSearch hook. */
export type UseTableDataSearchResult<T extends object> = {
data: T[];
searchQuery?: string;
onSearchQueryChange: (newSearchQuery?: string) => void;
};
const DEFAULT_DEBOUNCE_TIMEOUT = 300;
/**
* Utility class for handling table data search operations.
* Provides methods for filtering data based on search queries.
*/
class TableData<T extends object> {
private data: T[];
readonly columns: ColumnDefinition<T>;
constructor(data: T[], columns: ColumnDefinition<T>) {
this.data = data;
this.columns = columns;
}
search(searchQuery?: string) {
if (!searchQuery) {
return this;
}
const query = searchQuery.trim().toLowerCase();
this.data = this.data.filter((row) =>
this.columns.some(({ accessorFn }) =>
accessorFn?.(row)?.toLowerCase().includes(query)
)
);
return this;
}
getData() {
return this.data;
}
}
/**
* Custom React hook for managing table data with search functionality.
* Handles search state and provides filtered data along with control functions.
*/
export function useTableDataSearch<T extends object>({
columns,
data: initialData,
initialState: { searchQuery: initialSearchQuery } = {},
}: UseTableDataSearchArgs<T>): UseTableDataSearchResult<T> {
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
const debouncedSearchQuery = useDebounce(
searchQuery,
DEFAULT_DEBOUNCE_TIMEOUT
);
const tableData = useMemo(() => {
const tableDataFactory = new TableData<T>(initialData, columns);
return tableDataFactory.search(debouncedSearchQuery).getData();
}, [columns, initialData, debouncedSearchQuery]);
const onSearchQueryChange = (newSearchQuery?: string) => {
setSearchQuery(newSearchQuery);
};
return {
data: tableData,
searchQuery: debouncedSearchQuery,
onSearchQueryChange,
};
}
import type { CurrencyInputProps } from '@jutro/components';
export type TableMockDataProduct = {
name: string;
};
export type TableMockDataCurrency = NonNullable<CurrencyInputProps['value']>;
export type TableMockData = {
id: string;
product: TableMockDataProduct;
insured: string;
premium: TableMockDataCurrency;
};
export const tableMockData: Array<TableMockData> = [
{
id: '73065',
product: {
name: 'Go Commercial Auto',
},
insured: 'Marshall Rogahn',
premium: { currency: 'USD', amount: 1236.39 },
},
{
id: '80077',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'April Kub',
premium: { currency: 'USD', amount: 173.99 },
},
{
id: '64487',
product: {
name: 'USA Personal Auto',
},
insured: 'Abel Rippin',
premium: { currency: 'USD', amount: 1228.69 },
},
{
id: '12345',
product: {
name: 'Go Commercial Auto',
},
insured: 'John Smith',
premium: { currency: 'USD', amount: 892.45 },
},
{
id: '23456',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'Sarah Johnson',
premium: { currency: 'USD', amount: 567.23 },
},
{
id: '34567',
product: {
name: 'USA Personal Auto',
},
insured: 'Michael Brown',
premium: { currency: 'USD', amount: 445.67 },
},
{
id: '45678',
product: {
name: 'Go Commercial Auto',
},
insured: 'Emily Davis',
premium: { currency: 'USD', amount: 2134.89 },
},
{
id: '56789',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'David Wilson',
premium: { currency: 'USD', amount: 823.45 },
},
{
id: '67890',
product: {
name: 'USA Personal Auto',
},
insured: 'Jessica Miller',
premium: { currency: 'USD', amount: 298.76 },
},
{
id: '78901',
product: {
name: 'Go Commercial Auto',
},
insured: 'Christopher Taylor',
premium: { currency: 'USD', amount: 3456.78 },
},
];
Selection
To implement selection in the Table component, combine the structure components with a data management layer that manages selection state for individual rows and bulk operations.
The selection implementation requires managing selection state of individual rows and providing selection callbacks to your table cells. Use the selected property in BodyRow component to indicate whether a row is selected. This Boolean value can be used by child components to determine row state.
Handle selection interactions through Checkbox components placed in BodyCell and HeaderCell components. You can handle selection changes by updating the selection state and maintaining a list of selected row identifiers. Place individual row checkboxes in the first column of each row, and add a header checkbox in the corresponding HeaderCell for bulk selection operations like "select all" or "clear all".
Use a data management layer (such as the useTableDataSelection custom hook) to track which rows are selected, handle individual row selection, and manage bulk selection operations like "select all" or "clear all". The selection logic must handle adding and removing items from the selection set and provide methods for checking selection status. Connect the selection callbacks to Checkbox components by passing functions like toggleRowSelection and toggleRowsSelection to the onChange properties of the respective checkboxes. These callbacks update the internal selection state when users interact with the checkboxes.
Alternatively, you can use TanStack Table as a data management layer, which provides built-in row selection state management. With TanStack Table, selection is handled automatically through its useReactTable hook with row selection configuration.
Component implementation
import React, { useId, useMemo, type ReactNode } from 'react';
import {
BodyCell,
BodyRow,
Button,
Checkbox,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableHeader,
Typography,
} from '@jutro/components';
import { CheckIcon, RemoveIcon } from '@jutro/icons';
import type { TableProps } from '@jutro/components';
import { useTableDataSelection } from './useTableDataSelection';
import { TableMockData, tableMockData } from './table.mockdata';
type HeaderActions = {
hasAllRowsSelected?: () => boolean;
hasSomeRowsSelected?: () => boolean;
toggleRowsSelection?: () => void;
};
type RowActions = {
isRowSelected?: (id: string) => boolean;
toggleRowSelection?: (id: string) => void;
};
/** Column type definition that specifies the structure for table columns with selection capabilities. */
export type Column<T> = {
id: string;
header: (actions?: HeaderActions) => ReactNode;
cell: (row: T, actions?: RowActions) => ReactNode;
align?: 'left' | 'right' | 'center';
};
export type ColumnDefinition<T extends object> = Array<Column<T>>;
/** Column configuration for the table with Product, Insured, and Premium columns. */
export const basicColumns: ColumnDefinition<TableMockData> = [
{
id: 'product',
header: () => (
<Typography variant="heading-5" tag="span">
Product
</Typography>
),
cell: (row) => <Typography>{row.product.name}</Typography>,
},
{
id: 'insured',
header: () => (
<Typography variant="heading-5" tag="span">
Insured
</Typography>
),
cell: (row) => <Typography>{row.insured}</Typography>,
},
{
id: 'premium',
header: () => (
<Typography variant="heading-5" tag="span">
Premium
</Typography>
),
cell: (row) => (
<Typography>
{`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`}
</Typography>
),
align: 'right',
},
];
/** Component that displays the count of selected rows and provides a button to clear the selection. */
type SelectedRowsCounterProps = {
count: number;
onClear: () => void;
};
export const SelectedRowsCounter: React.FC<
SelectedRowsCounterProps
> = ({ count, onClear }) => {
if (!count) {
return null;
}
const noun = count === 1 ? 'row' : 'rows';
return (
<div>
<Typography tag="span">
{count} {noun} selected
</Typography>
<Button
label="Clear selection"
variant="tertiary"
size="small"
onClick={onClear}
/>
</div>
);
};
export const TableTitleString = 'Policy list';
/** Component that renders a table title section with heading and subtitle. */
export const TableTitle: React.FC<{
tableId: string;
children?: ReactNode;
}> = ({ tableId, children }) => (
<div>
<div>
<Typography variant="heading-2" id={tableId}>
{TableTitleString}
</Typography>
<Typography role="doc-subtitle">
Detailed record of individual policies
</Typography>
</div>
{children}
</div>
);
export function SelectionStory(args: TableProps): React.ReactElement {
const tableId = useId();
const columns = useMemo<ColumnDefinition<TableMockData>>(
() => [
{
id: 'selection',
header: ({
hasAllRowsSelected,
hasSomeRowsSelected,
toggleRowsSelection,
} = {}) => {
const allSelected = hasAllRowsSelected?.();
const someSelected = hasSomeRowsSelected?.();
const icon = allSelected ? <CheckIcon /> : <RemoveIcon />;
return (
<Checkbox
label=""
aria-label={
someSelected ? 'Deselect all' : 'Select all'
}
checked={allSelected || someSelected}
onChange={() => toggleRowsSelection?.()}
icon={icon}
/>
);
},
cell: (row, { isRowSelected, toggleRowSelection } = {}) => (
<Checkbox
label=""
aria-label={
isRowSelected?.(row.id)
? `Deselect ${row.id}`
: `Select ${row.id}`
}
checked={isRowSelected?.(row.id)}
onChange={() => toggleRowSelection?.(row.id)}
/>
),
},
...basicColumns,
],
[]
);
const {
data,
selectedRows,
hasAllRowsSelected,
hasSomeRowsSelected,
toggleRowsSelection,
isRowSelected,
toggleRowSelection,
deleteRow,
} = useTableDataSelection<TableMockData>({
data: tableMockData,
});
const onRowsDelete = () => {
selectedRows.forEach(deleteRow);
};
return (
<div>
<TableTitle tableId={tableId}>
{hasSomeRowsSelected() && (
<div>
<Button
label="Delete selected rows"
onClick={onRowsDelete}
/>
</div>
)}
</TableTitle>
<SelectedRowsCounter
count={selectedRows.length}
onClear={() => toggleRowsSelection?.()}
/>
<Table
aria-labelledby={tableId}
className={args.className}
noStripedRows={args.noStripedRows}
>
<TableHeader>
<HeaderRow>
{columns.map(({ id, header, align }, columnIndex) => (
<HeaderCell
key={id}
columnIndex={columnIndex}
align={align}
>
{header({
hasAllRowsSelected,
hasSomeRowsSelected,
toggleRowsSelection,
})}
</HeaderCell>
))}
</HeaderRow>
</TableHeader>
<TableBody>
{data.map((row, rowIndex) => (
<BodyRow key={row.id} selected={isRowSelected(row.id)}>
{columns.map(({ id, cell, align }, columnIndex) => (
<BodyCell
key={`${id}_${row.id}`}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={align}
>
{cell(row, {
isRowSelected,
toggleRowSelection,
})}
</BodyCell>
))}
</BodyRow>
))}
</TableBody>
</Table>
</div>
);
}
Data management
import { useState } from 'react';
/** Arguments for the useTableDataSelection hook. */
export type UseTableDataSelectionArgs<T extends { id: string }> = {
data: T[];
initialState?: {
selectedRows?: string[];
};
};
/** Return type for the useTableDataSelection hook. */
export type UseTableDataSelectionResult<T extends { id: string }> = {
data: T[];
selectedRows: string[];
hasAllRowsSelected: () => boolean;
hasSomeRowsSelected: () => boolean;
toggleRowsSelection: () => void;
isRowSelected: (rowId: string) => boolean;
toggleRowSelection: (rowId: string) => void;
deleteRow: (rowId: string) => void;
saveRow: (rowId: string, rowData: Partial<T>) => void;
};
/**
* Custom React hook for managing table data with selection functionality.
* Handles row selection state and provides selection control functions along with basic data manipulation.
*/
export function useTableDataSelection<T extends { id: string }>({
data: initialData,
initialState: { selectedRows: initialSelectedRows = [] } = {},
}: UseTableDataSelectionArgs<T>): UseTableDataSelectionResult<T> {
const [data, setData] = useState(initialData);
const [selectedRows, setSelectedRows] = useState(
initialSelectedRows
);
const hasAllRowsSelected = () =>
selectedRows.length !== 0 && selectedRows.length === data.length;
const hasSomeRowsSelected = () => selectedRows.length > 0;
const toggleRowsSelection = () => {
setSelectedRows((alreadySelected) => {
if (alreadySelected.length > 0) {
return [];
}
return [...data.map((row) => row.id)];
});
};
const isRowSelected = (rowId: string) =>
selectedRows.includes(rowId);
const toggleRowSelection = (rowId: string) => {
setSelectedRows((alreadySelected) => {
if (alreadySelected.includes(rowId)) {
return alreadySelected.filter((id) => id !== rowId);
}
return [rowId, ...alreadySelected];
});
};
const deleteRow = (rowId: string) => {
setData((current) => current.filter((row) => row.id !== rowId));
setSelectedRows((current) =>
current.filter((id) => id !== rowId)
);
};
const saveRow = (rowId: string, rowData: Partial<T>) => {
setData((current) =>
current.map((row) => {
if (row.id !== rowId) {
return row;
}
return { ...row, ...rowData };
})
);
};
return {
data,
selectedRows,
hasAllRowsSelected,
hasSomeRowsSelected,
toggleRowsSelection,
isRowSelected,
toggleRowSelection,
deleteRow,
saveRow,
};
}
import type { CurrencyInputProps } from '@jutro/components';
export type TableMockDataProduct = {
name: string;
};
export type TableMockDataCurrency = NonNullable<CurrencyInputProps['value']>;
export type TableMockData = {
id: string;
product: TableMockDataProduct;
insured: string;
premium: TableMockDataCurrency;
};
export const tableMockData: Array<TableMockData> = [
{
id: '73065',
product: {
name: 'Go Commercial Auto',
},
insured: 'Marshall Rogahn',
premium: { currency: 'USD', amount: 1236.39 },
},
{
id: '80077',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'April Kub',
premium: { currency: 'USD', amount: 173.99 },
},
{
id: '64487',
product: {
name: 'USA Personal Auto',
},
insured: 'Abel Rippin',
premium: { currency: 'USD', amount: 1228.69 },
},
{
id: '12345',
product: {
name: 'Go Commercial Auto',
},
insured: 'John Smith',
premium: { currency: 'USD', amount: 892.45 },
},
{
id: '23456',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'Sarah Johnson',
premium: { currency: 'USD', amount: 567.23 },
},
{
id: '34567',
product: {
name: 'USA Personal Auto',
},
insured: 'Michael Brown',
premium: { currency: 'USD', amount: 445.67 },
},
{
id: '45678',
product: {
name: 'Go Commercial Auto',
},
insured: 'Emily Davis',
premium: { currency: 'USD', amount: 2134.89 },
},
{
id: '56789',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'David Wilson',
premium: { currency: 'USD', amount: 823.45 },
},
{
id: '67890',
product: {
name: 'USA Personal Auto',
},
insured: 'Jessica Miller',
premium: { currency: 'USD', amount: 298.76 },
},
{
id: '78901',
product: {
name: 'Go Commercial Auto',
},
insured: 'Christopher Taylor',
premium: { currency: 'USD', amount: 3456.78 },
},
];
Sorting
To implement sorting in the Table component, combine the structure components with a data management layer such as a custom hook that manages sorting state.
The sorting implementation requires managing sort state of current sorted column and direction, and providing sort callbacks to header cells. Use HeaderCell properties, such as isSortable, isSorted, and onSort to display the correct sorting indicators and handle user interactions. You can handle sort changes by updating the data state and applying the appropriate sorting logic to your dataset.
Use a data management layer (such as the useTableDataSorting custom hook) to sort your dataset based on the current column and direction, then pass the sorted data to your table rows. The sorting logic must handle different data types (strings, numbers, dates) and provide consistent ascending and descending order behavior.
Alternatively, you can use TanStack Table as a data management layer, which provides built-in sorting state management and handles column sorting automatically through its useReactTable hook with sorting configuration.
Accessibility
The HeaderCell component automatically handles all necessary accessibility attributes for sorting functionality. When you provide the isSortable and isSorted properties, the component automatically generates appropriate aria-sort attributes (none, ascending, or descending) and descriptive aria-label values that communicate the sorting state to screen readers. You don't need to manually set these accessibility attributes.
However, you can manually override these accessibility labels by passing them as HTML props. Note that manually passed aria-label attributes are treated as native HTML properties and are not automatically translated. If you need internationalization for custom accessibility labels, you must use a translator function yourself:
<HeaderCell aria-label={translator(consumerCustomMessage)} .../>
The component only supports aria-sort values of none, ascending, and descending. If you have a use case for other aria-sort values (such as other for custom sorting that is neither ascending nor descending), you need to pass them manually as the component does not handle these values automatically.
Component implementation
import React, { useId, type ReactNode } from 'react';
import {
BodyCell,
BodyRow,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableHeader,
} from '@jutro/components';
import type { HeaderCellProps, TableProps } from '@jutro/components';
import { Typography } from '@jutro/components';
import { useTableDataSorting } from './useTableDataSorting';
import { TableMockData, tableMockData } from './table.mockdata';
type SortingFn<T> = (row: T) => unknown;
/** Column type definition that specifies the structure for table columns with sorting capabilities. */
/** Each column has an id, header renderer, cell renderer, optional alignment, and sorting configuration. */
export type Column<T> = {
id: string;
header: () => ReactNode;
cell: (row: T) => ReactNode;
align?: 'left' | 'right' | 'center';
accessorFn?: (row: T) => string;
sortingFn?: Array<SortingFn<T>> | SortingFn<T>;
isSortable?: boolean;
};
export type ColumnDefinition<T extends object> = Array<Column<T>>;
/** Column configuration for the table with Product, Insured, and Premium columns with sorting. */
export const columns: ColumnDefinition<TableMockData> = [
{
id: 'product',
header: () => (
<Typography variant="heading-5" tag="span">
Product
</Typography>
),
cell: (row) => <Typography>{row.product.name}</Typography>,
accessorFn: (row) => row.product.name,
sortingFn: ({ product: { name } }) => name,
},
{
id: 'insured',
header: () => (
<Typography variant="heading-5" tag="span">
Insured
</Typography>
),
cell: (row) => <Typography>{row.insured}</Typography>,
accessorFn: (row) => row.insured,
sortingFn: ({ insured }) => insured,
},
{
id: 'premium',
header: () => (
<Typography variant="heading-5" tag="span">
Premium
</Typography>
),
cell: (row) => (
<Typography>
{`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`}
</Typography>
),
align: 'right',
accessorFn: (row) =>
`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`,
sortingFn: ({ premium: { amount } }) => amount,
},
];
export const TableTitleString = 'Policy list';
/** Component that renders a table title section with heading and subtitle. */
export const TableTitle: React.FC<{ tableId: string }> = ({ tableId }) => (
<div>
<div>
<Typography variant="heading-2" id={tableId}>
{TableTitleString}
</Typography>
<Typography role="doc-subtitle">
Detailed record of individual policies
</Typography>
</div>
</div>
);
export function SortingStory(
args: TableProps & HeaderCellProps
): React.ReactElement {
const tableId = useId();
const { data, isColumnSorted, onSortChange } =
useTableDataSorting<TableMockData>({
columns,
data: tableMockData,
});
return (
<div>
<TableTitle tableId={tableId} />
<Table
noStripedRows={args.noStripedRows}
sortingIcons={args.sortingIcons}
className={args.className}
aria-labelledby={tableId}
>
<TableHeader>
<HeaderRow>
{columns.map(({ id, header, align, isSortable = true }, index) => (
<HeaderCell
key={index}
columnIndex={index}
align={align}
isSortable={isSortable}
isSorted={isColumnSorted(id)}
onSort={() => onSortChange(id)}
>
{header()}
</HeaderCell>
))}
</HeaderRow>
</TableHeader>
<TableBody>
{data.map((row, rowIndex) => (
<BodyRow key={row.id}>
{columns.map(({ id, cell, align }, columnIndex) => (
<BodyCell
key={`${id}_${row.id}`}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={align}
>
{cell(row)}
</BodyCell>
))}
</BodyRow>
))}
</TableBody>
</Table>
</div>
);
}
Data management
import { useMemo, useState } from 'react';
import { orderBy } from 'lodash';
import type { ColumnDefinition } from './Sorting';
type SortingDirections = 'asc' | 'desc';
type SortedColumn = {
columnId: string;
direction: SortingDirections;
};
/** Arguments for the useTableDataSorting hook. */
export type UseTableDataSortingArgs<T extends object> = {
columns: ColumnDefinition<T>;
data: T[];
initialState?: {
sortedColumn?: SortedColumn;
};
};
/** Return type for the useTableDataSorting hook. */
export type UseTableDataSortingResult<T extends object> = {
data: T[];
isColumnSorted: (columnId?: string) => false | 'asc' | 'desc';
onSortChange: (columnId?: string) => void;
};
/**
* Utility class for handling table data sorting operations.
* Provides methods for sorting data based on column configurations.
*/
class TableData<T extends object> {
private data: T[];
readonly columns: ColumnDefinition<T>;
constructor(data: T[], columns: ColumnDefinition<T>) {
this.data = data;
this.columns = columns;
}
sort(sortedColumn?: SortedColumn) {
if (!sortedColumn) {
return this;
}
const { columnId, direction } = sortedColumn;
const [column] = this.columns.filter(({ id }) => id === columnId);
if (!column) {
return this;
}
const getSortingCallbacks = () => {
const { sortingFn, accessorFn } = column;
if (!sortingFn) {
return [(row: T) => accessorFn?.(row)];
}
if (!Array.isArray(sortingFn)) {
return [(row: T) => sortingFn(row)];
}
return sortingFn;
};
const sortingCallbacks = getSortingCallbacks();
const sortingDirections = Array(sortingCallbacks.length).fill(
direction
);
this.data = orderBy(this.data, sortingCallbacks, sortingDirections);
return this;
}
getData() {
return this.data;
}
}
/**
* Custom React hook for managing table data with sorting functionality.
* Handles sorting state and provides sorted data along with control functions.
*/
export function useTableDataSorting<T extends object>({
columns: columnsConfig,
data: initialData,
initialState: {
sortedColumn: initialSortedColumn,
} = {},
}: UseTableDataSortingArgs<T>): UseTableDataSortingResult<T> {
const [sortedColumn, setSortedColumn] = useState(initialSortedColumn);
const tableData = useMemo(() => {
const tableDataFactory = new TableData<T>(initialData, columnsConfig);
return tableDataFactory.sort(sortedColumn).getData();
}, [columnsConfig, initialData, sortedColumn]);
const isColumnSorted = (columnId?: string): SortingDirections | false => {
if (!sortedColumn) {
return false;
}
const { columnId: sortedId, direction } = sortedColumn;
return sortedId === columnId ? direction : false;
};
const onSortChange = (columnId?: string) => {
if (!columnId) {
setSortedColumn(undefined);
return;
}
setSortedColumn(current => {
if (!current || current.columnId !== columnId) {
return { columnId, direction: 'desc' };
}
const { direction } = current;
return {
...current,
direction: direction === 'asc' ? 'desc' : 'asc',
};
});
};
return {
data: tableData,
isColumnSorted,
onSortChange,
};
}
import type { CurrencyInputProps } from '@jutro/components';
export type TableMockDataProduct = {
name: string;
};
export type TableMockDataCurrency = NonNullable<CurrencyInputProps['value']>;
export type TableMockData = {
id: string;
product: TableMockDataProduct;
insured: string;
premium: TableMockDataCurrency;
};
export const tableMockData: Array<TableMockData> = [
{
id: '73065',
product: {
name: 'Go Commercial Auto',
},
insured: 'Marshall Rogahn',
premium: { currency: 'USD', amount: 1236.39 },
},
{
id: '80077',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'April Kub',
premium: { currency: 'USD', amount: 173.99 },
},
{
id: '64487',
product: {
name: 'USA Personal Auto',
},
insured: 'Abel Rippin',
premium: { currency: 'USD', amount: 1228.69 },
},
{
id: '12345',
product: {
name: 'Go Commercial Auto',
},
insured: 'John Smith',
premium: { currency: 'USD', amount: 892.45 },
},
{
id: '23456',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'Sarah Johnson',
premium: { currency: 'USD', amount: 567.23 },
},
{
id: '34567',
product: {
name: 'USA Personal Auto',
},
insured: 'Michael Brown',
premium: { currency: 'USD', amount: 445.67 },
},
{
id: '45678',
product: {
name: 'Go Commercial Auto',
},
insured: 'Emily Davis',
premium: { currency: 'USD', amount: 2134.89 },
},
{
id: '56789',
product: {
name: 'Go Worker\'s Compensation',
},
insured: 'David Wilson',
premium: { currency: 'USD', amount: 823.45 },
},
{
id: '67890',
product: {
name: 'USA Personal Auto',
},
insured: 'Jessica Miller',
premium: { currency: 'USD', amount: 298.76 },
},
{
id: '78901',
product: {
name: 'Go Commercial Auto',
},
insured: 'Christopher Taylor',
premium: { currency: 'USD', amount: 3456.78 },
},
];
