fix(canvas/a11y): add type=button to tab toolbar and settings buttons

WCAG 4.1.2 / bug #1669 follow-up — fixing remaining buttons missing
type="button" across tab components and settings.

Files changed:
- FilesTab/FilesToolbar.tsx (5 buttons): +New, Upload, Export,
  Clear, ↻ (all had onClick, no type=button)
- config/secrets-section.tsx (7 buttons): Remove, Edit/Update/Cancel
  across 2 SecretRow variants + add-variable form
- config/form-inputs.tsx (2 buttons): tag remove ×, section collapse toggle
- ActivityTab.tsx (1 button): row expand toggle
- TracesTab.tsx (1 button): Refresh
- settings/UnsavedChangesGuard.tsx (2 buttons): Keep editing, Discard
  (Radix AlertDialog asChild wrappers — type=button prevents form submit)

Total: 18 buttons fixed across 6 files. 934/934 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Molecule AI · core-uiux 2026-04-24 14:41:35 +00:00
parent 6b62391e5d
commit 1126d7b66d
6 changed files with 20 additions and 20 deletions

View File

@ -31,12 +31,12 @@ export function UnsavedChangesGuard({
</AlertDialog.Title>
<div className="guard-dialog__actions">
<AlertDialog.Cancel asChild>
<button className="guard-dialog__keep-btn" onClick={onKeepEditing}>
<button type="button" className="guard-dialog__keep-btn">
Keep editing
</button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<button className="guard-dialog__discard-btn" onClick={onDiscard}>
<button type="button" className="guard-dialog__discard-btn">
Discard
</button>
</AlertDialog.Action>

View File

@ -186,7 +186,7 @@ function ActivityRow({
: "bg-zinc-800/60 border-zinc-700/40"
}`}
>
<button onClick={onToggle} className="w-full text-left px-3 py-2">
<button type="button" onClick={onToggle} className="w-full text-left px-3 py-2">
{/* Top row: type badge + method + time */}
<div className="flex items-center gap-2">
<span className={`text-[8px] font-mono px-1.5 py-0.5 rounded ${typeStyle.text} ${typeStyle.bg} border ${typeStyle.border}`}>

View File

@ -44,7 +44,7 @@ export function FilesToolbar({
<div className="flex gap-1.5">
{root === "/configs" && (
<>
<button onClick={onNewFile} aria-label="Create new file" className="text-[10px] text-blue-400 hover:text-blue-300" title="Create new file">
<button type="button" onClick={onNewFile} aria-label="Create new file" className="text-[10px] text-blue-400 hover:text-blue-300" title="Create new file">
+ New
</button>
<input
@ -57,20 +57,20 @@ export function FilesToolbar({
className="hidden"
onChange={(e) => e.target.files && onUpload(e.target.files)}
/>
<button onClick={() => uploadRef.current?.click()} aria-label="Upload folder" className="text-[10px] text-blue-400 hover:text-blue-300" title="Upload folder">
<button type="button" onClick={() => uploadRef.current?.click()} aria-label="Upload folder" className="text-[10px] text-blue-400 hover:text-blue-300" title="Upload folder">
Upload
</button>
</>
)}
<button onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Download all files">
<button type="button" onClick={onDownloadAll} aria-label="Download all files" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Download all files">
Export
</button>
{root === "/configs" && (
<button onClick={onClearAll} aria-label="Delete all files" className="text-[10px] text-red-400/60 hover:text-red-400" title="Delete all files">
<button type="button" onClick={onClearAll} aria-label="Delete all files" className="text-[10px] text-red-400/60 hover:text-red-400" title="Delete all files">
Clear
</button>
)}
<button onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Refresh">
<button type="button" onClick={onRefresh} aria-label="Refresh file list" className="text-[10px] text-zinc-500 hover:text-zinc-300" title="Refresh">
</button>
</div>

View File

@ -55,7 +55,7 @@ export function TracesTab({ workspaceId }: Props) {
<div className="p-4 space-y-2">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-zinc-400">{traces.length} traces</span>
<button onClick={loadTraces} className="text-[10px] text-zinc-500 hover:text-zinc-300">
<button type="button" onClick={loadTraces} className="text-[10px] text-zinc-500 hover:text-zinc-300">
Refresh
</button>
</div>

View File

@ -104,7 +104,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 aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-zinc-500 hover:text-red-400">×</button>
<button type="button" aria-label={`Remove tag ${v}`} onClick={() => onChange(values.filter((_, j) => j !== i))} className="text-zinc-500 hover:text-red-400">×</button>
</span>
))}
</div>
@ -131,7 +131,7 @@ export function Section({ title, children, defaultOpen = true }: { title: string
const [open, setOpen] = useState(defaultOpen);
return (
<div className="border border-zinc-800 rounded mb-2">
<button onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-3 py-1.5 text-[10px] text-zinc-400 hover:text-zinc-200 bg-zinc-900/50">
<button type="button" onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-3 py-1.5 text-[10px] text-zinc-400 hover:text-zinc-200 bg-zinc-900/50">
<span className="font-medium uppercase tracking-wider">{title}</span>
<span>{open ? "▾" : "▸"}</span>
</button>

View File

@ -113,9 +113,9 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
{isSet && <span className="text-[10px] text-green-500 bg-green-900/30 px-1.5 py-0.5 rounded">Set</span>}
{scope && <ScopeBadge scope={scope} />}
{!editing && isSet && (globalMode || scope !== "global") && (
<button onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button>
<button type="button" onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button>
)}
<button onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300">
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300">
{actionLabel()}
</button>
</div>
@ -128,7 +128,7 @@ function SecretRow({ label, secretKey, isSet, scope, globalMode, onSave, onDelet
type={isPlaintext ? "text" : "password"} autoFocus
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500"
/>
<button
<button type="button"
onClick={() => { onSave(value); setEditing(false); setValue(""); }}
disabled={!value}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30"
@ -165,10 +165,10 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
<span className="text-[10px] text-green-500">Set</span>
{!globalMode && <ScopeBadge scope={scope} />}
{canDelete && !editing && (
<button onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button>
<button type="button" onClick={onDelete} className="text-[11px] text-red-400 hover:text-red-300">Remove</button>
)}
{(canDelete || showOverride) && (
<button onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300">
<button type="button" onClick={() => setEditing(!editing)} className="text-[11px] text-blue-400 hover:text-blue-300">
{editing ? "Cancel" : showOverride ? "Override" : "Update"}
</button>
)}
@ -181,7 +181,7 @@ function CustomSecretRow({ secretKey, scope, globalMode, onSave, onDelete }: {
placeholder="New value" type="password" autoFocus
className="flex-1 bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 font-mono focus:outline-none focus:border-blue-500"
/>
<button
<button type="button"
onClick={() => { onSave(value); setEditing(false); setValue(""); }}
disabled={!value}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30"
@ -355,16 +355,16 @@ export function SecretsSection({ workspaceId, requiredEnv }: { workspaceId: stri
<input value={newValue} onChange={(e) => setNewValue(e.target.value)} placeholder="Value" type="password"
className="w-full bg-zinc-900 border border-zinc-600 rounded px-2 py-1 text-[10px] text-zinc-100 focus:outline-none focus:border-blue-500" />
<div className="flex gap-2">
<button onClick={() => { if (newKey && newValue) handleSave(newKey, newValue); }} disabled={!newKey || !newValue}
<button type="button" onClick={() => { if (newKey && newValue) handleSave(newKey, newValue); }} disabled={!newKey || !newValue}
className="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-[10px] rounded text-white disabled:opacity-30">
Save{globalMode ? " (Global)" : ""}
</button>
<button onClick={() => { setShowAdd(false); setNewKey(""); setNewValue(""); }}
<button type="button" onClick={() => { setShowAdd(false); setNewKey(""); setNewValue(""); }}
className="px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-[10px] rounded text-zinc-300">Cancel</button>
</div>
</div>
) : (
<button onClick={() => setShowAdd(true)} className="text-[10px] text-blue-400 hover:text-blue-300">
<button type="button" onClick={() => setShowAdd(true)} className="text-[10px] text-blue-400 hover:text-blue-300">
+ Add {globalMode ? "Global " : ""}Variable
</button>
)}