Table
Beispiele
Die Tabellen-Komponente bietet eine flexible Struktur für die Anzeige von Daten, die mit verschiedenen Data Management Layers kombiniert werden kann, um verschiedene Funktionen zu implementieren. Optional können Sie externe Bibliotheken wie TanStack Table nutzen, um Teile der Datenverwaltungslogik zu handhaben.
Für Funktionen wie Zeilenaktionen, Suche, Sortierung und Auswahl können Sie sich die folgenden Codebeispiele ansehen, die die Implementierung ausschließlich mit Jutro-Komponenten veranschaulichen. Sie finden auch Beispiele, die TanStack Table als Teil der Datenverwaltungslogik einbinden. Die Beispiele enthalten alle Dateien, die Sie für die Prüfung in Ihrer Umgebung brauchen, z. B. Hooks und Mock-Daten. Für jedes Beispiel wird ein anderer Satz von Dateien verwendet.
Im Abschnitt Verwendung finden Sie Informationen zum korrekten Design einer Tabelle und zu den verschiedenen von Guidewire bereitgestellten Konfigurationsoptionen.
Einfaches Beispiel
Zum Erstellen einer Basisimplementierung einer Table-Komponente müssen Sie die folgenden Komponenten verwenden:
Tablezum Festlegen einer StrukturTableHeaderzum Definieren des KopfzeilenabschnittsTableBodyzum Definieren des HauptteilsHeaderRowzum Definieren einer Zeile im KopfBodyRowzum Definieren einer Zeile im HauptteilHeaderCellzum Definieren einer Zelle im KopfBodyCellzum Definieren einer Zelle im Hauptteil
Wenn Sie diese Komponenten verwenden, müssen Sie die in den Abschnitten Code und Verwendung definierten Regeln befolgen. Dazu gehören die korrekte Verschachtelung der Komponenten und das Hinzufügen geeigneter Beschriftungen für Barrierefreiheit, wie das in diesem Beispiel verwendete aria-label.
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>
);
}
Tabelle mit einem Titel
Die Table-Komponente enthält keine integrierten Titel oder Untertitel. Es wird empfohlen, dass Sie Ihrer Implementierung einen beschreibenden Titel und bei Bedarf einen Untertitel hinzufügen. Sie können sie ganz einfach mit der Jutro-Komponente Typography hinzufügen.
Fügen Sie der Komponente Table geeignete Beschriftungen für Barrierefreiheit wie aria-labelledby und aria-describedby hinzu, um auf den Titel bzw. Untertitel zu verweisen.
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>
);
}
Zeilenaktionen
Zum Implementieren von Zeilenaktionen in der Komponente Table kombinieren Sie die Strukturkomponenten mit einem Data Management Layer, der einzelne Zeilenoperationen verarbeitet, und einer Aktionsspalte, die interaktive Elemente für jede Zeile enthält.
Für die Implementierung von Zeilenaktionen ist es erforderlich, Aktions-Callbacks zu verwalten und diese den in Tabellenzellen platzierten Aktionskomponenten bereitzustellen. Erstellen Sie Aktionsschaltflächen oder Menüs in einer speziellen Spalte, über die einzelne Zeilen bearbeitet werden können, z. B. Bearbeiten, Löschen oder Detailansicht. Diese spezielle Spalte ist in der Regel die letzte Spalte in der Tabelle. Die Aktionskomponenten empfangen Zeilendaten und Aktions-Callbacks als Parameter und können Vorgänge auslösen, die den Datensatz oder den Anwendungsstatus ändern.
Verwenden Sie einen Data Management Layer (z. B. den benutzerdefinierten Hook useTableRowActions), um Zeilenoperationen wie das Hinzufügen, Aktualisieren oder Löschen einzelner Zeilen aus Ihrem Datensatz zu verarbeiten. Die Aktionslogik muss Methoden zum Ausführen dieser Vorgänge und zum Aktualisieren des internen Zustands entsprechend bereitstellen. Bei komplexeren Szenarien wie der Inline-Bearbeitung kombinieren Sie Zeilenaktionen mit einer Verwaltungsschicht für den Bearbeitungsstatus (z. B. dem benutzerdefinierten useEditableRow-Hook), die nachverfolgt, welche Zeilen sich im Bearbeitungsmodus befinden, und temporäre Bearbeitungsdaten verwaltet.
Verbinden Sie die Aktions-Callbacks, indem Sie Funktionen wie deleteRow und saveRow an die Aktionsspalten-Komponenten übergeben. Diese Callbacks werden ausgeführt, wenn Benutzer mit Aktionsschaltflächen interagieren und den Datensatz ändern oder Statusänderungen auslösen. Implementieren Sie für Inline-Bearbeitungsszenarien bedingtes Rendering in Ihren Zellen, bei dem basierend auf dem Bearbeitungszustand der Zeile zwischen Anzeige- und Eingabekomponenten gewechselt wird. Hierbei werden Funktionen wie isRowEditing und renderCell verwendet, um die Anzeigelogik zu verwalten.
Alternativ können Sie TanStack Table als Data Management Layer verwenden. Dies bietet Flexibilität für die Implementierung benutzerdefinierter Zeilenaktionen durch Spaltendefinitionen mit benutzerdefinierten Cell Renderers. Mit TanStack Table werden Zeilenaktionen durch Spaltenkonfigurationen implementiert, die die Aktionskomponenten und ihr Verhalten definieren.
Implementierung von Komponenten
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>
);
}
Datenmanagement
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 },
},
];
Implementierung von Komponenten
import React, { useCallback, useId, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import type { ColumnDef, RowData } from '@tanstack/react-table';
import {
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
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,
EditableProduct,
EditableInsured,
EditablePremium,
} from './Editable';
import { useEditableRow } from './useEditableRow';
import { tableMockData } from './table.mockdata';
import type { TableMockData } from './table.mockdata';
const initialData = tableMockData.slice(0, 10);
/** Tanstack Table module augmentation to extend ColumnMeta interface with alignment and aria-label properties. */
declare module '@tanstack/react-table' {
interface TableMeta<TData extends RowData> {
deleteRows: (rowIds: string[]) => void;
}
interface ColumnMeta<TData extends RowData, TValue> {
align?: 'left' | 'right' | 'center';
ariaLabel?: string;
}
}
/** Column configuration for the table with Product, Insured, and Premium columns. */
export const genericColumns: ColumnDef<TableMockData, any>[] = [
{
id: 'product',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Product
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) => row.product.name,
meta: {
ariaLabel: 'Product',
},
},
{
id: 'insured',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Insured
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorKey: 'insured',
meta: {
ariaLabel: 'Insured',
},
},
{
id: 'premium',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Premium
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) =>
`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`,
meta: {
align: 'right',
ariaLabel: 'Premium',
},
},
];
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 RowActionsTanstackStory(args: TableProps): React.ReactElement {
const tableId = useId();
const [data, setData] = useState(initialData);
const deleteRow = useCallback((rowId: string) => {
setData((current) => current.filter((row) => row.id !== rowId));
}, []);
const saveRow = useCallback(
(rowId: string, rowData: Partial<TableMockData>) => {
setData((current) =>
current.map((row) => {
if (row.id !== rowId) {
return row;
}
return { ...row, ...rowData };
})
);
},
[]
);
const {
rowData,
setAtomicRowData,
onRowSave,
isRowEditing,
startEditing,
abortEditing,
} = useEditableRow<TableMockData>();
// In accordance with the Tanstack Table documentation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columns = useMemo<ColumnDef<TableMockData, any>[]>(
() => [
...genericColumns,
{
id: 'actions',
accessorFn: () => null,
meta: { align: 'right' },
header: () => (
<Typography
tag="span"
variant="heading-5"
>
Actions
</Typography>
),
cell: ({ row: { original } }) => (
<div>
<Button
label={`Delete ${original.id}`}
icon={<DeleteIcon />}
variant="tertiary"
onClick={() => deleteRow(original.id)}
hideLabel
/>
<Button
label={`Edit ${original.id}`}
icon={<EditIcon />}
variant="tertiary"
onClick={() => startEditing(original.id, original)}
hideLabel
/>
</div>
),
},
],
[deleteRow, startEditing]
);
const table = useReactTable({
getCoreRowModel: getCoreRowModel(),
data,
columns,
});
const editableCells = useMemo<Record<string, ReactNode>>(
() => ({
product: (
<EditableProduct
id="product"
value={rowData?.product}
onChange={setAtomicRowData}
/>
),
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, abortEditing, onRowSave, saveRow]
);
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>
{table.getHeaderGroups().map((headerGroup) => (
<HeaderRow key={headerGroup.id}>
{headerGroup.headers.map((header, columnIndex) => (
<HeaderCell
key={header.id}
columnIndex={columnIndex}
align={header.column.columnDef.meta?.align}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</HeaderCell>
))}
</HeaderRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row, rowIndex) => (
<BodyRow key={row.id}>
{row.getVisibleCells().map((cell, columnIndex) => (
<BodyCell
key={cell.id}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={cell.column.columnDef.meta?.align}
>
{renderCell(row.original.id, cell.column.id, () =>
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
</BodyCell>
))}
</BodyRow>
))}
</TableBody>
</Table>
</div>
);
}
Datenmanagement
import { useState, useCallback } from 'react';
/** 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 },
},
];
Suche
Zum Implementieren von Suchfunktionen in der Table-Komponente kombinieren Sie die Strukturkomponenten mit einem Data Management Layer und einer Sucheingabekomponente, die Tabellendaten in Echtzeit filtert.
Die Suchimplementierung erfordert die Verwaltung des Suchabfragestatus und die Bereitstellung von Such-Callbacks für eine Sucheingabe-Komponente. Erstellen Sie eine Sucheingabe-Komponente wie SearchBar, mit der Benutzer Suchanfragen eingeben können, und verbinden Sie sie über eine Suchhandler-Funktion mit Ihrer Tabelle. Die Suchfunktion filtert Ihren Datensatz auf der Grundlage der Suchabfrage. Dabei wird diese mithilfe von Accessor-Funktionen mit durchsuchbaren Spaltenwerten verglichen.
Verwenden Sie einen Data Management Layer (z. B. den benutzerdefinierten Hook useTableDataSearch), um den Status der Suchabfragen zu verwalten, Benutzereingaben für bessere Leistung zu verzögern (debounce) und den Datensatz basierend auf der aktuellen Suchabfrage zu filtern. Das Debouncing ist wichtig, um übermäßige Filtervorgänge während der Benutzereingabe zu verhindern, die bei großen Datensätzen zu Leistungsproblemen führen können. Sie können das Debouncing implementieren, indem Sie Techniken wie setTimeout oder Bibliotheken wie die Lodash-Funktion debounce verwenden, um die Ausführung der Suche zu verzögern, bis der Benutzer die Eingabe beendet hat.
Die Suchlogik muss die Abfrage anhand von accessorFn-Eigenschaften, die in Ihrer Spaltenkonfiguration definiert sind, mit bestimmten Spaltenwerten vergleichen. Für jede Spalte, die durchsuchbar sein soll, ist eine Accessor-Funktion erforderlich, die den durchsuchbaren Text für diese Spalte zurückgibt. Verbinden Sie die Such-Callbacks, indem Sie Funktionen wie onSearchQueryChange an die onChange Eigenschaft Ihrer Sucheingabe-Komponente übergeben.
Alternativ können Sie TanStack Table als Data Management Layer verwenden, die integrierte globale Filterfunktionen bietet. Mit TanStack Table wird die Suchfunktionalität automatisch über einen useReactTable-Hook mit getFilteredRowModel-Funktion und globaler Filterkonfiguration verwaltet.
Implementierung von Komponenten
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>
);
}
Datenmanagement
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 },
},
];
Implementierung von Komponenten
import React, {
useId,
useState,
useEffect,
useRef,
type ReactNode,
} from 'react';
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
} from '@tanstack/react-table';
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 type { ColumnDef, RowData } from '@tanstack/react-table';
import { tableMockData, TableMockData } from './table.mockdata';
/** Tanstack Table module augmentation to extend ColumnMeta interface with alignment and aria-label properties. */
declare module '@tanstack/react-table' {
interface ColumnMeta<TData extends RowData, TValue> {
align?: 'left' | 'right' | 'center';
ariaLabel?: string;
}
}
/** Column configuration for the table with Product, Insured, and Premium columns. */
export const columns: ColumnDef<TableMockData, any>[] = [
{
id: 'product',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Product
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) => row.product.name,
meta: {
ariaLabel: 'Product',
},
},
{
id: 'insured',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Insured
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorKey: 'insured',
meta: {
ariaLabel: 'Insured',
},
},
{
id: 'premium',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Premium
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) =>
`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`,
meta: {
align: 'right',
ariaLabel: 'Premium',
},
},
];
/** 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;
}
{
}
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';
{
}
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 SearchTanstackStory(args: TableProps): React.ReactElement {
const tableId = useId();
const [searchQuery, setSearchQuery] = useState('');
{
}
const debouncedSearch = useDebounce(searchQuery, 300);
const table = useReactTable({
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onGlobalFilterChange: setSearchQuery,
data: tableMockData,
columns,
state: {
globalFilter: debouncedSearch,
},
globalFilterFn: 'includesString',
});
{
}
const renderTableBody = () => {
if (!table.getRowModel().rows.length) {
return (
<TableEmptyState detailedMessageContent="Your search returned no matching policies. Try a different query." />
);
}
return table.getRowModel().rows.map((row, rowIndex) => (
<BodyRow key={row.id}>
{row.getVisibleCells().map((cell, columnIndex) => (
<BodyCell
key={cell.id}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={cell.column.columnDef.meta?.align}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</BodyCell>
))}
</BodyRow>
));
};
return (
<div>
<TableTitle tableId={tableId}>
<div>
<SearchBar
value={searchQuery}
onChange={setSearchQuery}
/>
</div>
</TableTitle>
<Table
aria-labelledby={tableId}
className={args.className}
noStripedRows={args.noStripedRows}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<HeaderRow key={headerGroup.id}>
{headerGroup.headers.map((header, columnIndex) => (
<HeaderCell
key={header.id}
columnIndex={columnIndex}
align={header.column.columnDef.meta?.align}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</HeaderCell>
))}
</HeaderRow>
))}
</TableHeader>
<TableBody>{renderTableBody()}</TableBody>
</Table>
</div>
);
}
Datenmanagement
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 },
},
];
Auswahl
Um die Selektion in der Table-Komponente zu implementieren, kombinieren Sie die Strukturkomponenten mit einem Data Management Layer, der den Auswahlstatus für einzelne Zeilen- und Massenoperationen verwaltet.
Die Implementierung der Auswahl erfordert die Verwaltung des Auswahlstatus einzelner Zeilen und die Bereitstellung von Auswahl-Callbacks für Ihre Tabellenzellen. Verwenden Sie die selected-Eigenschaft in der BodyRow-Komponente, um anzugeben, ob eine Zeile ausgewählt ist. Dieser boolesche Wert kann von untergeordneten Komponenten verwendet werden, um den Zeilenstatus zu bestimmen.
Handhaben Sie Auswahlinteraktionen über Checkbox-Komponenten, die in BodyCell- und HeaderCell-Komponenten platziert sind. Sie können Auswahländerungen handhaben, indem Sie den Auswahlstatus aktualisieren und eine Liste ausgewählter Zeilenbezeichner pflegen. Platzieren Sie einzelne Zeilen-Kontrollkästchen in der ersten Spalte jeder Zeile und fügen Sie für Massenauswahlvorgänge wie „Alle auswählen“ oder „Alle löschen“ ein Kopfzeilen-Kontrollkästchen in der entsprechenden HeaderCell hinzu.
Verwenden Sie einen Data Management Layer (z. B. den benutzerdefinierten Hook useTableDataSelection), um nachzuverfolgen, welche Zeilen ausgewählt sind, um die Auswahl einzelner Zeilen zu verarbeiten und Massenauswahlvorgänge wie „Alle auswählen“ oder „Alle löschen“ zu verwalten. Die Auswahllogik muss das Hinzufügen und Entfernen von Elementen aus dem Auswahlsatz verarbeiten und Methoden zur Überprüfung des Auswahlstatus bereitstellen. Verknüpfen Sie die Auswahl-Callbacks mit Checkbox-Komponenten, indem Sie Funktionen wie toggleRowSelection und toggleRowsSelection an die onChange-Eigenschaften der jeweiligen Kontrollkästchen übergeben. Diese Callbacks aktualisieren den internen Auswahlstatus, wenn Benutzer mit den Kontrollkästchen interagieren.
Alternativ können Sie TanStack Table als Data Management Layer verwenden, die eine integrierte Verwaltung des Zeilenauswahlstatus bietet. Mit TanStack Table wird die Auswahl automatisch über den useReactTable-Hook mit Zeilenauswahlkonfiguration gehandhabt.
Implementierung von Komponenten
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>
);
}
Datenmanagement
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 },
},
];
Implementierung von Komponenten
import React, { useId, useMemo, useState, type ReactNode } from 'react';
import type { ColumnDef, RowData } from '@tanstack/react-table';
import {
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
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 { TableMockData, tableMockData } from './table.mockdata';
/** Tanstack Table module augmentation to extend ColumnMeta interface with alignment and aria-label properties. */
declare module '@tanstack/react-table' {
interface TableMeta<TData extends RowData> {
deleteRows: (rowIds: string[]) => void;
}
interface ColumnMeta<TData extends RowData, TValue> {
align?: 'left' | 'right' | 'center';
ariaLabel?: string;
}
}
/** Column configuration for the table with Product, Insured, and Premium columns. */
export const basicColumns: ColumnDef<TableMockData, any>[] = [
{
id: 'product',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Product
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) => row.product.name,
meta: {
ariaLabel: 'Product',
},
},
{
id: 'insured',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Insured
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorKey: 'insured',
meta: {
ariaLabel: 'Insured',
},
},
{
id: 'premium',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Premium
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) =>
`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`,
meta: {
align: 'right',
ariaLabel: 'Premium',
},
},
];
/** 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 SelectionTanstackStory(args: TableProps): React.ReactElement {
const [data, setData] = useState(tableMockData);
const tableId = useId();
const columns = useMemo<ColumnDef<TableMockData, any>[]>(
() => [
{
id: 'selection',
accessorFn: () => null,
header: ({ table }) => {
const allSelected = table.getIsAllRowsSelected();
const someSelected = table.getIsSomeRowsSelected();
const icon = allSelected ? <CheckIcon /> : <RemoveIcon />;
return (
<Checkbox
label=""
aria-label={
allSelected || someSelected ? 'Deselect all' : 'Select all'
}
checked={allSelected || someSelected}
onChange={table.getToggleAllRowsSelectedHandler()}
icon={icon}
/>
);
},
cell: ({ row }) => (
<Checkbox
label=""
aria-label={
row.getIsSelected()
? `Deselect ${row.original.id}`
: `Select ${row.original.id}`
}
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
...basicColumns,
],
[]
);
const table = useReactTable({
getCoreRowModel: getCoreRowModel(),
data,
columns,
enableRowSelection: true,
getRowId: (row) => row.id,
meta: {
deleteRows: (rowIds: string[]) => {
setData((oldRows) => oldRows.filter(({ id }) => !rowIds.includes(id)));
},
},
});
const selectedRows = Object.keys(table.getState().rowSelection);
const onDeleteRows = () => {
table.options.meta?.deleteRows(selectedRows);
table.resetRowSelection();
};
const isAnyRowSelected =
table.getIsSomeRowsSelected() || table.getIsAllRowsSelected();
return (
<div>
<TableTitle tableId={tableId}>
{isAnyRowSelected && (
<div>
<Button
label="Delete selected rows"
onClick={onDeleteRows}
/>
</div>
)}
</TableTitle>
<SelectedRowsCounter
count={selectedRows.length}
onClear={() => table.toggleAllRowsSelected(false)}
/>
<Table
aria-labelledby={tableId}
className={args.className}
noStripedRows={args.noStripedRows}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<HeaderRow key={headerGroup.id}>
{headerGroup.headers.map((header, columnIndex) => (
<HeaderCell
key={header.id}
columnIndex={columnIndex}
align={header.column.columnDef.meta?.align}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</HeaderCell>
))}
</HeaderRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row, rowIndex) => (
<BodyRow
key={row.id}
selected={row.getIsSelected()}
>
{row.getVisibleCells().map((cell, columnIndex) => (
<BodyCell
key={cell.id}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={cell.column.columnDef.meta?.align}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</BodyCell>
))}
</BodyRow>
))}
</TableBody>
</Table>
</div>
);
}
Datenmanagement
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 },
},
];
Sortierung
Um die Sortierung in der Table-Komponente zu implementieren, kombinieren Sie die Strukturkomponenten mit einem Data Management Layer, z. B. einem benutzerdefinierten Hook, der den Sortierstatus verwaltet.
Das Implementieren der Sortierung erfordert die Verwaltung des Sortierstatus der aktuell sortierten Spalte und Sortierreihenfolge sowie die Bereitstellung von Sortier-Callbacks für Headerzellen. Verwenden Sie HeaderCell-Eigenschaften wie isSortable, isSorted und onSort, um die richtigen Sortierindikatoren anzuzeigen und die Benutzerinteraktionen zu handhaben. Sie können Sortieränderungen vornehmen, indem Sie den Datenstatus aktualisieren und die entsprechende Sortierlogik auf Ihren Datensatz anwenden.
Verwenden Sie einen Data Management Layer (z. B. den benutzerdefinierten Hook useTableDataSorting), um den Datensatz basierend auf der aktuellen Spalte und Sortierreihenfolge zu sortieren und die sortierten Daten dann an die Tabellenzeilen zu übergeben. Die Sortierlogik muss mit verschiedenen Datentypen (Zeichenfolgen, Zahlen, Datumsangaben) umgehen können und ein konsistentes Verhalten für die auf- und absteigende Sortierreihenfolge bieten.
Alternativ können Sie TanStack Table als Data Management Layer verwenden, die eine integrierte Verwaltung des Sortierzustands bietet und die Spaltensortierung automatisch über den useReactTable-Hook mit Sortierkonfiguration übernimmt.
Barrierefreiheit
Die HeaderCell-Komponente verarbeitet automatisch alle erforderlichen Barrierefreiheitsattribute der Sortierfunktion. Wenn Sie die Eigenschaften isSortable und isSorted bereitstellen, generiert die Komponente automatisch die entsprechenden aria-sort-Attribute (none, ascending oder descending) und beschreibende aria-label-Werte, die den Sortierstatus für Screenreader angeben. Sie müssen diese Barrierefreiheitsattribute nicht manuell festlegen.
Sie können diese barrierefreien Beschriftungen jedoch manuell überschreiben, indem Sie sie als HTML-Eigenschaften übergeben. Beachten Sie, dass manuell übergebene aria-label-Attribute als native HTML-Eigenschaften behandelt und nicht automatisch übersetzt werden. Wenn Sie die Internationalisierung für benutzerdefinierte barrierefreie Beschriftungen benötigen, müssen Sie selbst eine Übersetzerfunktion verwenden:
<HeaderCell aria-label={translator(consumerCustomMessage)} .../>
Die Komponente unterstützt nur die aria-sort-Werte none, ascending und descending. Wenn Sie einen Anwendungsfall für andere aria-sort-Werte haben (z. B. other für eine benutzerdefinierte Sortierung, die weder aufsteigend noch absteigend ist), müssen Sie diese manuell übergeben, da die Komponente diese Werte nicht automatisch verarbeitet.
Implementierung von Komponenten
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>
);
}
Datenmanagement
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 },
},
];
Implementierung von Komponenten
import React, { useId } from 'react';
import {
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
BodyCell,
BodyRow,
HeaderCell,
HeaderRow,
Table,
TableBody,
TableHeader,
} from '@jutro/components';
import { Typography } from '@jutro/components';
import type { HeaderCellProps, TableProps } from '@jutro/components';
import type { ColumnDef, RowData } from '@tanstack/react-table';
import { TableMockData, tableMockData } from './table.mockdata';
/** Tanstack Table module augmentation to extend ColumnMeta interface with alignment and aria-label properties. */
declare module '@tanstack/react-table' {
interface ColumnMeta<TData extends RowData, TValue> {
align?: 'left' | 'right' | 'center';
ariaLabel?: string;
}
}
/** Column configuration for the table with Product, Insured, and Premium columns. */
export const columns: ColumnDef<TableMockData, any>[] = [
{
id: 'product',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Product
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) => row.product.name,
meta: {
ariaLabel: 'Product',
},
},
{
id: 'insured',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Insured
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorKey: 'insured',
meta: {
ariaLabel: 'Insured',
},
},
{
id: 'premium',
header: () => (
<Typography
variant="heading-5"
tag="span"
>
Premium
</Typography>
),
cell: (props) => <Typography>{props.getValue()}</Typography>,
accessorFn: (row) =>
`${row.premium.currency} ${row.premium.amount?.toFixed(2)}`,
meta: {
align: 'right',
ariaLabel: 'Premium',
},
},
];
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 SortingTanstackStory(
args: TableProps & HeaderCellProps
): React.ReactElement {
const tableId = useId();
const table = useReactTable({
getCoreRowModel: getCoreRowModel(),
data: tableMockData,
columns,
getSortedRowModel: getSortedRowModel(),
enableSortingRemoval: false,
sortDescFirst: true,
});
return (
<div>
<TableTitle tableId={tableId} />
<Table
noStripedRows={args.noStripedRows}
className={args.className}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<HeaderRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => (
<HeaderCell
key={header.id}
columnIndex={index}
onSort={header.column.getToggleSortingHandler()}
isSortable={header.column.getCanSort()}
isSorted={header.column.getIsSorted()}
align={header.column.columnDef.meta?.align}
aria-label={header.column.columnDef.meta?.ariaLabel}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</HeaderCell>
))}
</HeaderRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row, rowIndex) => (
<BodyRow key={row.id}>
{row.getVisibleCells().map((cell, columnIndex) => (
<BodyCell
key={cell.id}
rowIndex={rowIndex}
columnIndex={columnIndex}
align={cell.column.columnDef.meta?.align}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</BodyCell>
))}
</BodyRow>
))}
</TableBody>
</Table>
</div>
);
}
Datenmanagement
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 },
},
];
Verwendung
Überblick
Die Tabelle ist eine Komponente, mit der große, komplexe Datensätze in einem scanbaren, leicht lesbaren Format angezeigt und bearbeitet werden können. In Tabellen werden Informationen in Zeilen und Spalten angeordnet, sodass Benutzer Daten vergleichen, Muster erkennen und Maßnahmen ergreifen können.
Zu verwenden
- Zur Darstellung komplexer Daten, die eine klare Organisation und Struktur erfordern.
- In Fällen, in denen die Reihenfolge der Datendarstellung wichtig ist und Benutzer die Sortierung steuern müssen.
- Wenn Benutzer Daten desselben Typs in verschiedenen Elementen vergleichen müssen.
Nicht zu verwenden
- Wenn die Reihenfolge der Darstellung von Daten nicht so wichtig ist wie die Daten selbst. Verwenden Sie stattdessen Kartenansichten oder Listenansichten.
- Wenn der Anwendungsfall keinen direkten Vergleich von Daten erfordert. Verwenden Sie stattdessen Kartenansichten oder Listenansichten.
- Wenn bei der Arbeit mit kleinen Datenmengen viel Leerräume entstehen würden. Nutzen Sie stattdessen ggf. eine Kartenansicht oder Listenansicht.
Formatierung
Aufbau

