diff --git a/.jules/palette.md b/.jules/palette.md index e69de29..3e61dec 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -0,0 +1,3 @@ +## 2024-08-02 - Custom Modals need ARIA and Keyboard Navigation +**Learning:** Custom modals using standard div elements lack native dialog behaviors, making them inaccessible to screen readers and difficult to use via keyboard. +**Action:** Always add `role="dialog"`, `aria-modal="true"`, and a `keydown` listener for the `Escape` key to custom modals to ensure a standard, accessible experience. diff --git a/src/features/library/components/MatchPreviewModal.tsx b/src/features/library/components/MatchPreviewModal.tsx index 78f0d0d..da8b820 100644 --- a/src/features/library/components/MatchPreviewModal.tsx +++ b/src/features/library/components/MatchPreviewModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Check, Loader2, X } from 'lucide-react' import { cx } from '../lib/cx' import type { MatchPreview, MatchResult, MetadataCandidate } from '../../../lib/types' @@ -83,6 +83,17 @@ export function MatchPreviewModal({ preview, onConfirm, onClose, onConfirmed }: const selected = candidates[selectedIdx] ?? null const noCandidates = candidates.length === 0 + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape') return + event.preventDefault() + onClose() + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + async function handleApprove() { if (!selected) return setIsApplying(true) @@ -117,7 +128,11 @@ export function MatchPreviewModal({ preview, onConfirm, onClose, onConfirmed }:
{/* Modal */} -
+
{/* Header */}
@@ -175,7 +190,7 @@ export function MatchPreviewModal({ preview, onConfirm, onClose, onConfirmed }: {SOURCE_LABELS[c.source] ?? c.source} - {c.title ?? 'Untitled'} + {c.title ?? 'Untitled'} {confidenceBadge(c.confidence)} {isActive && }