Skip to content
Merged
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
Expand Up @@ -26,3 +26,6 @@
## 2026-04-09 - Standardize Modal Accessibility for Cloud Library
**Learning:** The `CloudLibrary` component functioned as a modal visually but lacked standard ARIA modal attributes (`role="dialog"`, `aria-modal="true"`, `aria-labelledby`), causing screen readers to announce it incorrectly or not at all.
**Action:** When implementing custom modals, always include `role="dialog"`, `aria-modal="true"`, an explicit `aria-labelledby` referencing a visually hidden or visible title element, and an `aria-hidden="true"` on the clickable background overlay.
## 2024-11-20 - Standardize Backdrop Overlay Accessibility
**Learning:** Components using `fixed inset-0` with a click handler to close a modal will cause screen readers to announce the entire background as a clickable element. This violates accessibility conventions.
**Action:** When implementing clickable background overlays for custom modals, always separate the clickable backdrop into its own `<div>` sibling of the dialog element, and explicitly mark it with `aria-hidden="true"`.
13 changes: 10 additions & 3 deletions src/components/AISongModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,17 @@ export function AISongModal({ isOpen, onClose, onImport, onShowToast, audioEngin
<div
ref={modalRef}
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-2 sm:p-4"
onClick={(e) => e.target === e.currentTarget && handleClose()}
>
<div
className="absolute inset-0 z-0"
onClick={handleClose}
aria-hidden="true"
/>
<div
className="bg-[#0f1115] border border-emerald-500/30 rounded-xl shadow-[0_0_60px_rgba(16,185,129,0.2)] w-full max-w-3xl max-h-[95vh] sm:max-h-[90vh] flex flex-col animate-in fade-in zoom-in-95 duration-200"
role="dialog"
aria-modal="true"
aria-labelledby="ai-song-modal-title"
className="relative z-10 bg-[#0f1115] border border-emerald-500/30 rounded-xl shadow-[0_0_60px_rgba(16,185,129,0.2)] w-full max-w-3xl max-h-[95vh] sm:max-h-[90vh] flex flex-col animate-in fade-in zoom-in-95 duration-200"
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={(e) => e.preventDefault()}
Expand All @@ -671,7 +678,7 @@ export function AISongModal({ isOpen, onClose, onImport, onShowToast, audioEngin
<span className="text-xl sm:text-2xl">🤖</span>
</div>
<div>
<h2 className="text-base sm:text-lg font-bold text-white">Import AI Song</h2>
<h2 id="ai-song-modal-title" className="text-base sm:text-lg font-bold text-white">Import AI Song</h2>
<p className="text-[10px] sm:text-xs text-gray-400 hidden sm:block">Import songs from Claude, Gemini, Jules, Copilot, etc.</p>
</div>
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/components/RbsImportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,13 @@ export function RbsImportModal({ isOpen, onClose, onImport, onShowToast }: RbsIm
return (
<div
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<div role="dialog" aria-modal="true" aria-labelledby="rbs-import-title" className="bg-[#0f1115] border border-amber-500/30 rounded-xl shadow-[0_0_60px_rgba(245,158,11,0.2)] w-full max-w-4xl max-h-[90vh] flex flex-col">
<div
className="absolute inset-0 z-0"
onClick={onClose}
aria-hidden="true"
/>
<div role="dialog" aria-modal="true" aria-labelledby="rbs-import-title" className="relative z-10 bg-[#0f1115] border border-amber-500/30 rounded-xl shadow-[0_0_60px_rgba(245,158,11,0.2)] w-full max-w-4xl max-h-[90vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-3">
Expand Down
10 changes: 6 additions & 4 deletions src/components/VoiceEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,19 @@ export const VoiceEditor: React.FC<VoiceEditorProps> = ({ onClose }) => {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
role="presentation"
onClick={onClose}
>
<div
className="absolute inset-0 z-0"
onClick={onClose}
aria-hidden="true"
/>
<div
ref={trapRef}
className="bg-gray-900 border border-purple-500 rounded-xl p-6 w-[600px] shadow-2xl outline-none"
className="relative z-10 bg-gray-900 border border-purple-500 rounded-xl p-6 w-[600px] shadow-2xl outline-none"
role="dialog"
aria-modal="true"
aria-labelledby="voice-designer-title"
tabIndex={-1}
onClick={e => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<h2 id="voice-designer-title" className="text-xl font-orbitron text-purple-400">VOICE DESIGNER <span className="text-xs text-gray-500 ml-2">(WebGPU)</span></h2>
Expand Down
Loading