From 43982e921a0faddd23fa2bbee2a11f5f35a9e5f1 Mon Sep 17 00:00:00 2001 From: lsdragst Date: Mon, 29 Jun 2026 21:54:54 +0800 Subject: [PATCH] improve chapter navigation accessibility --- docs/frontend-redesign.md | 7 +- web/components/ChapterNav.tsx | 2 +- web/components/ChapterRail.tsx | 3 +- .../__tests__/ChapterNavigation.test.tsx | 91 +++++++++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 web/components/__tests__/ChapterNavigation.test.tsx diff --git a/docs/frontend-redesign.md b/docs/frontend-redesign.md index 9741671..75af1ae 100644 --- a/docs/frontend-redesign.md +++ b/docs/frontend-redesign.md @@ -82,7 +82,7 @@ - [x] `/` 精简 landing,`/notebooks` 承接登录后笔记本库 - [x] `dashboard`、`library` 保留 redirect - [x] 「新建笔记本」panel 复用 `CreateNotePanel` -- [ ] 确认 `FluidBG`/`ParticleBG` 无引用后删除 +- [x] 确认 `FluidBG`/`ParticleBG` 无引用后删除 **P1 · 三栏工作台** ✅ 主体完成 - [x] `notes/[id]/page.tsx` 收敛到 `NoteWorkspace` @@ -97,8 +97,8 @@ - [x] 公开导航不再暴露 `/mm-ablation` 内部实验页入口 - [x] 移动端 `ChatPanel` 回到内容流,避免 sticky 遮挡正文 - [ ] 浏览器 smoke:视频播放、seek、分享、导出、QA 轮询、书签同步 -- [ ] a11y:三栏 landmark、章节列表 `aria-current`、Lighthouse a11y ≥ 90 -- [ ] 删除确认无引用的旧视觉组件和 `AskBar` +- [ ] a11y:三栏 landmark、章节列表 `aria-current`、Lighthouse a11y ≥ 90(章节 `aria-current` 已补齐) +- [x] 删除确认无引用的旧视觉组件和 `AskBar` - 验收:`npx tsc --noEmit` 通过;浏览器 smoke 记录写入 `docs/memory/` ## 4.1 2026-06-25 二次升级计划:电影感首页 + 品牌系统 + 沉浸式登录 @@ -141,6 +141,7 @@ - 修复移动端内容优先级:视频不能长期挤占首屏,笔记内容和提问入口要更早出现。 - 问答入口避免遮挡正文,移动端改成稳定的底部输入或内容流内卡片。 - 章节抽屉补齐焦点管理、`aria-modal` 语义和关闭后的焦点恢复。 +- 章节列表使用 `aria-current="location"` 标记当前主章节和当前子章节,帮助读屏器理解播放位置。 - 公开导航不暴露内部实验页,`/mm-ablation` 保留为直接访问或开发入口。(已完成) - 继续清理 lint、无效 eslint-disable、废弃组件引用和图片加载 warning。 diff --git a/web/components/ChapterNav.tsx b/web/components/ChapterNav.tsx index fbd7c34..6ea27d5 100644 --- a/web/components/ChapterNav.tsx +++ b/web/components/ChapterNav.tsx @@ -41,7 +41,7 @@ export default function ChapterNav({ chapters, currentIdx, currentTime, onSeek } key={i} ref={active ? activeRef : undefined} onClick={() => onSeek(ch.start)} - aria-current={active ? "true" : undefined} + aria-current={active ? "location" : undefined} whileTap={{ scale: 0.97 }} whileHover={{ y: -1 }} className={`relative shrink-0 flex min-h-11 items-center gap-2 pl-2 pr-3 py-2 rounded-2xl text-xs diff --git a/web/components/ChapterRail.tsx b/web/components/ChapterRail.tsx index 68626ef..29f43bf 100644 --- a/web/components/ChapterRail.tsx +++ b/web/components/ChapterRail.tsx @@ -59,7 +59,7 @@ export default function ChapterRail({ ; + }, + }, +})); + +const chapters: Chapter[] = [ + { + title: "Intro", + start: 0, + end: 60, + indices: [], + children: [ + { title: "Opening", start: 0, end: 20, indices: [] }, + { title: "Setup", start: 20, end: 60, indices: [] }, + ], + }, + { title: "Deep dive", start: 60, end: 120, indices: [] }, +]; + +describe("chapter navigation accessibility", () => { + afterEach(() => { + cleanup(); + }); + + it("marks the active vertical chapter and child as the current location", () => { + window.HTMLElement.prototype.scrollIntoView = vi.fn(); + + render( + {}} + /> + ); + + expect(screen.getByRole("button", { name: /00:00Intro/ })).toHaveAttribute( + "aria-current", + "location" + ); + expect(screen.getByRole("button", { name: /00:20Setup/ })).toHaveAttribute( + "aria-current", + "location" + ); + expect(screen.getByRole("button", { name: /01:00Deep dive/ })).not.toHaveAttribute( + "aria-current" + ); + }); + + it("marks the active horizontal chapter as the current location", () => { + window.HTMLElement.prototype.scrollIntoView = vi.fn(); + + render( + {}} + /> + ); + + const nav = screen.getByRole("navigation", { name: "章节" }); + expect(within(nav).getByRole("button", { name: /Deep dive/ })).toHaveAttribute( + "aria-current", + "location" + ); + expect(within(nav).getByRole("button", { name: /Intro/ })).not.toHaveAttribute( + "aria-current" + ); + }); +});