指南:内置路由可达性与焦点管理)
Redwood 无障碍a11y指南内置路由可达性与焦点管理【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwoodRedwood 框架把无障碍Accessibility简称 a11y作为开箱即用的核心特性你不需要手写整套辅助技术支持逻辑——路由切换时的页面播报、焦点重置与跳转、跳过导航链接等能力都已内置在redwoodjs/router中。本文基于 version-7.x 的官方 a11y 文档结合 router 包源码 与单元测试完整讲解路由播报RouteAnnouncement、焦点管理RouteFocus / Skip Links的设计原理与实战用法读完即可在你的 Redwood 应用中落地键盘与屏幕阅读器友好的页面导航。为什么无障碍要从路由开始对于单页应用SPA而言无障碍的起点是路由器。因为页面切换不经过整页刷新屏幕阅读器用户无法像传统多页网站那样自然感知我到了一个新页面——如果不做任何处理导航发生时没有任何播报。这不仅是小瑕疵而是功能性缺陷用户会迷失在内容流中不知道当前身处何处。传统做法要求开发者自己为屏幕阅读器用户播报已导航到新页面。这既繁琐又容易出错。Redwood 的解法是只要你写出语义化、有内容的页面结构路由器会自动完成播报。其内置的路由播报器在每次导航时按以下优先级寻找播报文本RouteAnnouncement组件最具体、最优先页面的h1标题document.titlelocation.pathname这一顺序的设计理由是播报内容应尽量具体、有描述性这样用户不仅能定位自己、顺畅导航还能在之后重新找到这个页面。提示如果不确定自己的页面描述是否足够清晰可以参考 W3C 关于提供描述性页面标题的 WCAG 2.1 通用技巧 G88G88: Providing descriptive titles for Web pages。注意即使 Redwood 优先查找RouteAnnouncement你也不需要在每个页面都放置它——绝大多数情况下让h1作为播报内容完全够用。RouteAnnouncement是为需要自定义播报文案的场景准备的。RouteAnnouncement自定义路由播报RouteAnnouncement的工作原理非常简单它的子内容会被播报。它既可以包住页面上可见的内容这样播报与视觉保持一致也可以配合visuallyHidden属性提供一段仅对屏幕阅读器可见的隐藏文案。播报可见内容import { RouteAnnouncement } from redwoodjs/router const HomePage () { return ( // 这段内容仍然可见 RouteAnnouncement h1Welcome to my site!/h1 /RouteAnnouncement ) } export default HomePage播报视觉隐藏内容import { RouteAnnouncement } from redwoodjs/router const AboutPage () { return ( h1Welcome to my site!/h1 {/* 这段内容不可见但会被播报 */} RouteAnnouncement visuallyHidden All about me /RouteAnnouncement / ) } export default AboutPage官方文档特别提醒visuallyHidden不应是你最先考虑的手段——保持站点视觉体验与听觉体验的一致性很重要。但当你确实需要例如播报一段与页面可见标题不同、更能说明页面用途的文案时它随时可用。源码视角RouteAnnouncement的实现从源码看route-announcement.tsx 的实现非常轻量它渲染一个带有data-redwood-route-announcement标记的div。当visuallyHidden为 true 时应用一套标准的视觉隐藏样式绝对定位、1px 尺寸、clip: rect(0, 0, 0, 0)、overflow: hidden等把内容移出视觉渲染但保留在无障碍树中const hiddenStyle: React.CSSProperties { position: absolute, top: 0, width: 1, height: 1, padding: 0, overflow: hidden, clip: rect(0, 0, 0, 0), whiteSpace: nowrap, border: 0, }这个实现最初借鉴了 Gatsby 社区 madalyn 的成果源码注释中保留了出处链接是经过社区验证的标准做法。播报优先级如何在源码中体现getAnnouncement完整实现了文档描述的优先级链先查[data-redwood-route-announcement]节点的textContent没有则查页面第一个h1再退到document.title最后兜底返回new page at ${location.pathname}export const getAnnouncement () { const routeAnnouncement globalThis?.document.querySelectorAll( [data-redwood-route-announcement], )?.[0] if (routeAnnouncement?.textContent) { return routeAnnouncement.textContent } const pageHeading globalThis?.document.querySelector(h1) if (pageHeading?.textContent) { return pageHeading.textContent } if (globalThis?.document.title) { return document.title } return new page at ${globalThis?.location.pathname} }两个实现细节值得注意一是用querySelectorAll(...)?.[0]取第一个匹配节点说明页面中若放置多个RouteAnnouncement只有第一个生效二是只认非空文本——空h1会被跳过继续向下寻找这与测试用例中空 PageHeader 处理getAnnouncement handles empty PageHeader的验证一致。播报器的渲染与测试验证播报文本由 active-route-loader.tsx 在路由加载时写入页面内置的 announcer 节点idredwood-announcer。在 route-announcer.test.tsx 中测试验证了播报器节点带有aria-liveassertive与rolealert属性——这是辅助技术感知页面内容变化的关键机制同时逐个导航验证了优先级链的每一级回退例如页面含RouteAnnouncement时getAnnouncement()返回其内容不含时返回h1文本无h1时返回document.title两者皆无时返回new page at /noH1OrTitle。这也印证了官方文档中的建议只要页面语义结构完整有描述性的h1你什么都不用做播报自动生效。页面切换后的焦点管理每次页面切换时Redwood Router 会把焦点重置到 DOM 顶部让用户可以从新页面的起点开始浏览。这通常是符合预期的默认行为但对某些页面——尤其是导航项很多的页面——用户每次都要按 Tab 键穿过一大段导航才能到达主要内容体验十分繁琐而且每次页面切换都会如此。官方文档给出两种缓解手段跳过链接Skip Links与RouteFocus组件。Skip Links一键跳过导航既然主内容通常不是页面上第一个元素为键盘与屏幕阅读器用户提供一个直接跳到主内容的快捷方式是最佳实践。Redwood 在生成布局时直接支持该能力——使用--skipLink选项即可yarn rw g layout main --skipLink生成出的布局自带SkipNavLink与SkipNavContent组件import { SkipNavLink, SkipNavContent } from redwoodjs/router import redwoodjs/router/skip-nav.css const MainLayout ({ children }) { return ( SkipNavLink / nav/nav SkipNavContent / main{children}/main / ) } export default MainLayoutSkipNavLink渲染一个聚焦前始终隐藏、获得焦点时才显示的链接SkipNavContent渲染一个div作为该链接的跳转目标。这套组件源自 Reach UI仓库中因 React 18 的 peer dependency 问题将其内置到 skipNav.tsx源码注释保留了原始出处。对应的样式位于 skip-nav.css默认状态下链接被clip: rect(0 0 0 0)裁剪隐藏聚焦时则以固定定位出现在左上角position: fixed; top: 10px; left: 10px这正是对所有人可见、对键盘用户可用的经典实现。自定义跳转目标你可能希望把跳转链接指向特定内容区块。通过修改SkipNavLink的contentId与SkipNavContent的id即可实现两者必须成对匹配SkipNavLink contentIdmain-content / {/* ... */} SkipNavContent idmain-content /从源码看SkipNavLink的默认锚点是#reach-skip-navdefaultId它会把contentId拼进hrefSkipNavContent默认渲染同名的id作为目标。因此只要改了一边另一边必须同步改否则跳转锚点会失效。扩展阅读如果你想实现完全自定义的跳过链接可以参阅 Ben Myers 的 skip links 博客其内容也覆盖了更广泛的无障碍实践。RouteFocus把焦点送到特定元素有时你要做的不是跳过导航而是直接把用户送到某个位置。请谨慎使用——你确信该位置正是用户想要到达的地方才行因为把用户送到意外位置比送回顶部更糟。当某个页面切换后焦点确实应该落在特定元素上时使用RouteFocusimport { RouteFocus } from redwoodjs/router const ContactPage () ( nav {/* 导航太多了... */} /nav {/* 用户真正想交互的 contact 表单 */} RouteFocus TextField namename / /RouteFocus / ) export default ContactPageRouteFocus告诉路由器页面切换时把焦点交给它的第一个子元素。上面的例子中用户导航到联系页后焦点会直接落在表单的姓名输入框上——也就是用户来这里要填写的第一个字段。源码视角getFocus与resetFocusgetFocus会查找页面上第一个带data-redwood-route-focus标记的节点并校验其子元素是否可聚焦export const getFocus () { const routeFocus globalThis?.document.querySelectorAll( [data-redwood-route-focus], )?.[0] if ( !routeFocus?.children.length || (routeFocus.children[0] as HTMLElement).tabIndex 0 ) { return null } return routeFocus.children[0] as HTMLElement }route-focus.tsx 本身只是一个打上data-redwood-route-focus标记的div真正的逻辑全在getFocus若RouteFocus没有子元素、或子元素tabIndex 0不可聚焦则返回null。对应测试 route-focus.test.tsx 覆盖了这些边界场景无 RouteFocus、无子元素、纯文本节点、子元素不可聚焦等。当getFocus返回null时active-route-loader.tsx 的useEffect会调用resetFocus()把焦点重置回顶部否则调用routeFocus.focus()聚焦到目标元素。resetFocus的实现值得一提——它通过给body临时设置tabindex-1再聚焦从而在不打断 Tab 焦点流的前提下把焦点移回页面顶部源码注释说明直接调用document.activeElement.blur()无法重置焦点流export const resetFocus () { globalThis?.document.body.setAttribute(tabindex, -1) globalThis?.document.body.focus() globalThis?.document.body.removeAttribute(tabindex) }另外active-route-loader.tsx 中还包含一个细节若页面渲染在 iframe 中inIframe()为真上述焦点与播报逻辑会跳过避免干扰嵌入场景。总结Redwood 将无障碍视为从第一天就内置的核心能力而非锦上添花。围绕路由框架提供了完整的三层机制能力组件 / 机制作用页面播报RouteAnnouncement可选→h1→document.title→location.pathname屏幕阅读器用户感知页面切换跳过导航SkipNavLink/SkipNavContentyarn rw g layout main --skipLink键盘用户直达主内容焦点定向RouteFocus否则重置到顶部页面切换后焦点落在关键元素源码层面这些能力集中在 packages/router/src 的a11yUtils.ts、route-announcement.tsx、route-focus.tsx、skipNav.tsx与active-route-loader.tsx中并有route-announcer.test.tsx、route-focus.test.tsx等测试兜底。工具如自动化无障碍扫描并不能替代人工测试——但 Redwood 提供的这套内置机制至少保证了路由切换播报、焦点管理、跳过链接这三件最容易被遗漏的事默认就是对的。【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwood创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考