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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user