From f4667b4d2ade61fcf099c58d92c496f445370b5d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:51:46 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20Escape=20key=20?= =?UTF-8?q?and=20ARIA=20roles=20to=20modals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardizes modal keyboard accessibility and screen reader support by ensuring `CoverPickerModal`, `MatchPreviewModal`, and `RescanMetadataModal` all support Escape-to-close and have proper dialog ARIA roles. Co-authored-by: jspann21 <179991454+jspann21@users.noreply.github.com> --- .../library/components/CoverPickerModal.tsx | 14 +++++++++++++ .../library/components/MatchPreviewModal.tsx | 20 +++++++++++++++++-- .../components/RescanMetadataModal.tsx | 14 +++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/features/library/components/CoverPickerModal.tsx b/src/features/library/components/CoverPickerModal.tsx index 5430cde..20b03f4 100644 --- a/src/features/library/components/CoverPickerModal.tsx +++ b/src/features/library/components/CoverPickerModal.tsx @@ -159,6 +159,17 @@ export function CoverPickerModal({ } }, []) + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape') return + event.preventDefault() + onClose() + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + useEffect(() => { customUrlRef.current = customUrl }, [customUrl]) @@ -381,6 +392,9 @@ export function CoverPickerModal({ animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 20 }} transition={{ type: 'spring', damping: 25, stiffness: 250 }} + role="dialog" + aria-modal="true" + aria-label="Choose Cover Image" className="fixed inset-4 z-[61] mx-auto my-auto flex max-h-[min(85vh,700px)] max-w-2xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900" > {/* Header */} diff --git a/src/features/library/components/MatchPreviewModal.tsx b/src/features/library/components/MatchPreviewModal.tsx index 78f0d0d..9978691 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,12 @@ export function MatchPreviewModal({ preview, onConfirm, onClose, onConfirmed }:
{/* Modal */} -
+
{/* Header */}
diff --git a/src/features/library/components/RescanMetadataModal.tsx b/src/features/library/components/RescanMetadataModal.tsx index dd75d63..8f087d1 100644 --- a/src/features/library/components/RescanMetadataModal.tsx +++ b/src/features/library/components/RescanMetadataModal.tsx @@ -243,6 +243,17 @@ export function RescanMetadataModal({ } }, []) + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape') return + event.preventDefault() + onClose() + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + /* ---------- search ---------- */ const doSearch = useCallback(async () => { @@ -389,6 +400,9 @@ export function RescanMetadataModal({ animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 20 }} transition={{ type: 'spring', damping: 25, stiffness: 250 }} + role="dialog" + aria-modal="true" + aria-label="Rescan Metadata" className={cx( 'fixed inset-4 z-[61] mx-auto my-auto flex max-h-[min(90vh,800px)] flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900', step === 'fine-tune' ? 'max-w-[1400px]' : 'max-w-[900px]',