無障礙元件展示站。每個元件有獨立頁面,包含可操作範例、即時的 accessibility tree(Role / Name / Status)、鍵盤操作、ARIA 屬性、螢幕閱讀器預期行為與對應的 WCAG 條款。
npm install
npm run dev # http://localhost:3000
npm test # 單元測試
npm run typecheck # 型別檢查
npm run build # 正式建置某個路由在 dev server 上 404,但 npm run build 列得出來、測試也都過 ——
dev server 的路由狀態壞掉了,重啟即可:
pkill -f "next dev" && npm run dev最容易觸發的情境是在 dev server 執行中切換 git 分支。git checkout 會把目標
分支沒有的檔案刪掉,git merge 再建回來;Turbopack 的路由表跟不上這串刪除重建,
該路由就會停在未註冊狀態。touch 檔案救不回來,因為壞掉的是 server 內部狀態而
不是檔案監看。切分支前先停掉 dev server,或切完重啟。
同理,不要在 dev server 執行中跑 npm run build —— 兩者共用 .next 目錄。
視覺與元件庫移植自 SSO/center,tokens 定義在 app/globals.css 的 @theme
區塊。幾條規則:
- 字級用
typography-*語意 class,不直接寫text-* - 間距與圓角只用 tokens(
p-16、rounded-24…) - focus 一律是
focus-visible:ring-2 focus-visible:ring-orange-PRIMARY Card表面不加陰影
lib/apg-rules.ts 由 data/apg-pattern-rules.csv 自動產生(npm run generate:apg-rules),
收錄 289 條 W3C ARIA APG pattern 規則,涵蓋 20 個元件。
決定交付範圍的是「知識難度」欄:值為 2 的 119 條規則必須實作到 demo 上。 每個元件至少有一條,所以 20 個元件都要做。
mustDemo(slug) 回傳某元件全部的難度 2 規則。已完成的元件必須在
components-registry.ts 的 coveredRules 逐條列出,測試會比對兩者是否一致 ——
規則表新增一條難度 2 的規則,對應元件的測試就會紅燈。
那份 CSV 是人工維護的試算表匯出,有兩處陷阱,產生腳本都已處理並有測試釘住:
APG-TIP-009 整列欄位左移一格;知識難度(修) 為數字時優先於 知識難度。
- 先看
mustDemo("<slug>")列出該元件必須涵蓋的難度 2 規則 - 建立
app/(component)/<slug>/page.tsx,用DemoPage並傳入五個區塊 - 互動範例與
A11yTree放在同目錄的 client 元件(參考dialog/DialogDemo.tsx) - 把 registry 那筆的
status改成"done",並在coveredRules逐條列出規則編號 與它寫在頁面哪一處
DemoPage 的 sections 同時驅動內容與右側目錄,不需要另外維護目錄。
面板拆成三層,各自只做一件事:
lib/a11y/use-a11y-node.ts— 觀察。持續算出目標元素的{ role, roleDescription, name, description, status }components/demo/A11yTreeView.tsx— 呈現。純函式,吃一個A11yNode | nullcomponents/demo/A11yTree.tsx— 組合上面兩者
拆開的好處是餵給 view 的值可以來自即時觀察、保留下來的舊值,或手寫的宣告值 (用於尚未實作的元件頁,或與實際值並排對照)。
值本身這樣算出來:
- Role 由
lib/a11y/implicit-roles.ts推導(明寫的role屬性優先)。 有aria-roledescription時一併顯示成carousel(group):前者是螢幕閱讀器 唸出來的字,後者是決定輔助科技怎麼操作它的角色 - Name 由
dom-accessibility-api依 accname 規範計算 - Description 同樣由
dom-accessibility-api計算,通常來自aria-describedby - Status 收集
aria-expanded、aria-checked等會隨操作改變的屬性, 每一項寫成「屬性=值」,順序固定 - 四列一律顯示。沒有值時明講「(無)」而不是把整列藏起來 —— 讀者要看見的 正是「這個元件有名稱但沒有描述」這件事,消失的列說不出這句話
- 以
MutationObserver加事件監聽重算,互動時即時更新
用 selector 而非 ref,是因為目標元素可能在關閉時整個離開 DOM。
<A11yTree latch /> 會在目標離開無障礙樹後保留最後一次的值。用於 Dialog、
Toast 這類會整個離開 DOM 的元件:開啟時遮罩壓暗面板反而讀不到數值,關閉後
留著值才看得清、也才截得下來。
舊值在畫面上不另外標示文字,只把整組數值調淡。那個狀態透過
data-stale="true" 曝露在 DOM 上供測試驗證。
長駐頁面的元件(Button、Checkbox)不要開 latch,維持即時才誠實。
app/— 路由。(component)是 route group,不出現在網址中components/— 自 SSO center 移植的 design system 元件components/demo/— demo 站專用的展示元件lib/a11y/— accessibility tree 的計算邏輯(純函式,有完整單元測試)docs/superpowers/— 設計文件與實作計畫