Tabla
Ejemplos
El componente tabla proporciona una estructura flexible para mostrar datos, que se puede combinar con diferentes capas de administración de datos para implementar diversas características. Opcionalmente, puede aprovechar bibliotecas externas como TanStack Table para manejar partes de la lógica de administración de datos.
Para características como acciones de fila, búsqueda, ordenamiento y selección, puede revisar los siguientes ejemplos de código que demuestran la implementación utilizando solo componentes de Jutro y ejemplos que incorporan TanStack Table para parte de la lógica de administración de datos. Los ejemplos tienen todos los archivos necesarios para que los revise en su entorno, como ganchos y datos simulados. Cada ejemplo tiene un conjunto diferente de archivos.
Consulte la sección Uso para obtener detalles sobre cómo diseñar una tabla correctamente y las diferentes opciones de configuración que ofrecemos.
Ejemplo básico
Para crear una implementación básica de un componente Table, debe utilizar los siguientes componentes:
Tablepara establecer una estructuraTableHeaderpara definir la sección del encabezadoTableBodypara definir la sección del cuerpoHeaderRowpara definir una fila en el encabezado.BodyRowpara definir una fila en el cuerpoHeaderCelldefinir una celda en el encabezadoBodyCellpara definir una célula en el cuerpo
Al utilizar estos componentes, debe seguir las reglas definidas en las secciones Código y Uso. Esto incluye el anidamiento adecuado de los componentes y la adición de rótulos de accesibilidad adecuados, como los aria-label que se utilizan en este ejemplo.
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>
);
}
Tabla con un título
El componente Table no incluye títulos ni subtítulos integrados. Se recomienda agregar un título descriptivo en la implementación y un subtítulo si fuera necesario. Puede agregarlos fácilmente utilizando el componente Typography de Jutro.
Agregue rótulos de accesibilidad adecuados, como aria-labelledby y aria-describedby, al componente Table para hacer referencia al título y al subtítulo, respectivamente.
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>
);
}
Acciones de fila
Para implementar acciones de fila en el componente Table, combine los componentes de la estructura con una capa de administración de datos que maneje las operaciones de fila individuales y una columna de acciones que contenga elementos interactivos para cada fila.
La implementación de acciones de fila requiere administrar devoluciones de llamada de acción y proporcionarlas a componentes de acción colocados en celdas de la tabla. Cree botones de acción o menús en una columna exclusiva que realicen operaciones en filas individuales, como editar, eliminar o ver detalles. Esta columna exclusiva suele ser la última columna de la tabla. Los componentes de acción reciben datos de fila y devoluciones de llamada de acción como parámetros, y pueden activar operaciones que modifican el conjunto de datos o cambian el estado de la aplicación.
Utilice una capa de administración de datos (como el gancho personalizado useTableRowActions) para gestionar las operaciones de fila, como agregar, actualizar o eliminar filas individuales del conjunto de datos. La lógica de acción debe proporcionar métodos para realizar estas operaciones y actualizar el estado interno según corresponda. Para escenarios más complejos, como la edición integrada, combine acciones de fila con una capa de gestión del estado de edición (como el gancho personalizado useEditableRow) que haga un seguimiento de las filas que están en modo de edición y gestione los datos de edición temporal.
Conecte las devoluciones de llamada de acción pasando funciones como deleteRow y saveRow a los componentes de la columna de acciones. Estas devoluciones de llamada se ejecutan cuando los usuarios interactúan con los botones de acción y modifican el conjunto de datos o activan cambios de estado. Para casos de edición en línea, implemente una renderización condicional en las celdas que cambie entre los componentes de visualización y los componentes de entrada en función del estado de edición de la fila, utilizando funciones como isRowEditing y renderCell para administrar la lógica de visualización.
Otra alternativa es puede usar TanStack Table como una capa de administración de datos, lo que proporciona flexibilidad para implementar acciones de fila personalizadas a través de definiciones de columnas con renderizadores de celdas personalizados. Con TanStack Table, las acciones de fila se implementan a través de configuraciones de columnas que definen los componentes de acción y sus comportamientos.
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Búsqueda
Para implementar la búsqueda en el componente Table, combine los componentes de estructura con una capa de administración de datos y un componente de entrada de búsqueda que filtre los datos de la tabla en tiempo real.
La implementación de búsqueda requiere administrar el estado de la consulta de búsqueda y proporcionar devoluciones de llamada de búsqueda a un componente de entrada de búsqueda. Cree un componente de entrada de búsqueda como SearchBar que permita a los usuarios ingresar consultas de búsqueda y conéctelo a la tabla a través de una función de controlador de búsqueda. La funcionalidad de búsqueda filtra su conjunto de datos en función de la consulta de búsqueda comparándolo con valores de columna que permiten la búsqueda mediante funciones de descriptor de acceso.
Utilice una capa de administración de datos (como el gancho personalizado useTableDataSearch) para gestionar el estado de la consulta de búsqueda, hacer que las entradas del usuario reboten por el rendimiento y filtrar el conjunto de datos en función de la consulta de búsqueda actual. El rebote es esencial para evitar operaciones de filtrado excesivas a medida que los usuarios escriben, lo que puede causar problemas de rendimiento con grandes conjuntos de datos. Puede implementar el rebote, utilizando técnicas como setTimeout o bibliotecas como la función debounce de Lodash para retrasar la ejecución de la búsqueda hasta que el usuario haya dejado de escribir.
La lógica de búsqueda debe comparar la consulta con valores de columna específicos mediante el uso de propiedades accessorFn definidas en la configuración de columna. Cada columna que debería permitir la búsqueda necesita una función de descriptor de acceso que devuelva el texto que se puede buscar para esa columna. Conecte las devoluciones de llamada de búsqueda pasando funciones como onSearchQueryChange a la propiedad onChange de su componente de entrada de búsqueda.
Como alternativa, puede usar TanStack Table como una capa de administración de datos, que proporciona capacidades de filtrado global incorporado. Con TanStack Table, la funcionalidad de búsqueda se maneja automáticamente a través de su gancho useReactTable con función getFilteredRowModel y configuración de filtro global.
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Selección
Para implementar la selección en el componente Table, combine los componentes de estructura con una capa de administración de datos que gestione el estado de selección de las filas individuales y las operaciones por lote.
La implementación de la selección requiere administrar el estado de selección de las filas individuales y proporcionar devoluciones de llamada de selección a las celdas de la tabla. Utilice la propiedad selected en el componente BodyRow para indicar si una fila está seleccionada. Los componentes secundarios pueden utilizar este valor booleano para determinar el estado de fila.
Maneje las interacciones de selección a través de componentes Checkbox colocados en los componentes BodyCell y HeaderCell. Puede gestionar los cambios de selección actualizando el estado de selección y manteniendo una lista de identificadores de fila seleccionados. Coloque casillas de verificación de fila individual en la primera columna de cada fila y agregue una casilla de verificación de encabezado en la HeaderCell correspondiente para operaciones de selección por lote, como “seleccionar todo” o “borrar todo”.
Utilice una capa de administración de datos (como el gancho personalizado useTableDataSelection) para realizar un seguimiento de qué filas están seleccionadas, manejar la selección de filas individuales y administrar operaciones de selección por lote como “seleccionar todo” o “borrar todo”. La lógica de selección debe manejar la adición y eliminación de elementos del conjunto de selección y proporcionar métodos para verificar el estado de selección. Conecte las devoluciones de llamada de selección a los componentes Checkbox pasando funciones como toggleRowSelection y toggleRowsSelection a las propiedades onChange de las casillas de verificación respectivas. Estas devoluciones de llamada actualizan el estado de selección interno cuando los usuarios interactúan con las casillas de verificación.
Como alternativa, puede usar TanStack Table como una capa de administración de datos, que proporciona gestión integrada del estado de selección de filas. Con TanStack Table, la selección se maneja automáticamente a través de su gancho useReactTable con configuración de selección de filas.
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Ordenamiento
Para implementar el ordenamiento en el componente Table, combine los componentes de la estructura con una capa de administración de datos, como un gancho personalizado que gestione el estado del ordenamiento.
La implementación del ordenamiento requiere administrar el estado de orden de la columna ordenada actual y la dirección, y proporcionar devoluciones de llamada de orden a las celdas de encabezado. Utilice propiedades HeaderCell como isSortable, isSorted y onSort para mostrar los indicadores de ordenamiento correctos y manejar las interacciones de los usuarios. Para gestionar los cambios de orden, actualice el estado de los datos y aplique la lógica de ordenamiento adecuada a su conjunto de datos.
Utilice una capa de administración de datos (como el gancho personalizado useTableDataSorting) para ordenar el conjunto de datos en función de la columna y la dirección actuales y, a continuación, pase los datos ordenados a las filas de la tabla. La lógica de ordenamiento debe manejar diferentes tipos de datos (strings, números, fechas) y proporcionar un comportamiento de orden ascendente y descendente coherente.
Como alternativa, puede usar TanStack Table como una capa de administración de datos, que proporciona administración incorporada del estado del ordenamiento y maneja el ordenamiento de columnas automáticamente a través de su gancho useReactTable con configuración de ordenamiento.
Accesibilidad
El componente HeaderCell maneja automáticamente todos los atributos de accesibilidad necesarios para la funcionalidad de ordenamiento. Cuando proporciona las propiedades isSortable y isSorted, el componente genera automáticamente los atributos aria-sort (none, ascendingo descending) y valores descriptivos apropiados aria-label que comunican el estado de ordenamiento a los lectores de pantalla. No es necesario establecer manualmente estos atributos de accesibilidad.
Sin embargo, puede invalidar manualmente estos rótulos de accesibilidad pasándolos como propiedades HTML. Tenga en cuenta que los atributos aria-label pasados manualmente se tratan como propiedades HTML nativas y no se traducen automáticamente. Si necesita internacionalización para rótulos de accesibilidad personalizados, debe utilizar una función de traducción usted mismo:
<HeaderCell aria-label={translator(consumerCustomMessage)} .../>
El componente solo admite valores aria-sort de none, ascending y descending. Si tiene un caso de uso para otros valores aria-sort (como other para el ordenamiento personalizado que no sea ascendente ni descendente), debe pasarlos manualmente, ya que el componente no maneja estos valores automáticamente.
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Implementación de componentes
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>
);
}
Administración de los datos
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 },
},
];
Uso
Descripción general
La tabla es un componente que se utiliza para mostrar y manipular conjuntos de datos grandes y complejos en un formato escaneable y fácil de leer. Las tablas organizan la información en filas y columnas, lo que permite a los usuarios comparar datos, identificar patrones y actuar.
Cuándo se debe utilizar
- Para presentar datos complejos que requieren una organización y una estructura claras.
- Cuando el orden en que se presentan los datos es importante y los usuarios necesitan controlar el ordenamiento.
- Cuando los usuarios deben comparar datos del mismo tipo entre diferentes elementos.
Cuándo no se debe utilizar
- Cuando el orden de presentación de los datos no es tan significativo como los datos en sí. En su lugar, use vistas de tarjeta o vistas de lista.
- Cuando el caso de uso no requiere una comparación directa de datos. En su lugar, use vistas de tarjeta o vistas de lista.
- Cuando se trabaja con pequeñas cantidades de datos, lo que generaría un espacio en blanco excesivo. En su lugar, analice la posibilidad de utilizar una vista de tarjeta o una vista de lista.
Formato
Anatomía

