diff --git a/frontend/src/components/primitives/ConfirmDialog.tsx b/frontend/src/components/primitives/ConfirmDialog.tsx index 89ab7ac..26d0eed 100644 --- a/frontend/src/components/primitives/ConfirmDialog.tsx +++ b/frontend/src/components/primitives/ConfirmDialog.tsx @@ -1,6 +1,17 @@ -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Button } from './Spinner'; +const titleId = 'confirm-dialog-title'; +const descId = 'confirm-dialog-desc'; + +function focusablesIn(container: HTMLElement): HTMLElement[] { + return Array.from( + container.querySelectorAll( + 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ); +} + export function ConfirmDialog({ title, message, @@ -15,6 +26,42 @@ export function ConfirmDialog({ onCancel: () => void; }) { const [busy, setBusy] = useState(false); + const panelRef = useRef(null); + + useEffect(() => { + const previouslyFocused = document.activeElement as HTMLElement | null; + const panel = panelRef.current; + const cancelButton = panel?.querySelector('button[data-dialog-cancel]'); + cancelButton?.focus(); + + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + return; + } + if (e.key !== 'Tab' || !panel) return; + const items = focusablesIn(panel); + if (items.length === 0) return; + const first = items[0] as HTMLElement | undefined; + const last = items[items.length - 1] as HTMLElement | undefined; + if (!first || !last) return; + const active = document.activeElement; + if (e.shiftKey && (active === first || active === panel)) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && active === last) { + e.preventDefault(); + first.focus(); + } + } + + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + previouslyFocused?.focus?.(); + }; + }, [onCancel]); async function handleConfirm() { setBusy(true); @@ -26,13 +73,22 @@ export function ConfirmDialog({ className="fixed inset-0 z-50 flex items-center justify-center bg-ink-950/70 p-4 backdrop-blur-sm" role="dialog" aria-modal="true" + aria-labelledby={titleId} + aria-describedby={descId} + onMouseDown={(e) => { + if (e.target === e.currentTarget) onCancel(); + }} > -
+

Confirm

-

{title}

-

{message}

+

+ {title} +

+

+ {message} +

-