无障碍功能如何让我更好地掌握 JavaScript - 第二部分
最初发布于www.a11ywithlindsey.com。
内容警告:本帖中有 gif。
嘿,朋友们!今天的文章是《无障碍功能如何教会我更好地使用 JavaScript》的后续。如果你读过我的文章,你会发现我最喜欢的话题之一就是 JavaScript 和无障碍功能。我会讲解 JavaScript 对于实现交互元素无障碍的重要性。
在我之前的文章中,我谈到了如何创建一个兼顾可访问性的弹出式语言菜单。实现功能性和可访问性是我第一次接触原生 JavaScript。代码确实需要改进,我们在文章中也讨论过这个问题。然而,实现菜单的可访问性开始帮助我更好地理解 JavaScript。
今天,我们将讨论我是如何将一些令人尴尬的“手风琴”标记变得易于理解的。记住,一个基本要求是,我不能以任何方式修改内容标记。这个页面是一篇 WordPress 文章,这意味着我无法进入并编辑文章,使其变成我想要的标记。
开始
所以,这就是起始标记。
我喜欢简洁的 HTML,但无法修改标记让我很恼火。这个标记简直乱七八糟。首先,它以一个无序列表开始,虽然不算最糟糕,但也算不上理想。然后在列表项中,它包含一个用于面板标题的 span,一个 h3,另一个无序列表元素,最后是一个单独的列表项(也就是说它根本就不是一个列表?)。
我非常讨厌这种标记。
现在我已经讲完了,让我们来谈谈这里的几个目标:
- 加载页面时隐藏面板
- 单击即可打开或关闭手风琴面板。
- 使用空格键或回车键可以打开和关闭手风琴面板。
- 使该跨度可聚焦
我添加了一点 SCSS 来清理标记。我还在 CodePen 设置中添加了 normalize.css。
现在让我们来谈谈我四年前是如何解决这个问题的。
我如何处理这个问题
声明一下,这是 Lindsey 4 年前做的。只有一件事我不会做;不过,即便如此,我还是会在这段代码中添加更多内容,我会在下一节中讲到。
首先,让我们获取一些变量:
const accordion = document.getElementById('accordion')
然后,我们来创建一个条件语句。如果这个 accordion 存在,我们就获取一些其他变量。
if (accordion) {
const headers = document.querySelectorAll('.accordion__header')
const panels = document.querySelectorAll('.accordion__panel')
}
我添加了条件语句,因为我们要循环遍历该节点列表。我不想在null
现在让我们添加事件监听器
if (accordion) {
const headers = document.querySelectorAll('.accordion__header')
headers.forEach(header => header.addEventListener('click', toggleAccordion))
const panels = document.querySelectorAll('.accordion__panel')
}
然后,让我们添加这个函数,其中.accordion__header
代表this
,.nextElementSibling
是.accordion__panel
function toggleAccordion() {
this.nextElementSibling.classList.toggle('visually-hidden')
}
如果我们转到元素检查器并单击手风琴项目,我们会注意到类切换。
然后让我们visually-hidden
在 SCSS 中添加类(来源:A11y 项目):
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
}
现在让我们将visually-hidden
类添加到面板中,以便进行视觉切换。
if (accordion) {
const headers = document.querySelectorAll('.accordion__header')
headers.forEach(header => header.addEventListener('click', toggleAccordion))
const panels = document.querySelectorAll('.accordion__panel')
panels.forEach(panel => panel.classList.add('visually-hidden'))
}
如果您不考虑可访问性,您可能只添加一个点击事件就完事了。由于这些不是按钮,所以我们必须添加按键事件。我们需要复制按钮的功能。这就是为什么使用语义化 HTML 是提高可访问性的最佳方法。
首先,我们必须为每个标题添加一个 tabindex 0。
if (accordion) {
const headers = document.querySelectorAll('.accordion__header')
headers.forEach(header => {
header.tabIndex = 0
header.addEventListener('click', toggleAccordion)
})
const panels = document.querySelectorAll('.accordion__panel')
panels.forEach(panel => panel.classList.add('visually-hidden'))
}
当我们这样做时,只要按下键,我们就可以看到焦点样式tab
。
如果我们按下回车键或空格键,什么也不会发生。这是因为这个button
元素没有内置点击键盘事件。这就是为什么我时不时地会强调使用语义化 HTML。
我们必须keypress
在标题元素上添加一个事件。
headers.forEach(header => {
header.tabIndex = 0
header.addEventListener('click', toggleAccordion)
header.addEventListener('keypress', toggleAccordion)
})
k
这“起作用了”,但并不完全符合我们的预期。因为我们没有区分要切换哪个键,所以按下哪个键或空格键都无所谓。
首先,让我们将事件传递给toggleAccordion
函数,console.log()
然后
function toggleAccordion(e) {
console.log(e)
this.nextElementSibling.classList.toggle('visually-hidden')
}
插播一下。虽然我更喜欢用按钮来实现,但学习这种错误的方式让我学到了很多 JavaScript 知识。我学到了事件处理程序和事件对象。作为一个 JavaScript 新手,我从探索中学到了很多,即使这不是编写代码的最佳方式。
回到事件的话题。当我们在控制台中打开它时,我们会看到该事件的一系列属性。
我看到了一些可以使用的东西,特别是code
or key
。我将使用key
属性,因为当我按下空格键时,它会更加冗长。
所以我可以这样做,对吗?
function toggleAccordion(e) {
if (e.code === 'Enter' || e.code === 'Space') {
this.nextElementSibling.classList.toggle('visually-hidden')
}
}
嗯,不行。因为这没有考虑到click
事件本身。点击事件没有这个code
属性。它们有哪些属性可以用来处理这个点击事件呢?让我们把这个console.log(e)
属性添加到函数中,看看有什么可用的。
所以现在,我检查是否type
是点击或者code
是一个空格或输入。
为了使其更易于阅读,我将把 拆分code
成一个返回 true 或 false 的三元运算符。我最初做这件事时并没有这样做,但我想在条件语句中增加一点可读性。
function toggleAccordion(e) {
const pressButtonKeyCode =
e.code === 'Enter' || e.code === 'Space' ? true : false
if (e.type === 'click' || pressButtonKeyCode) {
this.nextElementSibling.classList.toggle('visually-hidden')
}
}
现在我们可以单击并用空格键和回车键打开。
我还有很多地方需要改进,我们接下来会逐一讲解。如果你想查看代码,可以看看下面的 CodePen:
我现在想改变什么
虽然从技术上来说这可行,但并非最理想的方案。我学习 JavaScript 的时候根本不知道什么是渐进增强,也不知道什么是 ARIA。
那么,让我们开始一步步来吧。如果你读过第一部分,你就会知道我非常喜欢用一个no-js
类来检测 JavaScript 是否已加载。
<ul id="accordion" class="accordion no-js">
<!-- Children elements -->
</ul>
然后,当我们的 JavaScript 加载时,我们做的第一件事就是删除该类。
const accordion = document.getElementById('accordion')
accordion.classList.remove('no-js')
如果该类存在,我们将添加一些默认样式no-js
,这意味着 JavaScript 不会加载:
.accordion {
&.no-js {
.accordion__header {
display: none;
}
.accordion__item {
border-top: 0;
border-bottom: 0;
&:first-child {
border-top: 1px solid;
}
&:last-child {
border-bottom: 1px solid;
}
}
.accordion__panel {
display: block;
border-top: 0;
}
}
}
我已经删除了从技术上来说不是按钮的按钮,并且默认打开所有内容。
现在,回到 JavaScript。在标题上,我们要将aria-expanded
属性设置为 false,并赋予其按钮角色。
headers.forEach(header => {
header.tabIndex = 0
header.setAttribute('role', 'button')
header.setAttribute('aria-expanded', false)
header.addEventListener('click', toggleAccordion)
header.addEventListener('keypress', toggleAccordion)
})
当我们设置角色时,我将把面板的角色设置为region
if (accordion) {
// header code
panels.forEach(panel => {
panel.setAttribute('role', 'region')
}
}
接下来,我将切换 aria-expanded 并删除函数中类的切换。需要注意的是,即使我们将属性设置为布尔值,它getAttribute()
返回的仍然是字符串。
function toggleAccordion(e) {
const pressButtonKeyCode =
e.code === 'Enter' || e.code === 'Space' ? true : false
const ariaExpanded = this.getAttribute('aria-expanded')
if (e.type === 'click' || pressButtonKeyCode) {
if (ariaExpanded === 'false') {
this.setAttribute('aria-expanded', true)
} else {
this.setAttribute('aria-expanded', false)
}
}
}
我们不需要在视觉上隐藏内容,因为我们有一个控制信息的按钮。阅读不想要的信息对屏幕阅读器用户来说不是一个好的体验。我喜欢在 CSS 中使用和来aria-expanded
切换面板。display: none
display: block
.accordion {
&__header {
// more scss
&[aria-expanded='true'] + .accordion__panel {
display: block;
}
}
&__panel {
display: none;
padding: 1rem;
border-top: 1px solid;
h3 {
margin-top: 0;
}
}
}
我将添加一些 ARIA 属性来帮助将标题和面板关联在一起。
- aria-controls - 这可能会让一些人感到困惑。我强烈推荐阅读Léonie 的文章
- aria-labelledby
我以WAI-ARIA 创作实践为基础。
首先,标题:
headers.forEach(header => {
header.tabIndex = 0
header.setAttribute('role', 'button')
// This will match the aria-labelledby on the panel
header.setAttribute('id', `accordion-header-${i + 1}`)
header.setAttribute('aria-expanded', false)
// This will match the id on the panel
header.setAttribute('aria-controls', `accordion-section-${i + 1}`)
header.addEventListener('click', toggleAccordion)
header.addEventListener('keypress', toggleAccordion)
})
然后我们会把它们拿来,确保它们与面板准确匹配
panels.forEach(panel => {
// This will match the aria-controls on the header
panel.setAttribute('id', `accordion-section-${i+1}`)
panel.setAttribute('role', 'region')
// This will match the id on the header
panel.setAttribute('aria-labelledby', `accordion-header-${i+1}`)
}
如果您想试用该代码,请分叉 CodePen 并进行检查。
结论
这是迄今为止最理想的标记吗?不是。这是否教会了我很多 JavaScript 知识?是的。这是否教会了我使用内置键盘事件的按钮的价值?是的。
保持联系!如果你喜欢这篇文章:
- 在Twitter上告诉我,并和你的朋友们分享这篇文章!另外,如果你有任何后续问题或想法,也欢迎随时在 Twitter 上给我留言。
- 在Patreon上支持我!如果您喜欢我的作品,可以考虑每月捐赠 1 美元。如果您捐赠 5 美元或更高,您将可以对以后的博客文章进行投票!我每月还会为所有赞助人举办“问我任何问题”活动!
- 参加为期 10 天的 a11y 挑战,获得更多无障碍乐趣!
干杯!祝你度过愉快的一周!
鏂囩珷鏉yu簮锛�https://dev.to/lkopacz/how-accessibility-taught-me-to-be-better-at-javascript-part-two-20af