refactor(DataTable): enhance accessibility features and improve localization for selection and loading states

This commit is contained in:
Marco Beretta
2025-09-26 19:08:25 +02:00
parent b4aa36b6f1
commit 7b588a607b
2 changed files with 77 additions and 20 deletions

View File

@@ -135,7 +135,12 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
const selectColumn: ColumnDef<ProcessedDataRow<TData>, TValue> = {
id: 'select',
header: ({ table }) => (
<div className="flex h-full items-center justify-center">
<div
className="flex h-full items-center justify-center"
role="button"
tabIndex={0}
aria-label={localize('com_ui_select_all')}
>
<SelectionCheckbox
checked={table.getIsAllRowsSelected()}
onChange={(value) => table.toggleAllRowsSelected(value)}
@@ -143,15 +148,25 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
/>
</div>
),
cell: ({ row }) => (
<div className="flex h-full items-center justify-center">
<SelectionCheckbox
checked={row.getIsSelected()}
onChange={(value) => row.toggleSelected(value)}
ariaLabel={`Select row ${row.index + 1}`}
/>
</div>
),
cell: ({ row }) => {
const rowDescription = row.original.name
? `named ${row.original.name}`
: `at position ${row.index + 1}`;
return (
<div
className="flex h-full items-center justify-center"
role="button"
tabIndex={0}
aria-label={localize(`com_ui_select_row`, { rowDescription })}
>
<SelectionCheckbox
checked={row.getIsSelected()}
onChange={(value) => row.toggleSelected(value)}
ariaLabel={localize(`com_ui_select_row`, { rowDescription })}
/>
</div>
);
},
meta: {
className: 'w-12',
},
@@ -292,6 +307,8 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
'h-[calc(100vh-8rem)] max-h-[80vh]',
className,
)}
role="region"
aria-label={localize('com_ui_data_table')}
>
<div className="flex w-full shrink-0 items-center gap-3 border-b border-border-light">
{shouldShowSearch && <DataTableSearch value={searchTerm} onChange={setSearchTerm} />}
@@ -312,8 +329,11 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
overscrollBehavior: 'contain',
} as React.CSSProperties
}
role="region"
aria-label={localize('com_ui_data_table_scroll_area')}
aria-describedby={showSkeletons ? 'loading-status' : undefined}
>
<Table>
<Table role="table" aria-label={localize('com_ui_data_table')}>
<TableHeader className="sticky top-0 z-10 bg-surface-secondary">
{headerGroups.map((headerGroup) => (
<TableRow key={headerGroup.id}>
@@ -328,24 +348,47 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
const isSelectHeader = header.id === 'select';
const meta = header.column.columnDef.meta as { className?: string } | undefined;
const canSort = header.column.getCanSort();
const sortAriaLabel = canSort
? `${header.column.columnDef.header} column, ${header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : 'sortable'}`
: undefined;
const handleSortingKeyDown = (e: React.KeyboardEvent) => {
if (canSort && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
header.column.toggleSorting();
}
};
return (
<TableHead
key={header.id}
className={cn(
'border-b border-border-light py-2',
isSelectHeader ? 'px-0 text-center' : 'px-3',
header.column.getCanSort() && 'cursor-pointer hover:bg-surface-tertiary',
canSort && 'cursor-pointer hover:bg-surface-tertiary',
meta?.className,
)}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={handleSortingKeyDown}
role={canSort ? 'button' : undefined}
tabIndex={canSort ? 0 : undefined}
aria-label={sortAriaLabel}
aria-sort={
header.column.getIsSorted() as
| 'ascending'
| 'descending'
| 'none'
| undefined
}
>
{isSelectHeader ? (
flexRender(header.column.columnDef.header, header.getContext())
) : (
<div className="flex items-center gap-2">
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() && (
<span className="text-text-primary">
{canSort && (
<span className="text-text-primary" aria-hidden="true">
{{
asc: <ArrowUp className="size-4 text-text-primary" />,
desc: <ArrowDown className="size-4 text-text-primary" />,
@@ -402,9 +445,16 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
)}
{isFetchingNextPage && (
<TableRow>
<TableCell colSpan={tableColumns.length} className="p-4 text-center">
<TableCell
colSpan={tableColumns.length}
className="p-4 text-center"
id="loading-status"
role="status"
aria-live="polite"
>
<div className="flex items-center justify-center gap-2">
<Spinner className="h-5 w-5" />
<Spinner className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">{localize('com_ui_loading_more_data')}</span>
</div>
</TableCell>
</TableRow>
@@ -413,9 +463,13 @@ function DataTable<TData extends Record<string, unknown>, TValue>({
</Table>
{!isLoading && !showSkeletons && rows.length === 0 && (
<div className="flex flex-col items-center justify-center py-12">
<div
className="flex flex-col items-center justify-center py-12"
role="status"
aria-live="polite"
>
<Label className="text-center text-text-secondary">
{searchTerm ? 'No search results' : localize('com_ui_no_data')}
{searchTerm ? localize('com_ui_no_search_results') : localize('com_ui_no_data')}
</Label>
</div>
)}

View File

@@ -4,7 +4,6 @@
"com_ui_no_results_found": "No results found",
"com_ui_no_data_available": "No data available",
"com_ui_select_all": "Select All",
"com_ui_select_row": "Select Row",
"com_ui_no_selection": "No selection",
"com_ui_confirm_bulk_delete": "Are you sure you want to delete the selected items? This action cannot be undone.",
"com_ui_delete_success": "Items deleted successfully",
@@ -15,5 +14,9 @@
"com_ui_delete_selected": "Delete Selected",
"com_ui_search_table": "Search table",
"com_ui_search_table_description": "Type to filter results",
"com_ui_search": "Search"
"com_ui_search": "Search",
"com_ui_data_table_scroll_area": "Scrollable data table area",
"com_ui_select_row": "Select Row {{0}}",
"com_ui_loading_more_data": "Loading more data...",
"com_ui_no_search_results": "No search results found"
}