fix(canvas/a11y): dialog aria-modal, icon-button labels, focus management
- CookieConsent.tsx: add aria-modal="true" (WCAG 2.1.1)
- ConsoleModal.tsx: add useRef + requestAnimationFrame focus management on open
- ConversationTraceModal.tsx: remove redundant aria-describedby={undefined}
- FileTree.tsx: add aria-label to directory/file delete buttons (WCAG 4.1.2)
- FileEditor.tsx: add aria-label to download button (WCAG 4.1.2)
- ScheduleTab.tsx: add aria-label to Run Now, Edit, Delete icon buttons
- form-inputs.tsx: add aria-label to tag removal button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
de11188cc4
commit
e211a25ccd
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api } from "@/lib/api";
|
||||
import { showToast } from "@/components/Toaster";
|
||||
@ -27,11 +27,21 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Focus close button when modal opens
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const raf = requestAnimationFrame(() => {
|
||||
closeButtonRef.current?.focus();
|
||||
});
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let ignore = false;
|
||||
@ -99,6 +109,7 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="text-zinc-400 hover:text-zinc-100 text-sm px-2"
|
||||
|
||||
@ -97,7 +97,6 @@ export function ConversationTraceModal({ open, workspaceId: _workspaceId, onClos
|
||||
<Dialog.Content
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
||||
aria-label="Conversation trace"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
{/* Modal panel */}
|
||||
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl max-w-[700px] w-full max-h-[85vh] flex flex-col overflow-hidden">
|
||||
|
||||
@ -88,6 +88,7 @@ export function CookieConsent() {
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cookie-consent-title"
|
||||
aria-describedby="cookie-consent-body"
|
||||
className="fixed bottom-0 left-0 right-0 z-[9999] border-t border-zinc-800 bg-zinc-950/95 backdrop-blur-sm p-4 shadow-[0_-4px_12px_rgba(0,0,0,0.4)]"
|
||||
|
||||
@ -55,8 +55,8 @@ export function FileEditor({
|
||||
{success && <span className="text-[9px] text-emerald-400">{success}</span>}
|
||||
<button
|
||||
onClick={onDownload}
|
||||
aria-label="Download file"
|
||||
className="text-[10px] text-zinc-500 hover:text-zinc-300"
|
||||
title="Download file"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
|
||||
@ -66,6 +66,7 @@ function TreeItem({
|
||||
<span className="text-[10px]">📁</span>
|
||||
<span className="text-[10px] text-zinc-400 flex-1">{node.name}</span>
|
||||
<button
|
||||
aria-label={`Delete ${node.name}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(node.path);
|
||||
@ -102,6 +103,7 @@ function TreeItem({
|
||||
<span className="text-[9px]">{getIcon(node.name, false)}</span>
|
||||
<span className="text-[10px] flex-1 truncate font-mono">{node.name}</span>
|
||||
<button
|
||||
aria-label={`Delete ${node.name}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(node.path);
|
||||
|
||||
@ -351,6 +351,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => handleRunNow(sched)}
|
||||
aria-label={`Run schedule ${sched.name} now`}
|
||||
className="text-[11px] px-1.5 py-0.5 text-blue-400 hover:bg-blue-600/20 rounded transition-colors"
|
||||
title="Run now"
|
||||
>
|
||||
@ -358,6 +359,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(sched)}
|
||||
aria-label={`Edit schedule ${sched.name}`}
|
||||
className="text-[11px] px-1.5 py-0.5 text-zinc-400 hover:bg-zinc-700 rounded transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
@ -365,6 +367,7 @@ export function ScheduleTab({ workspaceId }: Props) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPendingDelete({ id: sched.id, name: sched.name })}
|
||||
aria-label={`Delete schedule ${sched.name}`}
|
||||
className="text-[11px] px-1.5 py-0.5 text-red-400 hover:bg-red-600/20 rounded transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
|
||||
@ -97,7 +97,7 @@ export function TagList({ label, values, onChange, placeholder }: { label: strin
|
||||
{values.map((v, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 font-mono">
|
||||
{v}
|
||||
<button onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-zinc-500 hover:text-red-400">×</button>
|
||||
<button aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-zinc-500 hover:text-red-400">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user