Merge pull request #1594 from Molecule-AI/fix/canvas-a11y-clean

fix(canvas/a11y): aria-hidden on decorative SVGs + MissingKeysModal semantics
This commit is contained in:
molecule-ai[bot] 2026-04-22 18:11:12 +00:00 committed by GitHub
commit 6bd1691446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 23 additions and 11 deletions

View File

@ -166,7 +166,6 @@ export function CreateWorkspaceButton() {
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm" />
<Dialog.Content
className="fixed z-50 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-zinc-900 border border-zinc-700/60 rounded-2xl shadow-2xl shadow-black/40 w-[400px] max-h-[90vh] overflow-y-auto p-6"
aria-describedby={undefined}
>
<Dialog.Title className="text-base font-semibold text-zinc-100 mb-1">
Create Workspace

View File

@ -101,7 +101,7 @@ export function DeleteCascadeConfirmDialog({
{/* Warning */}
<div className="flex gap-3 mb-4">
<div className="mt-0.5 shrink-0 w-8 h-8 rounded-full bg-red-900/30 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-red-400">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-red-400" aria-hidden="true">
<path d="M8 3L14 13H2L8 3Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
<path d="M8 7v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { api } from "@/lib/api";
import { getKeyLabel } from "@/lib/deploy-preflight";
@ -38,6 +38,7 @@ export function MissingKeysModal({
}: Props) {
const [entries, setEntries] = useState<KeyEntry[]>([]);
const [globalError, setGlobalError] = useState<string | null>(null);
const firstInputRef = useRef<HTMLInputElement>(null);
// Initialize entries when modal opens or missingKeys change
useEffect(() => {
@ -55,7 +56,14 @@ export function MissingKeysModal({
setGlobalError(null);
}, [open, missingKeys]);
// Keyboard handler
// Focus first input when modal opens
useEffect(() => {
if (!open) return;
const raf = requestAnimationFrame(() => {
firstInputRef.current?.focus();
});
return () => cancelAnimationFrame(raf);
}, [open]);
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
@ -134,7 +142,12 @@ export function MissingKeysModal({
/>
{/* Dialog */}
<div className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 overflow-hidden">
<div
role="dialog"
aria-modal="true"
aria-labelledby="missing-keys-title"
className="relative bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl shadow-black/50 max-w-[440px] w-full mx-4 overflow-hidden"
>
{/* Header */}
<div className="px-5 py-4 border-b border-zinc-800">
<div className="flex items-center gap-2 mb-1">
@ -150,7 +163,7 @@ export function MissingKeysModal({
<circle cx="6" cy="8.5" r="0.5" fill="#fbbf24" />
</svg>
</div>
<h3 className="text-sm font-semibold text-zinc-100">
<h3 id="missing-keys-title" className="text-sm font-semibold text-zinc-100">
Missing API Keys
</h3>
</div>
@ -193,7 +206,7 @@ export function MissingKeysModal({
onChange={(e) => updateEntry(index, { value: e.target.value.trimStart() })}
placeholder={entry.key.includes("API_KEY") ? "sk-..." : "Enter value"}
type="password"
autoFocus={index === 0}
ref={index === 0 ? firstInputRef : undefined}
onKeyDown={(e) => {
if (e.key === "Enter" && entry.value.trim()) {
handleSaveKey(index);

View File

@ -159,7 +159,7 @@ export function Toolbar() {
title={`Stop all running tasks (${counts.activeTasks} active)`}
aria-label={stopping ? "Stopping all running tasks" : `Stop all running tasks (${counts.activeTasks} active)`}
>
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-red-400">
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor" className="text-red-400" aria-hidden="true">
<rect x="2" y="2" width="12" height="12" rx="2" />
</svg>
<span className="text-[10px] text-red-300 font-medium">
@ -177,7 +177,7 @@ export function Toolbar() {
title={`Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} that need to pick up config or secret changes`}
aria-label={restartingAll ? "Restarting workspaces" : `Restart ${needsRestartNodes.length} workspace${needsRestartNodes.length === 1 ? "" : "s"} pending config or secret changes`}
>
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-amber-400">
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-amber-400" aria-hidden="true">
<path d="M2 8a6 6 0 1 1 1.76 4.24M2 13v-3h3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className="text-[10px] text-amber-300 font-medium">
@ -253,7 +253,7 @@ export function Toolbar() {
onClick={() => useCanvasStore.getState().setSearchOpen(true)}
className="flex items-center gap-1.5 px-2.5 py-1 bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700/40 rounded-lg transition-colors"
>
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
<path d="M11 11l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
@ -269,7 +269,7 @@ export function Toolbar() {
aria-expanded={helpOpen}
aria-label="Open quick help"
>
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" className="text-zinc-500" aria-hidden="true">
<path d="M8 12v.5M6.5 6.3A1.9 1.9 0 1 1 9 8.1c-.7.4-1 .8-1 1.7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.2" />
</svg>