Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 18 additions & 3 deletions src/features/library/components/MatchPreviewModal.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -117,7 +128,11 @@ export function MatchPreviewModal({ preview, onConfirm, onClose, onConfirmed }:
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />

{/* Modal */}
<div className="relative z-10 mx-4 flex max-h-[85vh] w-full max-w-2xl flex-col rounded-2xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-800">
<div
role="dialog"
aria-modal="true"
className="relative z-10 mx-4 flex max-h-[85vh] w-full max-w-2xl flex-col rounded-2xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-800"
>
{/* Header */}
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-4 dark:border-slate-700/60">
<div>
Expand Down Expand Up @@ -175,7 +190,7 @@ export function MatchPreviewModal({ preview, onConfirm, onClose, onConfirmed }:
<span className={cx('rounded-full px-2 py-0.5 text-[11px] font-medium', sourceColor)}>
{SOURCE_LABELS[c.source] ?? c.source}
</span>
<span className="max-w-[200px] truncate">{c.title ?? 'Untitled'}</span>
<span className="max-w-[200px] truncate" title={c.title ?? 'Untitled'}>{c.title ?? 'Untitled'}</span>
{confidenceBadge(c.confidence)}
{isActive && <Check size={14} className="text-accent-500" />}
</button>
Expand Down