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:
Molecule AI · core-uiux 2026-04-22 17:43:12 +00:00
parent de11188cc4
commit e211a25ccd
7 changed files with 20 additions and 4 deletions

View File

@ -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"

View File

@ -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">

View File

@ -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)]"

View File

@ -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>

View File

@ -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);

View File

@ -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"
>

View File

@ -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>