El componente tabla consta de los siguientes elementos:
- Título y subtítulo. Encabezado que describe el contenido de la tabla.
- Controles de tabla. Conjunto de controles para manipular cómo se muestran los datos, como la búsqueda y los filtros.
- Acciones de la tabla. Conjunto de botones que realizan una operación en el conjunto de datos de la tabla, como agregar un nuevo registro o exportar datos.
- Encabezados de columna. Rótulos para cada columna que identifican el tipo de datos. Los encabezados también pueden incluir ordenamiento y otras acciones.
- Filas de datos. Conjunto completo de celdas que representa un solo elemento o registro. Las filas pueden incluir controles de selección (casillas de verificación) e íconos de expansión.
- Acciones de fila. Conjunto de acciones en la última columna que un usuario puede realizar en una fila individual.
- Paginación. Controles que permiten a los usuarios navegar por las páginas de datos y ajustar el número de filas que se muestran por página.
Alineación y ubicación
De forma predeterminada, las columnas se reducen para ajustarse a su contenido, excepto la última columna, que se expande para ocupar el espacio disponible restante. Esto preserva la capacidad de escaneo de la información.
Si una tabla incluye acciones de fila, estas se deben colocar en esta última columna.
Contenido
Pautas generales de redacción
- En todos los aspectos del diseño de las interfaces de productos de Guidewire, utilice mayúsculas como se usan en las oraciones. No use mayúsculas en todas las palabras.
- Use verbos en tiempo presente y voz activa en la mayoría de las situaciones.
- Use contracciones comunes para darle al texto un tono más natural e informal (pauta correspondiente al inglés).
- Use un lenguaje sencillo. Evite la jerga innecesaria y el lenguaje complejo.
- Las palabras y las oraciones deben ser breves.
Concisión del contenido de la celda
Intente que el contenido dentro de una celda de la tabla no supere los 50 caracteres para garantizar la legibilidad. Para contenido más largo, el componente maneja el desbordamiento con las siguientes reglas específicas:
- El texto de entre 81 y 159 caracteres se ajusta en varias líneas.
- El texto de 160 caracteres o más se trunca con puntos suspensivos (...) y el texto completo se revelará en una descripción emergente al pasar el cursor por encima.
- Los datos numéricos siempre deben ser completamente visibles y no se ajustan ni truncan.
- Otros formatos de datos se ajustan si fuera necesario.
Redacción de encabezados de columna claros
Los encabezados deben ser cortos, descriptivos y estar escritos con mayúsculas como se usan en las oraciones.
- Los encabezados deben limitarse a una o dos palabras. Esto mejora la escaneabilidad y garantiza la legibilidad de los datos de la tabla.
- Comience los encabezados con un sustantivo cuando sea posible. Los encabezados son rótulos para los datos de la columna, por eso, usar un sustantivo (por ejemplo, nombre, estado, fecha de creación) es la forma más clara de describir esos datos.
Formato coherente de los números
Si escribe en inglés americano, use una coma como separador de mil en cualquier número con cuatro o más dígitos. No utilice comas en años (a menos que sean cinco o más dígitos), números de página, direcciones o decimales.
Para otras configuraciones regionales, siga las pautas de localización establecidas para el formato de números.
Claridad en estados vacíos
Escriba mensajes directos y útiles que expliquen por qué la tabla está vacía y, si es posible, qué puede hacer el usuario a continuación.
Comportamientos
Estados
Una tabla puede tener varios estados para comunicar el estado de los datos.
- Cargando: Cuando se recuperan datos, el cuerpo de la tabla muestra un indicador de carga con el texto “Cargando datos...”.
- Vacía: Cuando una tabla no contiene datos, muestra el estado vacío. El mensaje debe ser útil y se recomienda utilizar un botón de acción para guiar al usuario. Por ejemplo, cuando diseñe un estado “Sin resultados de búsqueda”, muestre un botón Borrar filtros si los filtros están activos. El tipo de botón (principal, secundario y terciario) se puede configurar para adaptarse al caso de uso específico.
- Error: Si los datos no se cargan debido a un error del sistema (como cuando se pierde la conexión a Internet), el estado vacío debe comunicar el problema y proporcionar una acción de recuperación, como un botón Actualizar.
- Estados de fila: Las filas pueden estar en estado predeterminado, seleccionado (resaltado) o expandido.
Interacciones
- Ordenamiento: Los usuarios pueden hacer clic en el encabezado de una columna para ordenar los datos de la tabla según esa columna. Un ícono direccional en el encabezado indica la dirección del ordenamiento actual.
- Selección: Los usuarios pueden seleccionar una o más filas con las casillas de verificación de la primera columna. Una casilla de verificación en el encabezado permite a los usuarios seleccionar o anular la selección de todas las filas visibles. Cuando se seleccionan filas, puede aparecer una barra de selección contextual en la parte superior de la tabla.
- Expansión de filas: Si una fila se puede expandir, los usuarios pueden hacer clic en el ícono de corchete angular para mostrar información adicional relacionada directamente debajo de la fila.
- Acciones de fila: Los usuarios realizan acciones en una sola fila utilizando los controles de la última columna. Muestra hasta dos acciones de ícono principales. Coloque todas las acciones adicionales en un menú de puntos suspensivos (...).
- Paginación: Los usuarios pueden navegar entre las páginas de datos utilizando los controles de paginación en la parte inferior de la tabla.
Capacidad de ser responsive y adaptabilidad
El componente tabla es totalmente responsive. Sin embargo, para los casos de uso que dan prioridad a los dispositivos móviles, una vista de tarjeta a menudo proporciona una mejor experiencia de usuario que una tabla tradicional de varias columnas.
Accesibilidad
Este componente de Jutro ha sido validado para cumplir con las pautas de accesibilidad de WCAG 2.2 AA en su configuración base predeterminada. Esto incluye garantizar que se cumpla lo siguiente:
- La relación de contraste de los elementos textuales con respecto a su fondo es superior a 4,5:1.
- El contenido no textual que debe transmitir significado (como íconos e indicadores de foco) tiene una relación de contraste de al menos 3:1 con sus colores adyacentes.
- El elemento se puede operar con teclado, así como con mouse.
- Se puede acceder al contenido mediante lectores de pantalla, como JAWS y VoiceOver.
El cumplimiento de los criterios de accesibilidad depende, en última instancia, de cómo se implementa y personaliza este componente. Los cambios realizados por el autor del contenido pueden afectar la accesibilidad. Para obtener más información sobre nuestro modelo de responsabilidad compartida, revise nuestra declaración completa sobre accesibilidad de Jutro.
Al utilizar este componente en su aplicación, asegúrese de lo siguiente:
- Se debe proporcionar un título descriptivo. Un título, idealmente con el elemento
<caption>, ayuda a los usuarios con lectores de pantalla a comprender el propósito de la tabla antes de comenzar a navegar por los datos. - Los encabezados de columna deben estar definidos correctamente. Los encabezados deben estar estructurados correctamente (usando
<th>con un atributoscope="col") para que los lectores de pantalla puedan anunciar el encabezado correcto para cada celda de datos a medida que un usuario navega. - Todos los elementos interactivos deben ser accesibles. Cualquier control dentro de la tabla, como botones o enlaces, debe tener un nombre claro y accesible. Para los botones de solo ícono, esto requiere proporcionar un
aria-labeldescriptivo (por ejemplo,aria-label="Delete item"). - Se debe anunciar el estado actual del ordenamiento. Cuando se ordena una columna, su encabezado debe comunicar la dirección (ascendente o descendente) a los lectores de pantalla, normalmente mediante el atributo
aria-sort. - Se debe indicar cuándo se está cargando contenido. Establezca el atributo
aria-busy="true"en la tabla cuando su contenido se esté cargando o actualizando activamente.
Navegación por teclado
La tabla proporciona una navegación completa por teclado siguiendo el patrón de cuadrícula.
Teclas de navegación
-
Teclas de flecha: Navega entre celdas adyacentes.
←(Flecha izquierda): Pasa a la celda anterior en la fila actual.→(Flecha derecha): Pasa a la siguiente celda de la fila actual.↑(Flecha hacia arriba): Pasa a la celda de arriba en la misma columna.↓(Flecha hacia abajo): Pasa a la celda de abajo en la misma columna.
-
Navegación mediante tabulador: Navegación secuencial por la tabla.
Tab: Pasa al siguiente elemento que pueda recibir el foco.Shift + Tab: Pasa al elemento anterior que pueda recibir el foco.
-
Navegación por la página: Navegación rápida para tablas grandes.
Page Up: Pasa a la primera fila de la columna actual.Page Down: Pasa a la última fila de la columna actual.
-
Navegación a los límites: Saltar a los límites de la tabla.
Home: Pasar a la primera celda de la fila actual.Ctrl + Home: Pasar a la primera celda de la tabla (arriba a la izquierda).End: Pasar a la última celda de la fila actual.Ctrl + End: Pasar a la última celda de la tabla (abajo a la derecha).
Compatibilidad con contenido interactivo
El sistema de navegación por teclado maneja de forma inteligente las celdas que contienen elementos interactivos:
- Botones y enlaces: Los elementos interactivos, como botones de ordenamiento o acciones de fila, dentro de las celdas siguen siendo accesibles a través de la navegación estándar mediante tabulador y las teclas de flecha. Cuando se utiliza la navegación con teclas de flecha, si una celda contiene un elemento interactivo, como un campo de entrada, botón o enlace, con un
tabindexsuperior a-1, el foco pasa a ese elemento en lugar de a la celda. Si hay varios elementos interactivos en la celda, el foco pasa al primero. - Contenido personalizado que puede recibir el foco: Cualquier elemento con un atributo
tabindexse integra correctamente en el flujo de navegación. - Entradas de formulario: Cuando una celda contiene campos de entrada, elementos
textareau otros elementos editablesform, la navegación respeta su modo de edición, lo que significa que cuando el foco está en uno de esos elementos, las teclas de flecha funcionan según lo previsto para la navegación dentro del elemento. Para alejarse del elemento, utiliceTaboShift + Tab.
Gestión del foco
En la tabla se utilizan técnicas avanzadas de gestión del foco:
- Patrón de tabindex itinerante: Solo una celda de la tabla puede recibir el foco a la vez, lo que reduce las paradas de tabulación y mejora la eficiencia de la navegación.
- Conocimiento de los límites: Las teclas de navegación respetan los límites de la tabla y no moverán el foco fuera de la cuadrícula de la tabla.
- Indicadores visuales de foco: Información visual clara que muestra qué celda tiene el foco actualmente.
Para que la gestión del foco funcione correctamente, asegúrese de que las propiedades rowIndex y columnIndex estén configuradas correctamente para el componente BodyCell y la propiedad columnIndex esté configurada correctamente para el componente HeaderCell. Consulte los contratos de componentes para obtener más detalles.
Código
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>
);
}
Instrucción de importación
import {
Table,
TableHeader,
TableBody,
HeaderRow,
HeaderCell,
BodyRow,
BodyCell,
} from '@jutro/components';
Contrato de componentes
Asegúrese de comprender la superficie de la API de componentes del sistema de diseño, así como sus implicaciones, ventajas y desventajas. Obtenga más información en nuestra introducción a la API de componentes.
Tabla
El componente Table define la estructura de la tabla.
Acepta TableHeader y TableBody como sus elementos secundarios. Para obtener más información, consulte la sección de anidamiento.
Propiedades
Además de las propiedades personalizadas del componente Table, puede pasarle atributos HTML estándares.
noStripedRows- Tipo
booleanDescripciónDetermines whether the table displays alternating striped rows for better visual distinction.
Valor predeterminadofalse sortingIcons- Tipo
{ ascendingIcon: element, descendingIcon: element, unsortedIcon: element }DescripciónDefines custom icons for column sorting buttons, including ascending, descending, and unsorted states.
TableHeader
El componente TableHeader define la sección del encabezado de la tabla.
Se debe utilizar como un elemento secundario del componente Table y solo acepta HeaderRow como su elemento secundario. Para obtener más información, consulte la sección de anidamiento.
Propiedades
El componente TableHeader no tiene propiedades personalizadas, pero puede pasarle atributos HTML estándares.
TableBody
El componente TableBody define la sección del cuerpo de la tabla.
Se debe utilizar como un elemento secundario del componente Table y solo acepta BodyRow como su elemento secundario. Para obtener más información, consulte la sección de anidamiento.
Propiedades
El componente TableBody no tiene propiedades personalizadas, pero puede pasarle atributos HTML estándares.
HeaderRow
El componente HeaderRow define una fila en la sección del encabezado de la tabla.
Se debe utilizar como un elemento secundario del componente TableHeader y solo acepta HeaderCell como su elemento secundario. Para obtener más información, consulte la sección de anidamiento.
Propiedades
El componente HeaderRow no tiene propiedades personalizadas, pero puede pasarle atributos HTML estándares.
BodyRow
El componente BodyRow define una fila en la sección del cuerpo de la tabla.
Se debe utilizar como un elemento secundario del componente TableBody y solo acepta BodyCell como su elemento secundario. Para obtener más información, consulte la sección de anidamiento.
Propiedades
Además de las propiedades personalizadas del componente BodyRow, puede pasarle atributos HTML estándares.
selected- Tipo
booleanDescripciónIndicates whether the row is selected. Used in the row selection feature implementation.
HeaderCell
El componente HeaderCell define una celda en la sección del encabezado de la tabla. Rellénelo con los encabezados de columna.
Se debe utilizar como un elemento secundario del componente HeaderRow. Para obtener más información, consulte la sección de anidamiento.
Propiedades
Además de las propiedades personalizadas del componente HeaderCell, puede pasarle atributos HTML estándares.
columnIndexobligatorio- Tipo
numberDescripciónSets the column index. Must be explicitly set to ensure accurate accessibility semantics when rendering multiple cells. The column index must start from 0.
align- Tipo
'left' | 'center' | 'right'DescripciónSets the alignment of the header cell content.
Valor predeterminado'left' isSortable- Tipo
booleanDescripciónIndicates whether the column is sortable or not. Used in the sorting feature implementation.
Valor predeterminadofalse isSorted- Tipo
false | 'asc' | 'desc'DescripciónIndicates the sorting state of the column. Used in the sorting feature implementation.
onSort- Tipo
(event: unknown) => voidDescripciónCallback 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
El componente BodyCell define una celda en la sección del cuerpo de la tabla. Rellénelo con los datos de cada celda individual en cada fila.
Se debe utilizar como un elemento secundario del componente BodyRow. Para obtener más información, consulte la sección de anidamiento.
Propiedades
Además de las propiedades personalizadas del componente BodyCell, puede pasarle atributos HTML estándares.
columnIndexobligatorio- Tipo
numberDescripciónSets the column index. Must be explicitly set to ensure accurate accessibility semantics when rendering multiple cells. The column index must start from 0.
rowIndexobligatorio- Tipo
numberDescripciónSets the row index. Must be explicitly set to ensure accurate accessibility semantics when rendering multiple cells. The row index must start from 0.
align- Tipo
'left' | 'center' | 'right'DescripciónSets the horizontal alignment of the body cell content.
Valor predeterminado'left'
Comportamientos personalizados
Anidamiento
El componente Table define la estructura de la tabla a través de las propiedades children de type ReactNode. Debe utilizar los componentes de la estructura de la tabla de la siguiente manera:
- El componente
TableaceptaTableHeaderyTableBodycomo elementos secundarios. - El componente
TableHeaderaceptaHeaderRowcomo elemento secundario y solo debe utilizarse en el contexto deTable. - El componente
TableBodyaceptaBodyRowcomo elemento secundario y solo debe utilizarse en el contexto deTable. - El componente
HeaderRowaceptaHeaderCellcomo elemento secundario y solo debe utilizarse en el contexto deTableHeader. - El componente
BodyRowaceptaBodyCellcomo elemento secundario y solo debe utilizarse en el contexto deTableBody. - El componente
HeaderCellsolo debe utilizarse en el contexto deHeaderRow. - El componente
BodyCellsolo debe utilizarse en el contexto deBodyRow.
Aunque es posible que estos componentes puedan mostrar o manejar otros tipos o elementos HTML, esta no es una función compatible.
Filas rayadas
De forma predeterminada, la tabla muestra colores de fondo de fila alternos (filas rayadas) para mejorar la legibilidad y ayudar a los usuarios a realizar un seguimiento de los datos en las columnas. Para desactivar este comportamiento, establezca la propiedad noStripedRows en true en el componente Table. Analice la posibilidad de hacerlo cuando trabaje con tablas que tengan muy pocas filas o cuando implemente un estilo de fila personalizado que entre en conflicto con el color alterno predeterminado.
Ejemplo con la propiedad noStripedRows establecida en true:
Ganchos
No hay ganchos para Table.
Claves de traducción
No hay traducciones para Table.
Para obtener información sobre cómo gestionar las traducciones, consulte nuestra sección sobre internacionalización.
Escotillas de escape
Para obtener más información, consulte nuestra documentación sobre escotillas de escape.
Paso de atributos HTML a componentes
Puede pasar atributos HTML estándares, como id, className y style, desde los elementos HTML correspondientes a todos los componentes de la tabla. Estos atributos se aplican al elemento raíz del componente respectivo.
Elementos raíz de componentes de tabla
| Componente de tabla | Elemento HTML |
|---|---|
Table | <table> |
TableHeader | <thead> |
TableBody | <tbody> |
HeaderRow | <tr> |
BodyRow | <tr> |
HeaderCell | <th> |
BodyCell | <td> |
Ejemplo
En este ejemplo, el atributo aria-labelledby se pasa al componente Table. Hace referencia id al del componente Typography que sirve como título de la tabla y lee el contenido de texto de ese componente como el nombre accesible de la tabla.
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>
);
}