fix(canvas/mobile): WCAG 2.4.7 focus-visible rings on all mobile interactive buttons
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
CI / Detect changes (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 12s
E2E API Smoke Test / detect-changes (pull_request) Successful in 6s
E2E Chat / detect-changes (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 5s
Harness Replays / detect-changes (pull_request) Successful in 5s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m2s
CI / Platform (Go) (pull_request) Successful in 5m11s
CI / Canvas (Next.js) (pull_request) Failing after 6m33s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Failing after 5m52s
gate-check-v3 / gate-check (pull_request) Successful in 5s
qa-review / approved (pull_request) Failing after 3s
security-review / approved (pull_request) Failing after 3s
CI / Python Lint & Test (pull_request) Successful in 6m32s
sop-tier-check / tier-check (pull_request) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1s
Harness Replays / Harness Replays (pull_request) Successful in 2s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 1s
E2E Chat / E2E Chat (pull_request) Failing after 3m59s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 6m54s
sop-checklist / all-items-acked (pull_request) acked: 5/7 — missing: root-cause, no-backwards-compat
sop-checklist / na-declarations (pull_request) N/A: (none)

Adds focus-visible:ring-2 to keyboard-navigable buttons across all mobile
components that previously relied on inline styles only:

- MobileChat.tsx: Back, More, tab-switch, Retry, Remove file, Attach, Send
- MobileHome.tsx: Spawn FAB
- MobileSpawn.tsx: Close, template select, tier select, Spawn agent
- MobileMe.tsx: Accent swatches, Theme/Density segmented controls
- MobileDetail.tsx: Back, More, tab-switch, Open chat CTA
- MobileComms.tsx: filter chips
- components.tsx: AgentCard, FilterChips

Tailwind focus-visible utilities applied alongside existing inline styles;
no visual change for mouse users.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Molecule AI · core-uiux 2026-05-16 22:13:22 +00:00
parent e8f2ce9327
commit c760768ffa
7 changed files with 21 additions and 1 deletions

View File

@ -339,6 +339,7 @@ export function MobileChat({
type="button"
onClick={onBack}
aria-label="Back"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: 36,
height: 36,
@ -385,6 +386,7 @@ export function MobileChat({
<button
type="button"
aria-label="More"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: 36,
height: 36,
@ -415,6 +417,7 @@ export function MobileChat({
key={t.id}
type="button"
onClick={() => setTab(t.id)}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
padding: "4px 0 8px",
border: "none",
@ -478,6 +481,7 @@ export function MobileChat({
onClick={() => {
loadInitial();
}}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
padding: "6px 14px",
borderRadius: 14,
@ -619,6 +623,7 @@ export function MobileChat({
type="button"
onClick={() => removePendingFile(i)}
aria-label={`Remove ${f.name}`}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
border: "none",
background: "transparent",
@ -659,6 +664,7 @@ export function MobileChat({
onClick={() => fileInputRef.current?.click()}
disabled={!reachable || sending || uploading}
aria-label="Attach"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: 32,
height: 32,
@ -719,6 +725,7 @@ export function MobileChat({
onClick={send}
disabled={(!draft.trim() && pendingFiles.length === 0) || !reachable || sending || uploading}
aria-label="Send"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: 36,
height: 36,

View File

@ -218,6 +218,7 @@ export function MobileComms({ dark }: { dark: boolean }) {
key={o.id}
type="button"
onClick={() => setFilter(o.id)}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
display: "inline-flex",
alignItems: "center",

View File

@ -83,11 +83,12 @@ export function MobileDetail({
type="button"
onClick={onBack}
aria-label="Back"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={iconButtonStyle(p, dark)}
>
{Icons.back({ size: 18 })}
</button>
<button type="button" aria-label="More" style={iconButtonStyle(p, dark)}>
<button type="button" aria-label="More" className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none" style={iconButtonStyle(p, dark)}>
{Icons.more({ size: 18 })}
</button>
</div>
@ -183,6 +184,7 @@ export function MobileDetail({
key={t.id}
type="button"
onClick={() => setTab(t.id)}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
padding: "8px 14px",
borderRadius: 999,
@ -215,6 +217,7 @@ export function MobileDetail({
type="button"
onClick={onChat}
data-testid="mobile-chat-cta"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: "100%",
height: 52,

View File

@ -183,6 +183,7 @@ export function MobileHome({
type="button"
onClick={onSpawn}
aria-label="Spawn new agent"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
position: "absolute",
right: 24,

View File

@ -83,6 +83,7 @@ export function MobileMe({
type="button"
onClick={() => setAccent(c)}
aria-label={`Set accent ${c}`}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: 36,
height: 36,
@ -173,6 +174,7 @@ function SegmentedRow({
key={o.id}
type="button"
onClick={() => onChange(o.id)}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
flex: 1,
padding: "10px 8px",

View File

@ -148,6 +148,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
type="button"
onClick={onClose}
aria-label="Close"
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: 32,
height: 32,
@ -214,6 +215,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
setTplId(t.id);
setTier(tCode);
}}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
background: on
? dark
@ -330,6 +332,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
key={t}
type="button"
onClick={() => setTier(t)}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
flex: 1,
padding: "10px 8px",
@ -377,6 +380,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
type="button"
onClick={handleSpawn}
disabled={busy || !tplId || templates.length === 0}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
width: "100%",
height: 52,

View File

@ -292,6 +292,7 @@ export function AgentCard({
data-testid="workspace-card"
aria-label={`${agent.name}, status: ${agent.status}, tier ${agent.tier}${agent.remote ? ", remote" : ""}`}
onClick={onClick}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
display: "block",
width: "100%",
@ -445,6 +446,7 @@ export function FilterChips({
type="button"
aria-checked={on}
onClick={() => onChange(o.id)}
className="focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:outline-none"
style={{
display: "inline-flex",
alignItems: "center",