docs: P9-B spec + implementation plan (a11y/keyboard batch — WCAG contrast tokens, toast live-region restructure, skip-link, SPA focus management, table column-header scope, star accessible names)
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
# P9-B UX 第二批设计:可访问与键盘效率(a11y / keyboard)
|
||||
|
||||
日期:2026-10-01 | 决策链:owner UX 摸底三包(A 浏览器反馈 / B 效率可访问 / C 暗色模式)——A 已作 P9 第一批交付(crearte 0.22.0),第一梯队日常交互包已作 P10 交付(crearte 0.23.0)。本批 = 摸底 **B 包**,并收口 P10 终审转办的 T1(d)(实时区内嵌交互控件)。总纲仍为「继续增强该项目,从用户体验方面」。
|
||||
|
||||
仓库面:纯 `crearte`(前端)。零后端/部署改动。
|
||||
|
||||
## §1 现状勘查结论(已实测,不是推测)
|
||||
|
||||
已经就位、本批**不动**的面(避免重复劳动与无谓回归):
|
||||
|
||||
- `:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px }` 全局焦点环已在 `main.css:46`,`accent` 对 paper 3.26 / 对 surface 3.64,满足 WCAG 1.4.11 非文本对比 3:1。
|
||||
- `prefers-reduced-motion: reduce` 全局归零分支已在 `main.css:95-101`,覆盖 toast/lift/骨架动画。
|
||||
- 播放 iframe 已有 `:title="game.name"`(`GameHost.vue:105`);封面 `<img :alt="game.name">`(`GameCover.vue`);投稿封面预览 `alt="封面预览"`。
|
||||
- 页头三条主导航已有 `:aria-current="onX ? 'page' : undefined"`。
|
||||
- `FilterDrawer` 用原生 `<dialog>.showModal()`,焦点圈闭与 Esc 关闭由浏览器负责;关闭按钮有 `aria-label="关闭筛选"` 且 44px 触达面(`min-h-11 min-w-11`)。
|
||||
- `BaseSelect` 自绘 listbox 的键盘面已完整:trigger 上 ArrowDown/Up/Enter/Space 开面板,面板内 Esc 关并回焦、Arrow 移动、Enter/Space 选中、Tab 关不回焦,`role=listbox`/`role=option`/`aria-selected`/`aria-expanded`/`roving tabindex` 齐备。
|
||||
- 表单控件均有可见或 `sr-only` 的 `<label for>`(CatalogView / AdminUsersView 搜索、SubmitFormView 全字段)。
|
||||
- 登录/注册/账号三页的错误提示已做 `id` + `:aria-describedby` 字段级关联。
|
||||
- 收藏按钮已有 `:aria-pressed="favorited"`,心形字形已 `aria-hidden`。
|
||||
- **移动端页头无溢出**:实测 360px 与 320px 视口下 `documentElement.scrollWidth` 恰等于视口宽(320/360),页头内层 `scrollWidth` 亦相等。故本批**不引入汉堡菜单**——没有需要修的问题,加菜单只会增加键盘陷阱面。
|
||||
|
||||
实测出的真实缺口(本批范围):
|
||||
|
||||
1. `--color-success: #2fa46a` 对 paper 仅 **2.83**,对 surface 3.16 —— 作为成功 toast 的底色配 `text-paper`(12px 粗体,非大字号)实测 **3.16**,AA 正文需 4.5,**FAIL**。
|
||||
2. 错误 toast 用 `bg-accent`(#e8552f)配 `text-paper` 实测 **3.64**,**FAIL**(AA 正文需 4.5)。
|
||||
3. 三个管理视图共 **19 个 `<th>` 零 `scope`**,且两个「操作」列的表头是空 `<th class="px-3 py-2" />`(无可访问名);全仓 `scope="row"` 出现 0 次。屏幕阅读器读表格时无法把单元格与列头关联。
|
||||
4. 五星评分按钮的可访问名就是字形本身「★」/「☆」——读屏逐个念「星 星 星」,无法知道这是几分、当前评了几分、点了会怎样。
|
||||
5. 全站**无 skip-link**:键盘用户每个页面都要 Tab 过页头(logo + 3 条导航 + 贴纸 + 登录/用户菜单 4 项)才能到正文。
|
||||
6. SPA 导航后**无焦点管理**:`router` 的 `afterEach` 只设 title/description,焦点留在旧位置(通常是 body),读屏用户点链接后听不到任何新页面上下文。
|
||||
7. (P10 T1(d) 转办)toast 容器整体是 `role="status" aria-live="polite"`,而每条 toast 内含可交互的「关闭提示」按钮——实时区内嵌交互控件是公认反模式:读屏会在轮询实时区时把按钮一并播报,且用户无法可靠地把焦点停在该控件上。
|
||||
|
||||
## §2 决策表
|
||||
|
||||
| # | 决策点 | 定案 | 理由 / 放弃的替代 |
|
||||
|---|---|---|---|
|
||||
| D-A | `success` 色令牌加深 | `--color-success: #1f7a4d`(`text-paper` 在其上 **4.76**、白 **5.32**;作文字色对 paper 4.76 / 对 surface 5.32) | 唯一消费者是成功 toast 底色,改令牌即全站生效且不留分叉。放弃「只改 toast 用别的绿」——会让令牌与实际用色脱节,下一个消费者重踩。放弃 #186039(6.79):过暗,脱离新粗野主义的高饱和海报感。 |
|
||||
| D-B | 错误 toast 底色 | `error: 'bg-accent-ink text-paper'`(**4.87**) | `accent-ink` 已是既有令牌且已被 AdminUsersView 用作 `bg-accent-ink text-paper` 底色,复用即一致,不新增色。放弃新增 `--color-error` 令牌:与 `accent-ink` 同值同义,纯冗余。放弃给错误 toast 加白字:`text-paper` 本就是纸白,无需动。 |
|
||||
| D-C | 对比度守卫测试 | 新增 `app/lib/contrast.test.ts`:解析 `main.css` 的 `@theme` 令牌,按 WCAG 2.1 相对亮度算比值,钉死关键配色对 ≥4.5(焦点环 ≥3) | 仿既有 `app/lib/no-gradient.test.ts` 的源码级守卫范式,让「不许再引入低对比配色」成为可执行约束而非口头纪律。放弃逐组件视觉回归:成本高且测不到令牌层。 |
|
||||
| D-D | skip-link | `App.vue` 根 div 首子元素插 `<a href="#main" class="skip-link …">跳到主内容</a>`;`<main id="main" tabindex="-1">`;样式走 Tailwind `sr-only` + `focus:not-sr-only` 组合,**不新增 CSS 规则** | 原生锚点跳转会同时移动焦点(href 指向带 tabindex=-1 的 main),无需 JS。`focus:z-[80]` 压过 toast 的 `z-[70]`。放弃自绘 `.skip-link` CSS:工具类已足够,少一处需维护的样式。 |
|
||||
| D-E | 路由后焦点管理 | `attachTitleHook` 的 `afterEach` 改为 `(to, from)`,当 `to.path !== from.path` 时对 `#main` 调 `focus({ preventScroll: true })`;`#main` 不存在时静默 no-op | 只在**路径**变化时移焦:`/games → /games?tag=数字`(P10 T3 的标签点击)属同页筛选,抢焦点会打断用户;路径变化才是真正的「换页」。`preventScroll` 因为 `scrollBehavior` 已负责滚动,双重滚动会抖。放弃聚焦各页 h1:17 个视图有 2 个含双 h1(GameView / OutboundView 的 notFound 分支),聚焦目标不唯一,且 h1 加 tabindex 会污染 Tab 序。 |
|
||||
| D-F | 表格列头语义 | 三个管理视图全部 `<th>` 加 `scope="col"`;两个空的操作列表头改为 `<th scope="col" class="px-3 py-2"><span class="sr-only">操作</span></th>` | `scope="col"` 是最小且正确的修复。**不**改 `<td>` → `<th scope="row">`:那要把每行首格的排版结构(含 `<span>` 嵌套、font-bold 混排)重写成表头单元格,视觉回归风险大而收益有限(列头关联已解决读屏的主要痛点)。空表头补 sr-only 文本,否则操作列在读屏里是「无名第 5 列」。 |
|
||||
| D-G | 表格守卫测试 | 新增 `app/lib/tableScope.test.ts`:扫描 `app/**/*.vue`,断言每个 `<th` 都带 `scope=` | 与 D-C 同为源码级守卫,覆盖现有 5 张表与将来新增的表,比逐视图单测便宜且不随表格增删失效。放弃只测两个有单测的视图:AdminView 无测试文件,逐视图补测是 3 份重复劳动。 |
|
||||
| D-H | 五星评分可访问名 | 分组 `<span role="group" :aria-label="rated ? \`评分:${score} 星\` : '评分'">`;每颗星 `:aria-label="\`评 ${i} 星\`"`,内层字形 `<span aria-hidden="true">` | `aria-label` 覆盖字形成为可访问名,读屏念「评 3 星」而非「星」。分组标签携带当前值(已评几分),补齐「点了会怎样/现在是什么」。**不**用 `radiogroup`/`radio` + `aria-checked`:本控件支持「点当前分 = 撤评」,radio 无法表达取消选中;也**不**用逐星 `aria-pressed`——点亮是 `i <= litStars` 的连续区间,逐星 pressed 会把「4 分」播报成「1、2、3、4 星都按下」,语义反而更糊。 |
|
||||
| D-I | toast 实时区重构(收口 P10 T1(d)) | 容器去掉 `role`/`aria-live`,只留定位职责;每条 toast 内文本改为 `<p role="status" aria-live="polite" class="min-w-0 flex-1">{{ t.text }}</p>`,关闭按钮成为该 `<p>` 的**兄弟节点**(不再是实时区后代) | 实时区只包文本 → 读屏播报纯消息,不再夹带控件;按钮仍在 toast 内可正常 Tab 到。放弃「容器保留 role、按钮 aria-hidden」:那样按钮对读屏消失,鼠标用户能关、读屏用户关不掉,更糟。放弃把 toast 整体移出实时区:新消息就完全不被播报了,违背 toast 的存在意义。 |
|
||||
| D-J | e2e 覆盖 | 新增 `e2e/a11y.spec.ts` 三条:① Tab 首站是 skip-link、回车后焦点落 `#main`;② `/admin/users` 的 columnheader 可访问名齐备(含「操作」);③ 作品页 star 按钮可访问名为「评 N 星」 | skip-link 是纯键盘时序行为,happy-dom 单测测不出真实 Tab 序;管理页需登录态(`helpers.seedSession(page,'admin')`)。既有 e2e 文件一字不动,只新增。 |
|
||||
| D-K | 移动端页头 | **本批不做**(实测无溢出,见 §1) | 没有问题就不修。引入汉堡菜单会新增焦点圈闭、Esc、aria-expanded 三处需维护的状态机,纯负收益。 |
|
||||
|
||||
## §3 设计细节(实现须逐字采用)
|
||||
|
||||
### 3.1 D-A / D-B / D-C:色彩对比
|
||||
|
||||
`app/styles/main.css` 的 `@theme` 块内,唯一改动:
|
||||
|
||||
```css
|
||||
--color-success: #1f7a4d;
|
||||
```
|
||||
|
||||
`app/components/ToastHost.vue` 的 `KIND_CLASS`,唯一改动(`error` 行):
|
||||
|
||||
```ts
|
||||
const KIND_CLASS: Record<ToastKind, string> = {
|
||||
success: 'bg-success text-paper',
|
||||
error: 'bg-accent-ink text-paper',
|
||||
info: 'bg-highlight text-ink'
|
||||
}
|
||||
```
|
||||
|
||||
新增 `app/lib/contrast.test.ts`:从 `main.css` 正则提取 `--color-<name>: #rrggbb`,实现 WCAG 相对亮度与对比度,断言下列配对(`fg on bg`):
|
||||
|
||||
- ≥ 4.5:`paper on success`、`paper on accent-ink`、`ink on highlight`、`ink on accent`、`ink-soft on paper`、`ink-faint on paper`、`ink-soft on surface`、`ink-faint on surface`、`paper on ink`、`accent-ink on paper`
|
||||
- ≥ 3:`accent on paper`(焦点环,1.4.11 非文本)
|
||||
|
||||
亮度公式(逐字,别自创):
|
||||
|
||||
```ts
|
||||
function channel(c: number): number {
|
||||
const s = c / 255
|
||||
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
|
||||
}
|
||||
function luminance(hex: string): number {
|
||||
const h = hex.replace('#', '')
|
||||
const [r, g, b] = [0, 2, 4].map((i) => channel(parseInt(h.slice(i, i + 2), 16)))
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
export function contrast(a: string, b: string): number {
|
||||
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x)
|
||||
return (hi + 0.05) / (lo + 0.05)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 D-D / D-E:skip-link 与路由后焦点
|
||||
|
||||
`app/App.vue` 模板(`<AppHeader />` 之前插 skip-link,`<main>` 加 `id` 与 `tabindex`):
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div class="flex min-h-screen flex-col">
|
||||
<a
|
||||
href="#main"
|
||||
class="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-[80] focus:border-2 focus:border-ink focus:bg-highlight focus:px-3 focus:py-2 focus:text-sm focus:font-extrabold focus:shadow-hard"
|
||||
>跳到主内容</a>
|
||||
<AppHeader />
|
||||
<main id="main" tabindex="-1" class="mx-auto flex w-full max-w-6xl flex-1 flex-col px-4 py-6">
|
||||
<RouterView />
|
||||
</main>
|
||||
<AppFooter />
|
||||
<ToastHost />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
`app/router/index.ts`:新增导出函数 + 改 `attachTitleHook` 签名内的回调:
|
||||
|
||||
```ts
|
||||
// SPA 导航后把焦点交给主内容区:读屏用户据此获得新页面上下文(D-E)。
|
||||
// 仅路径变化时移动——同路径改 query(如目录 /games?tag=x 筛选)不打断用户焦点。
|
||||
// preventScroll:滚动由 router.scrollBehavior 负责,避免双重滚动抖动。
|
||||
export function focusMain(): void {
|
||||
document.getElementById('main')?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
export function attachTitleHook(r: Router): void {
|
||||
r.afterEach((to, from) => {
|
||||
setPageTitle(joinTitle(sectionTitleOf(to.name)))
|
||||
setPageDescription('')
|
||||
if (to.path !== from.path) focusMain()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
注意:`to.path !== from.path` 在首次导航时 `from` 是 `START_LOCATION`(path `/`),从 `/` 进 `/` 不会误触;从 `/` 进 `/games` 会正确触发。`#main` 尚未挂载(router 早于 app mount 就绪的边界)时 `getElementById` 返回 null,可选链静默 no-op。
|
||||
|
||||
### 3.3 D-F / D-G:表格列头
|
||||
|
||||
三个文件的每个 `<th>` 加 `scope="col"`,类名与文本一字不动。两处空表头(`AdminView.vue:152` 队列表的 `<th class="px-3 py-2" />`、`AdminUsersView.vue:123` 的 `<th class="px-3 py-2" />`)改为:
|
||||
|
||||
```html
|
||||
<th scope="col" class="px-3 py-2"><span class="sr-only">操作</span></th>
|
||||
```
|
||||
|
||||
新增 `app/lib/tableScope.test.ts`:递归扫 `app/**/*.vue`(排除 `*.test.ts`),对每行匹配 `<th\b`,断言该 `<th` 起始标签内含 `scope=`。多行书写的 `<th` 需读到闭合 `>` 为止再判定(AdminUsersView 的空表头是单行,但守卫要对将来健壮)。违规时报告 `相对路径:行号: 行内容`,断言 `toEqual([])`——与 `no-gradient.test.ts` 同风格。
|
||||
|
||||
### 3.4 D-H:五星评分
|
||||
|
||||
`app/components/GameReactions.vue` 的星标分组:
|
||||
|
||||
```html
|
||||
<span
|
||||
class="inline-flex items-center"
|
||||
role="group"
|
||||
:aria-label="rated ? `评分:${score} 星` : '评分'"
|
||||
>
|
||||
<button
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
type="button"
|
||||
:data-testid="`star-${i}`"
|
||||
:aria-label="`评 ${i} 星`"
|
||||
:disabled="busy"
|
||||
class="px-0.5 font-mono text-base leading-none disabled:opacity-60"
|
||||
@click="pickStar(i)"
|
||||
><span aria-hidden="true" :class="i <= litStars ? 'text-accent-ink' : 'text-ink-soft'">{{ i <= litStars ? '★' : '☆' }}</span></button>
|
||||
</span>
|
||||
```
|
||||
|
||||
`data-testid`、`disabled`、类名、点击语义(点当前分撤评)全部不动;既有测试对 `.text()` 的断言不受影响(`aria-label` 不改文本内容)。
|
||||
|
||||
### 3.5 D-I:toast 实时区
|
||||
|
||||
`app/components/ToastHost.vue` 模板:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<!-- 不 Teleport:容器常驻,屏幕阅读器语义稳定。
|
||||
容器只负责定位——role/aria-live 落在每条 toast 的文本上,
|
||||
使可交互的关闭按钮成为实时区的兄弟而非后代(P10 T1(d) 收口)。 -->
|
||||
<div
|
||||
data-testid="toast-host"
|
||||
class="pointer-events-none fixed bottom-4 right-4 z-[70] flex w-72 flex-col gap-2"
|
||||
>
|
||||
<TransitionGroup name="toast">
|
||||
<div
|
||||
v-for="t in toasts"
|
||||
:key="t.id"
|
||||
data-testid="toast"
|
||||
:class="[
|
||||
'pointer-events-auto flex items-start justify-between gap-2 border-2 border-ink px-3 py-2 text-xs font-bold shadow-hard',
|
||||
KIND_CLASS[t.kind]
|
||||
]"
|
||||
>
|
||||
<p role="status" aria-live="polite" class="min-w-0 flex-1">{{ t.text }}</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭提示"
|
||||
class="-mr-1 shrink-0 cursor-pointer"
|
||||
@click="dismiss(t.id)"
|
||||
>
|
||||
<PhX :size="12" weight="bold" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
`useToast.ts` 一字不动(store 语义与本批无关)。
|
||||
|
||||
### 3.6 D-J:e2e
|
||||
|
||||
新增 `src/e2e/a11y.spec.ts`,三条用例,既有 e2e 文件与 `helpers.ts` 一字不动:
|
||||
|
||||
```ts
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { seedSession } from './helpers'
|
||||
|
||||
test('skip-link:Tab 首站可见,回车后焦点落在主内容区', async ({ page }) => {
|
||||
await page.goto('http://localhost:4173/')
|
||||
await page.keyboard.press('Tab')
|
||||
const skip = page.getByRole('link', { name: '跳到主内容' })
|
||||
await expect(skip).toBeVisible()
|
||||
await expect(skip).toBeFocused()
|
||||
await page.keyboard.press('Enter')
|
||||
await expect(page.locator('main#main')).toBeFocused()
|
||||
})
|
||||
|
||||
test('管理端表格列头具备可访问名(含 sr-only 的操作列)', async ({ page }) => {
|
||||
await seedSession(page, 'admin')
|
||||
await page.goto('http://localhost:4173/admin/users')
|
||||
const headers = page.getByRole('columnheader')
|
||||
await expect(headers).toHaveCount(6)
|
||||
await expect(headers.nth(0)).toHaveAccessibleName('用户名')
|
||||
await expect(headers.nth(5)).toHaveAccessibleName('操作')
|
||||
})
|
||||
|
||||
test('评分按钮的可访问名是「评 N 星」而非字形', async ({ page }) => {
|
||||
await seedSession(page, 'user')
|
||||
await page.goto('http://localhost:4173/games/fixture/2048')
|
||||
await expect(page.getByTestId('star-3')).toHaveAccessibleName('评 3 星')
|
||||
await expect(page.locator('[role=group][aria-label^="评分"]')).toHaveCount(1)
|
||||
})
|
||||
```
|
||||
|
||||
## §4 边界与不做的事
|
||||
|
||||
- 禁触:`e2e/landing.spec.ts`、`e2e/noauth.spec.ts`、`e2e/author-page.noauth.spec.ts`、`e2e/merge-repo.spec.ts`、`e2e/ux.spec.ts`、`e2e/submit-flow.spec.ts`、`e2e/helpers.ts`、`playwright.config.ts`、`vitest.config.ts`、`vite.config*.ts`、`scripts/`、`package.json`。
|
||||
- `runtime/` 本批**完全不动**(含 GameHost)——播放区可访问性(iframe title、全屏按钮语义)已在既有代码达标。
|
||||
- 不新增依赖、不改 `version`(恒 0.1.0)、不动 CHANGELOG(控制者统一写)。
|
||||
- 不改任何视觉设计:平面海报新粗野主义(无渐变/无圆角/硬阴影)必须保持,`no-gradient.test.ts` 守卫须继续绿。除 D-A/D-B 两处配色外,不动任何既有颜色与排版。
|
||||
- 不做移动端汉堡菜单(D-K,实测无溢出)。
|
||||
- 不做 `<td>` → `<th scope="row">` 的行头重构(D-F 理由)。
|
||||
- 不做暗色模式(C 包,另行立项)。
|
||||
|
||||
## §5 测试计划
|
||||
|
||||
vitest(新增/扩展):
|
||||
|
||||
- 新 `app/lib/contrast.test.ts`:令牌解析成功(能取到 paper/ink/success/accent-ink/highlight/accent/ink-soft/ink-faint/surface);§3.1 列出的 ≥4.5 配对逐条断言;焦点环 `accent on paper` ≥3。
|
||||
- 新 `app/lib/tableScope.test.ts`:守卫断言 `violations === []`(即全仓每个 `<th` 都带 scope)。
|
||||
- 扩 `app/components/ToastHost.test.ts`:① 容器**不再**有 `role`/`aria-live`(改为断言属性不存在);② 每条 toast 的 `p[role=status][aria-live=polite]` 存在且文本正确;③ 关闭按钮是该 `<p>` 的兄弟(`button.parentElement === p.parentElement`,且 `p.contains(button) === false`);④ kind 配色断言中 `error` 由 `bg-accent` 改 `bg-accent-ink`;⑤ 既有的「点关闭 → store 少一条」保留。
|
||||
- 扩 `app/router/index.test.ts`:新 describe「SPA 导航后焦点(D-E)」——先在 `document.body` 注入 `<main id="main" tabindex="-1">`,push `/` → `/games` 后断言 `document.activeElement.id === 'main'`;`/games` → `/games?tag=x` 后断言焦点**未**被 main 抢走(先 blur 或先聚焦别的元素再验);`#main` 不存在时 push 不抛错。
|
||||
- 扩 `app/components/GameReactions.test.ts`:star-1..5 的 `aria-label` 为「评 N 星」;分组 `role=group` 且未评时 `aria-label === '评分'`、已评(`rated: true, score: 4`)时为「评分:4 星」;内层字形 `aria-hidden="true"`;既有 `.text()` 为 ★/☆ 的断言全部保留且仍绿。
|
||||
|
||||
e2e:
|
||||
|
||||
- 新 `e2e/a11y.spec.ts` 三条(§3.6 逐字)。
|
||||
- 既有全部 e2e 文件一字不动,且必须继续全绿(`landing.spec.ts` 的 `getByRole('heading', { level: 1 })`、`locator('main')`、`ux.spec.ts` 的 `locator('main').click({position:{x:5,y:5}})` 与 toast 断言均须不受影响——skip-link 在 main 之外、toast 的 `data-testid` 与文本内容不变)。
|
||||
|
||||
验收腿:vitest 全绿(基线 601 + 新增)、`vue-tsc` 零错、`npm run build` OK、主 e2e(基线 77+1skip + 新增 3)、noauth 4。
|
||||
|
||||
## §6 CHANGELOG 口径(控制者写)
|
||||
|
||||
版本 `0.24.0`。Added:skip-link、路由后焦点管理、表格列头语义、评分可访问名、对比度/表格守卫测试、a11y e2e。Changed:success 令牌加深、错误 toast 底色换 accent-ink、toast 实时区重构(交互控件移出)。Tests:数字以实跑为准。中英双语,同条目英中相邻行、条目间空行。
|
||||
Reference in New Issue
Block a user