fix(a11y): add ARIA attributes to core UI components

AnimatedModal: ariaLabelledBy prop, EntityCombobox: combobox/listbox
pattern, FilterBar: role="search", SortableColumnHeader: aria-sort,
global-error: html lang attr, eslint label rule depth config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 23:06:25 +02:00
parent 09dcedb646
commit 797aa5e350
6 changed files with 53 additions and 26 deletions
+23 -9
View File
@@ -1,7 +1,7 @@
"use client";
import { createPortal } from "react-dom";
import { useState, useRef, useMemo, useCallback, type ReactNode } from "react";
import { useState, useRef, useMemo, useCallback, useId, type ReactNode } from "react";
import { useDebounce } from "~/hooks/useDebounce.js";
import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js";
@@ -37,6 +37,7 @@ export function EntityCombobox<T extends { id: string }>({
const debouncedSearch = useDebounce(search, 300);
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const listboxId = useId();
const closeDropdown = useCallback(() => {
setOpen(false);
setSearch("");
@@ -82,6 +83,10 @@ export function EntityCombobox<T extends { id: string }>({
<input
ref={inputRef}
type="text"
role="combobox"
aria-expanded={open}
aria-controls={open ? listboxId : undefined}
aria-autocomplete="list"
value={open ? search : selectedLabel}
onChange={(e) => setSearch(e.target.value)}
onFocus={handleFocus}
@@ -97,7 +102,11 @@ export function EntityCombobox<T extends { id: string }>({
{value && !disabled && !open && (
<button
type="button"
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); select(null); }}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
select(null);
}}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-lg leading-none"
aria-label="Clear"
tabIndex={-1}
@@ -107,8 +116,8 @@ export function EntityCombobox<T extends { id: string }>({
)}
</div>
{open && (
typeof document !== "undefined"
{open &&
(typeof document !== "undefined"
? createPortal(
<div
ref={panelRef}
@@ -120,12 +129,18 @@ export function EntityCombobox<T extends { id: string }>({
width: position.minWidth,
}}
>
<ul className="max-h-52 overflow-y-auto py-1">
<ul id={listboxId} role="listbox" className="max-h-52 overflow-y-auto py-1">
{items.length === 0 ? (
<li className="px-3 py-2 text-sm text-gray-400 dark:text-gray-500">No results</li>
<li
role="option"
aria-selected={false}
className="px-3 py-2 text-sm text-gray-400 dark:text-gray-500"
>
No results
</li>
) : (
items.map((item) => (
<li key={item.id}>
<li key={item.id} role="option" aria-selected={item.id === value}>
<button
type="button"
onMouseDown={() => select(item.id)}
@@ -144,8 +159,7 @@ export function EntityCombobox<T extends { id: string }>({
</div>,
document.body,
)
: null
)}
: null)}
</div>
);
}