Skip to content

Commit 923838d

Browse files
DaviRain-Suclaude
andcommitted
Accessibility pass across the JSX UI (a11y warnings 632 -> 32)
Add src/lib/a11y.js `clickable()` (role + tabIndex + onClick + Enter/Space onKeyDown) + unit test, and spread it onto genuinely-clickable <div>/<span> elements so they are keyboard-operable and focusable without any visual change (mouse behavior identical). Across 28 components: `type="button"` on bare <button>s, `aria-hidden` on decorative icon <svg>s (+ a <title> on the interactive echoes constellation), and role/semantic fixes. Done as fan-out -> adversarial review -> correction. The review caught real regressions, all corrected: - Restored 7 intentional `autoFocus` inputs (an over-eager noAutofocus "fix" had deleted them). - Reverted ~13 <a>->'<button>' conversions that broke anchor-targeted CSS (.crumb a, .appbar .nav a, .brand) and showed UA button chrome — kept as <a>. - Restored the wallet sheet-scrim's onMouseDown + e.currentTarget-guard dismiss and the app modal-content stopPropagation container (biome-ignored as backdrops/containment, not controls). - Turned conditional handlers back into conditional spreads `{...(cond ? clickable(fn) : {})}` so inert elements aren't focusable no-op buttons. Promoted back to ERROR (now enforced by the blocking lint gate): useButtonType, useKeyWithClickEvents, useFocusableInteractive, useAriaPropsSupportedByRole, noNoninteractiveElementToInteractiveRole, noSvgWithoutTitle. Left as warn (tracked backlog, 32): noStaticElementInteractions (11), useValidAnchor (13 — anchors that need CSS work to become real buttons), noAutofocus (7 — intentional focus UX), useSemanticElements (1). biome check exit 0, npm test green (178 + 10 + 73), vite build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dcaa2c9 commit 923838d

32 files changed

Lines changed: 818 additions & 347 deletions

biome.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,9 @@
4646
},
4747
"a11y": {
4848
"noStaticElementInteractions": "warn",
49-
"useButtonType": "warn",
50-
"useKeyWithClickEvents": "warn",
51-
"noSvgWithoutTitle": "warn",
5249
"useValidAnchor": "warn",
5350
"useSemanticElements": "warn",
54-
"noAutofocus": "warn",
55-
"useFocusableInteractive": "warn",
56-
"useAriaPropsSupportedByRole": "warn",
57-
"noNoninteractiveElementToInteractiveRole": "warn"
51+
"noAutofocus": "warn"
5852
}
5953
}
6054
},