Die Tabellenkomponente besteht aus den folgenden Elementen:
- Titel und Untertitel: Eine Überschrift, die den Inhalt der Tabelle beschreibt.
- Tabellensteuerelemente: Eine Reihe von Steuerelementen zum Ändern der Anzeige von Daten, z. B. Suche und Filter.
- Tabellenaktionen: Eine Gruppe von Schaltflächen, die eine Aktion mit dem Datensatz der Tabelle ausführen, wie beispielsweise das Hinzufügen eines neuen Eintrags oder das Exportieren von Daten.
- Spaltenüberschriften: Beschriftungen für jede Spalte, die den Datentyp angeben. Kopfzeilen können auch eine Sortierfunktion und andere Aktionen enthalten.
- Datenzeilen: Ein vollständiger Satz von Zellen, die ein einzelnes Element oder einen Datensatz darstellen. Zeilen können Auswahlsteuerelemente (Kontrollkästchen) und Erweitern-Symbole enthalten.
- Zeilenaktionen: Eine Reihe von Aktionen in der letzten Spalte, die ein Benutzer für eine einzelne Zeile ausführen kann.
- Paginierung: Steuerelemente, mit denen Benutzer durch Datenseiten navigieren und die Anzahl der pro Seite angezeigten Zeilen anpassen können.
Ausrichtung und Positionierung
Standardmäßig werden die Spalten so verkleinert, dass sie an den Inhalt angepasst werden, mit Ausnahme der letzten Spalte, die so erweitert wird, dass sie den verbleibenden verfügbaren Platz ausfüllt. Auf diese Weise bleibt die Scanbarkeit (Übersichtlichkeit) der Informationen erhalten.
Wenn eine Tabelle Zeilenaktionen enthält, müssen diese in dieser letzten Spalte platziert werden.
Inhalt
Allgemeine Richtlinien fürs Schreiben
- Verwenden Sie die Großschreibung des ersten Worts und von Substantiven für alle Aspekte der Gestaltung von Guidewire-Produktschnittstellen. Schreiben Sie nicht alles groß.
- Verwenden Sie in den meisten Situationen Verben im Präsens und im Aktiv.
- Verwenden Sie gebräuchliche Abkürzungen, um Ihrem Text einen natürlichen und informellen Ton zu verleihen.
- Verwenden Sie eine einfache Sprache. Vermeiden Sie unnötigen Jargon und komplexe Sprache.
- Halten Sie Wörter und Sätze kurz.
Halten Sie den Zelleninhalt kurz und bündig
Der Inhalt einer Tabellenzelle sollte nicht mehr als 50 Zeichen enthalten, um die Lesbarkeit zu gewährleisten. Bei längeren Inhalten schafft die Komponente Überläufe nach folgenden spezifischen Regeln:
- Text zwischen 81 und 159 Zeichen wird in mehrere Zeilen umgebrochen.
- Text mit 160 Zeichen oder mehr wird durch Auslassungspunkte (...) abgeschnitten und der vollständige Text wird in einer Quickinfo angezeigt, wenn Sie darauf zeigen.
- Numerische Daten müssen immer vollständig sichtbar sein und dürfen nicht umgebrochen oder abgeschnitten werden.
- Bei anderen Datenformaten wird bei Bedarf ein Zeilenumbruch durchgeführt.
Schreiben Sie eindeutige Spaltenüberschriften
Überschriften müssen kurz, beschreibend und in der üblichen Groß- und Kleinschreibung verfasst sein.
- Beschränken Sie Überschriften auf ein oder zwei Wörter. Dies verbessert die Scanbarkeit und stellt die Lesbarkeit der Tabellendaten sicher.
- **Beginnen Sie Überschriften nach Möglichkeit mit einem Substantiv.**Überschriften sind die Beschriftungen für die Daten in der Spalte, sodass die Verwendung eines Substantivs (z. B. Name, Status, Erstellungsdatum) die eindeutigste Möglichkeit ist, diese Daten zu beschreiben.
Zahlen einheitlich formatieren
Wenn Sie in unserer Ausgangssprache (amerikanisches Englisch) schreiben, formatieren Sie Zahlen mit einem Komma als Tausendertrennzeichen in allen Zahlen mit vier oder mehr Ziffern. Verwenden Sie keine Kommas für Jahre (außer f ünf oder mehr Ziffern), Seitenzahlen, Adressen oder Dezimalstellen.
Befolgen Sie für andere Gebietsschemata die festgelegten Lokalisierungsrichtlinien für die Zahlenformatierung.
Leerzustände klar erläutern
Schreiben Sie direkte, hilfreiche Meldungen, die erklären, warum die Tabelle leer ist und, wenn möglich, was der Benutzer als nächstes tun kann.
Verhalten
Zustände
Eine Tabelle kann mehrere Zustände haben, um den Status der Daten zu kommunizieren.
- Laden: Wenn Daten abgerufen werden, wird im Hauptteil der Tabelle eine Ladeanzeige mit dem Text „Daten werden geladen...“ angezeigt.
- Leer: Wenn eine Tabelle keine Daten enthält, wird ein Leerzustand angezeigt. Die Meldung muss hilfreich sein. Eine Aktionsschaltfläche wird dringend empfohlen, um den Benutzer weiter zu führen. Wenn Sie beispielsweise eine Design für den Status „Keine Suchergebnisse“ entwerfen, zeigen Sie die Schaltfläche Filter löschen an, wenn Filter aktiv sind. Der Schaltflächentyp (primär, sekundär und tertiär) kann je nach Anwendungsfall konfiguriert werden.
- Fehler: Wenn Daten aufgrund eines Systemfehlers (z. B. einer unterbrochenen Internetverbindung) nicht geladen werden können, muss das Problem im Leerzustand kommuniziert und eine Wiederherstellungsaktion bereitgestellt werden, z. B. die Schaltfläche Aktualisieren.
- Zeilenstatus: Zeilen können sich im standardmäßigen, ausgewählten (hervorgehobenen) oder erweiterten Status befinden.
Interaktionen
- Sortierung: Benutzer können auf einen Spaltentitel klicken, um die Tabellendaten nach dieser Spalte zu sortieren. Ein Richtungssymbol in der Kopfzeile zeigt die aktuelle Sortierreihenfolge an.
- Auswählen: Benutzer können eine oder mehrere Zeilen über Kontrollkästchen in der ersten Spalte auswählen. Über ein Kontrollkästchen in der Überschrift können Benutzer alle sichtbaren Zeilen aktivieren oder deaktivieren. Wenn Zeilen ausgewählt sind, kann oben in der Tabelle eine kontextbezogene Auswahlleiste angezeigt werden.
- Erweitern von Zeilen: Wenn eine Zeile erweiterbar ist, können Benutzer auf das Chevron-Symbol klicken, um zusätzliche, verwandte Informationen direkt unter der Zeile anzuzeigen.
- Zeilenaktionen: Benutzer führen mithilfe der Steuerelemente in der letzten Spalte Aktionen in einer einzelnen Zeile aus. Zeigen Sie maximal zwei primäre Symbolaktionen an. Platzieren Sie alle zusätzlichen Aktionen in einem Menü mit Auslassungspunkten (...).
- Paginierung: Benutzer können mithilfe der Seitensteuerungselemente am unteren Rand der Tabelle zwischen Datenseiten navigieren.
Reaktionsfähigkeit und Anpassungsfähigkeit
Die Tabellenkomponente ist vollständig responsiv. Für Mobile-First-Nutzungsfälle bietet eine Kartenansicht jedoch häufig eine bessere Benutzererfahrung als eine herkömmliche mehrspaltige Tabelle.
Barrierefreiheit
Diese Komponente wurde validiert, um die Richtlinien für Barrierefreiheit WCAG 2.2 AA in der Standardbasiskonfiguration zu erfüllen. Dazu wird u. a. Folgendes sichergestellt:
- Das Kontrastverhältnis von Textelementen zu ihrem Hintergrund liegt über 4,5:1.
- Nicht-textuelle Inhalte, die eine Bedeutung vermitteln sollen (z. B. Symbole und Fokusanzeigen), weisen ein Kontrastverhältnis von mindestens 3:1 zu den angrenzenden Farben auf.
- Das jeweilige Element kann sowohl über eine Tastatur als auch über eine Maus bedient werden.
- Der Zugriff auf die Inhalte erfolgt über Screenreader wie JAWS oder VoiceOver.
Die Konformität mit den Richtlinien für Barrierefreiheit hängt letztendlich davon ab, wie diese Komponente implementiert und angepasst wird. Vom Autor des Inhalts vorgenommene Änderungen können sich auf die Barrierefreiheit auswirken. Details zu unserem Modell der geteilten Verantwortung finden Sie in unserer Erklärung zur Barrierefreiheit von Jutro.
Stellen Sie bei Verwendung dieser Komponente in Ihrer Anwendung Folgendes sicher:
- Ein beschreibender Titel wird bereitgestellt. Ein Titel, idealerweise unter Verwendung des
<caption>-Elements, hilft Benutzern mit Screenreadern, den Zweck der Tabelle zu verstehen, bevor sie mit der Navigation durch die Daten beginnen. - Spaltenüberschriften sind korrekt definiert. Die Überschriften müssen richtig strukturiert sein (Verwendung von
<th>mit einemscope="col"-Attribut), damit Screenreader beim Navigieren des Benutzers die richtige Überschrift für jede Datenzelle ausgeben können. - Alle interaktiven Elemente sind barrierefrei. Alle Steuerelemente in der Tabelle, z. B. Schaltflächen oder Links, müssen einen klaren, barrierefreien Namen haben. Bei Schaltflächen, die nur aus Symbolen bestehen, muss ein beschreibendes
aria-labelvorhanden sein (z. B.aria-label="Delete item"). - Der aktuelle Sortierstatus wird bekannt gegeben. Wenn eine Spalte sortiert wird, muss ihre Überschrift für Screenreader die Sortierrichtung (aufsteigend oder absteigend) mitteilen. In der Regel wird dabei das
aria-sort-Attribut verwendet. - Zeigt an, wann Inhalte geladen werden. Legen Sie das Attribut
aria-busy="true"für die Tabelle fest, wenn ihr Inhalt aktiv geladen oder aktualisiert wird.
Tastaturnavigation
Die Tabelle bietet eine umfassende Tastaturnavigation nach dem Rastermuster.
Navigationstasten
-
Pfeiltasten: Navigieren zwischen benachbarten Zellen.
←(Pfeil nach links) – Wechselt zur vorherigen Zelle in der aktuellen Zeile.→(Pfeil nach rechts) – Wechselt zur nächsten Zelle in der aktuellen Zeile.↑(Pfeil nach oben) – Wechselt zur Zelle darüber in derselben Spalte.↓(Pfeil nach unten) – Wechselt zur Zelle darunter in derselben Spalte.
-
TAB-Navigation – Sequenzielle Navigation durch die Tabelle.
Tab– Wechselt zum nächsten fokussierbaren Element.Shift + Tab– Wechselt zum vorherigen fokussierbaren Element.
-
Seitennavigation – Schnelle Navigation für große Tabellen.
Page Up– Wechselt in die erste Zeile der aktuellen Spalte.Page Down– Wechselt in die letzte Zeile der aktuellen Spalte.
-
Begrenzungsnavigation – Springt an die Tabellengrenzen.
Home– Wechselt zur ersten Zelle in der aktuellen Zeile.Ctrl + Home– Wechselt zur ersten Zelle in der Tabelle (oben links).End– Wechselt zur letzten Zelle in der aktuellen Zeile.Ctrl + End– Wechselt zur letzten Zelle in der Tabelle (unten rechts).
Unterstützung interaktiver Inhalte
Das Tastaturnavigationssystem behandelt Zellen auf intelligente Weise, die interaktive Elemente enthalten:
- Schaltflächen und Links – Interaktive Elemente in Zellen (z. B. Sortierschaltflächen oder Zeilenaktionen) bleiben sowohl über die standardmäßige TAB-Navigation als auch über die Pfeiltastennavigation zugänglich. Wenn eine Zelle bei der Navigation mit Pfeiltasten ein interaktives Element (z. B. Eingabefeld, Schaltfläche oder Link) mit einem
tabindex-Wert größer als-1enthält, wird der Fokus auf dieses Element und nicht auf die Zelle gesetzt. Wenn sich mehrere interaktive Elemente in einer Zelle befinden, wird der Fokus auf das erste Element gesetzt. - Benutzerdefinierter fokussierbarer Inhalt – Jedes Element mit
tabindex-Attribut wird ordnungsgemäß in den Navigationsablauf integriert. - Formulareingaben – Wenn eine Zelle Eingabefelder,
textarea-Elemente oder andere bearbeitbareform-Elemente enthält, wird bei der Navigation der Bearbeitungsmodus beibehalten. Wenn der Fokus auf einem solchen Element liegt, funktionieren die Pfeiltasten wie erwartet für die Navigation innerhalb des Elements. Um sich vom Element zu entfernen, verwenden SieTaboderShift + Tab.
Fokusmanagement
In der Tabelle werden erweiterte Fokusmanagement-Techniken verwendet:
- Roving-Tabindex-Muster – Es kann jeweils nur eine Zelle in der Tabelle fokussiert werden. Dadurch werden die Tabstopps reduziert und die Effizienz der Navigation verbessert.
- Begrenzungserkennung – Navigationstasten respektieren Tabellengrenzen und verschieben den Fokus nicht außerhalb des Tabellenrasters.
- Visuelle Fokusindikatoren – Eindeutiges visuelles Feedback zeigt an, welche Zelle gerade den Fokus hat.
Damit das Fokusmanagement ordnungsgemäß funktioniert, stellen Sie sicher, dass die Eigenschaften rowIndex und columnIndex für die BodyCell-Komponente korrekt festgelegt sind und dass die columnIndex-Eigenschaft für die HeaderCell-Komponente korrekt festgelegt ist. Weitere Einzelheiten finden Sie unter Komponentenverträge .
Code
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>
);
}
Importanweisung
import {
Table,
TableHeader,
TableBody,
HeaderRow,
HeaderCell,
BodyRow,
BodyCell,
} from '@jutro/components';
Komponentenvertrag
Vergewissern Sie sich, dass Sie die API-Oberfläche der Design System-Komponenten und die damit verbundenen Auswirkungen und Kompromisse verstehen. Erfahren Sie mehr in unserer Einführung zur Komponenten-API.
Table
Die Table-Komponente definiert die Struktur der Tabelle.
Sie akzeptiert TableHeader und TableBody als untergeordnete Elemente. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Zusätzlich zu den benutzerdefinierten Eigenschaften der Table-Komponente können Sie ihr Standard-HTML-Attribute übergeben.
noStripedRows- Typ
booleanBeschreibungDetermines whether the table displays alternating striped rows for better visual distinction.
Standardwertfalse sortingIcons- Typ
{ ascendingIcon: element, descendingIcon: element, unsortedIcon: element }BeschreibungDefines custom icons for column sorting buttons, including ascending, descending, and unsorted states.
TableHeader
Die TableHeader-Komponente definiert den Überschriftenteil der Tabelle.
Sie muss als untergeordnetes Element der Table-Komponente verwendet werden und akzeptiert nur HeaderRow als untergeordnetes Element. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Die TableHeader-Komponente hat keine benutzerdefinierten Eigenschaften, aber Sie können ihr Standard-HTML-Attribute übergeben.
TableBody
Die TableBody-Komponente definiert den Hauptteil der Tabelle.
Sie muss als untergeordnetes Element der Table-Komponente verwendet werden und akzeptiert nur BodyRow als untergeordnetes Element. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Die TableBody-Komponente hat keine benutzerdefinierten Eigenschaften, aber Sie können ihr Standard-HTML-Attribute übergeben.
HeaderRow
Die HeaderRow-Komponente definiert eine Zeile im Überschriftenteil der Tabelle.
Sie muss als untergeordnetes Element der TableHeader-Komponente verwendet werden und akzeptiert nur HeaderCell als untergeordnetes Element. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Die HeaderRow-Komponente hat keine benutzerdefinierten Eigenschaften, aber Sie können ihr Standard-HTML-Attribute übergeben.
BodyRow
Die BodyRow-Komponente definiert eine Zeile im Hauptteil der Tabelle.
Sie muss als untergeordnetes Element der TableBody-Komponente verwendet werden und akzeptiert nur BodyCell als untergeordnetes Element. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Zusätzlich zu den benutzerdefinierten Eigenschaften der BodyRow-Komponente können Sie ihr Standard-HTML-Attribute übergeben.
selected- Typ
booleanBeschreibungIndicates whether the row is selected. Used in the row selection feature implementation.
HeaderCell
Die HeaderCell-Komponente definiert eine Zelle im Überschriftenteil der Tabelle. Sie wird mit Spaltenüberschriften gefüllt.
Sie muss als untergeordnetes Element der HeaderRow-Komponente verwendet werden. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Zusätzlich zu den benutzerdefinierten Eigenschaften der HeaderCell-Komponente können Sie ihr Standard-HTML-Attribute übergeben.
columnIndexerforderlich- Typ
numberBeschreibungSets the column index. Must be explicitly set to ensure accurate accessibility semantics when rendering multiple cells. The column index must start from 0.
align- Typ
'left' | 'center' | 'right'BeschreibungSets the alignment of the header cell content.
Standardwert'left' isSortable- Typ
booleanBeschreibungIndicates whether the column is sortable or not. Used in the sorting feature implementation.
Standardwertfalse isSorted- Typ
false | 'asc' | 'desc'BeschreibungIndicates the sorting state of the column. Used in the sorting feature implementation.
onSort- Typ
(event: unknown) => voidBeschreibungCallback function triggered when a user clicks or activates the sortable column header. Use it to handle sorting logic, such as toggling between ascending, descending, and unsorted states, and updating the table data accordingly.
BodyCell
Die BodyCell-Komponente definiert eine Zelle im Hauptteil der Tabelle. Jede einzelne Zelle in den Zeilen wird mit entsprechenden Daten gefüllt.
Sie muss als untergeordnetes Element der BodyRow-Komponente verwendet werden. Weitere Informationen finden Sie im Abschnitt Verschachtelung .
Eigenschaften
Zusätzlich zu den benutzerdefinierten Eigenschaften der BodyCell-Komponente können Sie ihr Standard-HTML-Attribute übergeben.
columnIndexerforderlich- Typ
numberBeschreibungSets the column index. Must be explicitly set to ensure accurate accessibility semantics when rendering multiple cells. The column index must start from 0.
rowIndexerforderlich- Typ
numberBeschreibungSets the row index. Must be explicitly set to ensure accurate accessibility semantics when rendering multiple cells. The row index must start from 0.
align- Typ
'left' | 'center' | 'right'BeschreibungSets the horizontal alignment of the body cell content.
Standardwert'left'
Benutzerdefinierte Verhalten
Verschachtelung
Die Table-Komponente definiert die Struktur der Tabelle über die children-Eigenschaften des ReactNode-Typs. Die Komponenten der Tabellenstruktur sind folgendermaßen zu verwenden:
- Die
Table-Komponente akzeptiertTableHeaderundTableBodyals untergeordnete Elemente. - Die
TableHeader-Komponente akzeptiertHeaderRowals untergeordnetes Element und darf nur im Kontext vonTableverwendet werden. - Die
TableBody-Komponente akzeptiertBodyRowals untergeordnetes Element und darf nur im Kontext vonTableverwendet werden. - Die
HeaderRow-Komponente akzeptiertHeaderCellals untergeordnetes Element und darf nur im Kontext vonTableHeaderverwendet werden. - Die
BodyRow-Komponente akzeptiertBodyCellals untergeordnetes Element und darf nur im Kontext vonTableBodyverwendet werden. - Die
HeaderCell-Komponente darf nur im Kontext vonHeaderRowverwendet werden. - Die
BodyCell-Komponente darf nur im Kontext vonBodyRowverwendet werden.
Auch wenn diese Komponenten möglicherweise andere Typen oder HTML-Elemente anzeigen oder verarbeiten können, wird dies nicht unterstützt.
Gestreifte Zeilen
Standardmäßig werden in der Tabelle wechselnde Zeilenhintergrundfarben (gestreifte Zeilen) angezeigt, um die Lesbarkeit zu verbessern und den Benutzern die Nachverfolgung von Daten über Spalten hinweg zu erleichtern. Dieses Verhalten können Sie deaktivieren, indem Sie die noStripedRows-Eigenschaft in der Table-Komponente auf true setzen. Dies empfiehlt sich, wenn Sie mit Tabellen arbeiten, die sehr wenige Zeilen enthalten, oder wenn Sie benutzerdefinierte Zeilenformate implementieren, die mit den standardmäßigen gestreiften Zeilen in Konflikt stehen.
Beispiel mit noStripedRows-Eigenschaft auf true:
Hooks
Für Table sind keine Hooks verfügbar.
Übersetzungsschlüssel
Es gibt keine Übersetzungen für Table.
Informationen über die Verwaltung von Übersetzungen finden Sie in unserem Abschnitt über Internationalisierung.
Escape Hatches
Weitere Informationen finden Sie in unserer Dokumentation über Escape Hatches.
Übergeben von HTML-Attributen an Komponenten
Sie können HTML-Standardattribute (z. B. id, className und style) von entsprechenden HTML-Elementen an alle Tabellenkomponenten übergeben. Diese Attribute werden auf das Stammelement der jeweiligen Komponente angewendet.
Stammelemente von Tabellenkomponenten
| Tabellenkomponente | HTML-Element |
|---|---|
Table | <table> |
TableHeader | <thead> |
TableBody | <tbody> |
HeaderRow | <tr> |
BodyRow | <tr> |
HeaderCell | <th> |
BodyCell | <td> |
Beispiel
In diesem Beispiel wird das aria-labelledby-Attribut an die Table-Komponente übergeben. Es referenziert die id der Typography-Komponente, die als Tabellentitel dient, und liest den Textinhalt dieser Komponente als barrierefreien Namen der Tabelle.
export function TableHTMLPropExample() {
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>
<Typography variant="heading-3" id="policy-list-title">
Policy list
</Typography>
<Table aria-labelledby="policy-list-title">
<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>
);
}