feat: complete TASK-WP4-06 accessibility

This commit is contained in:
suyx
2026-08-03 15:23:17 +08:00
parent d09fda13c5
commit a1b82ecdc4
9 changed files with 428 additions and 30 deletions
+56
View File
@@ -0,0 +1,56 @@
import { useLayoutEffect, useRef, type RefObject } from "react";
const focusableSelector = [
"a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])",
'[tabindex]:not([tabindex="-1"])',
].join(",");
function focusableElements(dialog: HTMLElement) {
return [...dialog.querySelectorAll<HTMLElement>(focusableSelector)].filter((element) => !element.hidden && element.getClientRects().length > 0);
}
export function useDialogFocus(onClose: () => void): RefObject<HTMLElement | null> {
const dialogRef = useRef<HTMLElement>(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useLayoutEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return undefined;
const returnTarget = document.activeElement instanceof HTMLElement ? document.activeElement : null;
const initial = dialog.querySelector<HTMLElement>("[data-dialog-initial-focus]") ?? focusableElements(dialog)[0] ?? dialog;
initial.focus();
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
onCloseRef.current();
return;
}
if (event.key !== "Tab") return;
const focusable = focusableElements(dialog);
if (focusable.length === 0) {
event.preventDefault();
dialog.focus();
return;
}
const first = focusable[0]!;
const last = focusable.at(-1)!;
if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement))) {
event.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", handleKeyDown, true);
return () => {
document.removeEventListener("keydown", handleKeyDown, true);
if (returnTarget?.isConnected) returnTarget.focus();
};
}, []);
return dialogRef;
}