src/components/cli-auth.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function CliAuth({ deviceCode, userCode }) {
5555
code: {userCode || deviceCode.slice(0, 8).toUpperCase()}
5656
</div>
5757
<button
58+
type="button"
5859
className="btn btn-primary"
5960
disabled={status === "working" || status === "done"}
6061
onClick={approve}

src/components/ios-frame.jsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ function IOSStatusBar({ dark = false, time = "9:41" }) {
6767
paddingRight: 1,
6868
}}
6969
>
70-
<svg width="19" height="12" viewBox="0 0 19 12">
70+
<svg width="19" height="12" viewBox="0 0 19 12" aria-hidden="true">
7171
<rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill={c} />
7272
<rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill={c} />
7373
<rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill={c} />
7474
<rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill={c} />
7575
</svg>
76-
<svg width="17" height="12" viewBox="0 0 17 12">
76+
<svg width="17" height="12" viewBox="0 0 17 12" aria-hidden="true">
7777
<path
7878
d="M8.5 3.2C10.8 3.2 12.9 4.1 14.4 5.6L15.5 4.5C13.7 2.7 11.2 1.5 8.5 1.5C5.8 1.5 3.3 2.7 1.5 4.5L2.6 5.6C4.1 4.1 6.2 3.2 8.5 3.2Z"
7979
fill={c}
@@ -84,7 +84,7 @@ function IOSStatusBar({ dark = false, time = "9:41" }) {
8484
/>
8585
<circle cx="8.5" cy="10.5" r="1.5" fill={c} />
8686
</svg>
87-
<svg width="27" height="13" viewBox="0 0 27 13">
87+
<svg width="27" height="13" viewBox="0 0 27 13" aria-hidden="true">
8888
<rect
8989
x="0.5"
9090
y="0.5"
@@ -209,7 +209,14 @@ function IOSNavBar({ title = "Title", dark = false, trailingIcon = true }) {
209209
>
210210
{/* back chevron */}
211211
{pillIcon(
212-
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" style={{ marginLeft: -1 }}>
212+
<svg
213+
width="12"
214+
height="20"
215+
viewBox="0 0 12 20"
216+
fill="none"
217+
style={{ marginLeft: -1 }}
218+
aria-hidden="true"
219+
>
213220
<path
214221
d="M10 2L2 10l8 8"
215222
stroke={muted}
@@ -222,7 +229,7 @@ function IOSNavBar({ title = "Title", dark = false, trailingIcon = true }) {
222229
{/* trailing ellipsis */}
223230
{trailingIcon &&
224231
pillIcon(
225-
<svg width="22" height="6" viewBox="0 0 22 6">
232+
<svg width="22" height="6" viewBox="0 0 22 6" aria-hidden="true">
226233
<circle cx="3" cy="3" r="2.5" fill={muted} />
227234
<circle cx="11" cy="3" r="2.5" fill={muted} />
228235
<circle cx="19" cy="3" r="2.5" fill={muted} />
@@ -283,7 +290,7 @@ function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark
283290
<div style={{ flex: 1, color: text }}>{title}</div>
284291
{detail && <span style={{ color: sec, marginRight: 6 }}>{detail}</span>}
285292
{chevron && (
286-
<svg width="8" height="14" viewBox="0 0 8 14" style={{ flexShrink: 0 }}>
293+
<svg width="8" height="14" viewBox="0 0 8 14" style={{ flexShrink: 0 }} aria-hidden="true">
287294
<path
288295
d="M1 1l6 6-6 6"
289296
stroke={ter}
@@ -425,12 +432,12 @@ function IOSKeyboard({ dark = false }) {
425432
// special-key icons
426433
const icons = {
427434
shift: (
428-
<svg width="19" height="17" viewBox="0 0 19 17">
435+
<svg width="19" height="17" viewBox="0 0 19 17" aria-hidden="true">
429436
<path d="M9.5 1L1 9.5h4.5V16h8V9.5H18L9.5 1z" fill={glyph} />
430437
</svg>
431438
),
432439
del: (
433-
<svg width="23" height="17" viewBox="0 0 23 17">
440+
<svg width="23" height="17" viewBox="0 0 23 17" aria-hidden="true">
434441
<path
435442
d="M7 1h13a2 2 0 012 2v11a2 2 0 01-2 2H7l-6-7.5L7 1z"
436443
fill="none"
@@ -442,7 +449,7 @@ function IOSKeyboard({ dark = false }) {
442449
</svg>
443450
),
444451
ret: (
445-
<svg width="20" height="14" viewBox="0 0 20 14">
452+
<svg width="20" height="14" viewBox="0 0 20 14" aria-hidden="true">
446453
<path
447454
d="M18 1v6H4m0 0l4-4M4 7l4 4"
448455
fill="none"

src/components/product-agents.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ function AgentSquare({ onBack }) {
6060
<div className="asqc-foot">
6161
<span className="asqc-stat">被引用 {a.cited.toLocaleString()}</span>
6262
<span className="asqc-stat">追踪 {a.tracking}</span>
63-
<button className="asqc-call">调用 / 订阅</button>
63+
<button type="button" className="asqc-call">
64+
调用 / 订阅
65+
</button>
6466
</div>
6567
</div>
6668
))}
@@ -72,7 +74,9 @@ function AgentSquare({ onBack }) {
7274
基于开放的 MCP 接口与 CC0 内容,训练一个读书
7375
Agent,署名发布到广场。无需平台许可,无人抽成。
7476
</div>
75-
<button className="btn btn-ghost">查看开发者文档 →</button>
77+
<button type="button" className="btn btn-ghost">
78+
查看开发者文档 →
79+
</button>
7680
</div>
7781
</div>
7882
</div>

src/components/product-agentview.jsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import { I } from "./product-shared.jsx";
33
import { findCatalogBook, getCatalogTotal, licenseLabel } from "../lib/catalog.js";
4+
import { clickable } from "../lib/a11y.js";
45

56
/* product-agentview.jsx — "Agent View": flip any page into the structured,
67
addressable, MCP representation an AI Agent sees. Reuses .term styles. */
@@ -73,15 +74,15 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
7374
const top = ranked.slice(0, 5);
7475
return (
7576
<>
76-
<div className="drawer-scrim" onClick={onClose} />
77+
<div className="drawer-scrim" {...clickable(onClose)} />
7778
<div className="agentview-drawer">
7879
<div className="av-head">
7980
<div className="av-orb">{I.agent}</div>
8081
<div>
8182
<div className="av-t">Agent 视角 · 榜单</div>
8283
<div className="av-s">同一份榜单,Agent 调用到的样子</div>
8384
</div>
84-
<span className="x" onClick={onClose}>
85+
<span className="x" {...clickable(onClose)}>
8586
{I.x}
8687
</span>
8788
</div>
@@ -96,7 +97,9 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
9697
</code>
9798
<span
9899
className="copy"
99-
onClick={() => onCopy(`liber://charts/${ctxCharts.win}/${ctxCharts.metric}`)}
100+
{...clickable(() =>
101+
onCopy(`liber://charts/${ctxCharts.win}/${ctxCharts.metric}`),
102+
)}
100103
>
101104
{I.copy}
102105
</span>
@@ -177,7 +180,7 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
177180
</div>
178181
<div className="av-foot">
179182
榜单不是排他的产品功能,而是<b>一份谁都能读的公共信号</b> ·{" "}
180-
<span className="av-square-link" onClick={onSquare}>
183+
<span className="av-square-link" {...clickable(onSquare)}>
181184
看看哪些 Agent 在用它 →
182185
</span>
183186
</div>
@@ -188,7 +191,7 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
188191

189192
return (
190193
<>
191-
<div className="drawer-scrim" onClick={onClose} />
194+
<div className="drawer-scrim" {...clickable(onClose)} />
192195
<div className="agentview-drawer">
193196
<div className="av-head">
194197
<div className="av-orb">{I.agent}</div>
@@ -198,7 +201,7 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
198201
{corpus ? "整座图书馆" : `《${book.t}》`} · AI Agent 看到的样子
199202
</div>
200203
</div>
201-
<span className="x" onClick={onClose}>
204+
<span className="x" {...clickable(onClose)}>
202205
{I.x}
203206
</span>
204207
</div>
@@ -211,7 +214,7 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
211214
<div className="ar">
212215
<span className="k">liber</span>
213216
<code>{addr.uri}</code>
214-
<span className="copy" onClick={() => onCopy(addr.uri)}>
217+
<span className="copy" {...clickable(() => onCopy(addr.uri))}>
215218
{I.copy}
216219
</span>
217220
</div>
@@ -382,12 +385,12 @@ function AgentView({ context, onClose, onCopy, onSquare, onGraph }) {
382385
全部 CC0 · 无需鉴权 · 无抽成 · 无下架 — <b>内容即接口</b>
383386
<br />
384387
{onGraph && (
385-
<span className="av-square-link" onClick={onGraph}>
388+
<span className="av-square-link" {...clickable(onGraph)}>
386389
看全馆思维链接图谱 →
387390
</span>
388391
)}
389392
{onGraph && <br />}
390-
<span className="av-square-link" onClick={onSquare}>
393+
<span className="av-square-link" {...clickable(onSquare)}>
391394
浏览 Agent 广场 —谁在读这座图书馆 →
392395
</span>
393396
</div>

src/components/product-app.jsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Library } from "./product-library.jsx";
77
import { Detail } from "./product-detail.jsx";
88
import { SearchOverlay } from "./product-search.jsx";
99
import { CliAuth } from "./cli-auth.jsx";
10+
import { clickable } from "../lib/a11y.js";
1011
import {
1112
createRootRoute,
1213
createRoute,
@@ -500,11 +501,13 @@ function RootLayout() {
500501
{phonePreview && (
501502
<div
502503
className="phone-preview-scrim"
503-
onClick={() => {
504+
{...clickable(() => {
504505
setPhonePreview(false);
505506
window.dispatchEvent(new CustomEvent("liber-device-reset"));
506-
}}
507+
})}
507508
>
509+
{/* biome-ignore lint/a11y/noStaticElementInteractions: click-containment, not a control */}
510+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: click-containment, not a control */}
508511
<div className="phone-preview-wrap" onClick={(e) => e.stopPropagation()}>
509512
<div className="phone-preview-cap">移动端预览 · 390pt</div>
510513
<IOSDevice>
@@ -517,6 +520,7 @@ function RootLayout() {
517520
</div>
518521
</IOSDevice>
519522
<button
523+
type="button"
520524
className="phone-preview-close"
521525
onClick={() => {
522526
setPhonePreview(false);
@@ -751,7 +755,7 @@ function Placeholder({ name, onBack }) {
751755
这一部分还在设计中。当前原型聚焦在{" "}
752756
<b style={{ color: "var(--accent)" }}>书库 → 详情 → 阅读器</b> 这条主线。
753757
</p>
754-
<button className="btn btn-primary" onClick={onBack}>
758+
<button type="button" className="btn btn-primary" onClick={onBack}>
755759
回到书库 <span className="arr"></span>
756760
</button>
757761
</div>

0 commit comments

Comments
 (0)