正确隐藏键盘和屏幕上的内容 reader
Properly hiding content from both keyboard and screen reader
我们在页面上有一个模式,当隐藏时,我们希望不让键盘用户能够跳入内容,也不想让屏幕阅读器阅读。
为了处理这个问题,我在父级 DIV 上进行了设置,以便在隐藏时具有以下内容:
<div aria-hidden="true" tabindex="-1">
[child HTML/content]
<div>
不幸的是,这不起作用。您仍然可以进入内容并阅读内容(至少通过 Chrome 并使用 VoiceOver)。
理想情况下,我们还可以设置 display: none
-- 我可以做到这一点 -- 但目前我们依赖于一些 CSS 过渡动画,因此需要设置在动画之后以编程方式。
然而,在走这条路之前,我最初的理解是 aria-hidden 和 tabindex 应该解决这个问题,我是否遗漏了什么?
简答
使用没有转换的 display:none
将是最好的选择,并且不需要 aria-hidden
。
如果需要转场,那就转场,然后在转场后设置 display: none
属性。
注意失去焦点,但如果转换超过 100 毫秒,则必须进行大量焦点管理以解决设置延迟 display:none
。
更长的答案
aria-hidden="true"
从辅助功能树中删除一个项目及其子项。但是,它不会阻止可以接收焦点(即 <input>
)的子项接收焦点。
tabindex="-1"
不会从已经可聚焦的子元素中移除焦点。
解决所有问题的最简单方法是删除过渡并简单地切换显示 属性。这不仅解决了您的焦点问题,而且还消除了 aria-hidden
的需要,使事情变得更简单。
话虽如此,转换可能是您规范的一部分并且不可避免。如果是这种情况,则需要考虑一些事项。
在我们的评论讨论和您的问题中,您提到在转换完成后使用 setTimeout
将显示 属性 设置为 none。
根据您的设计,此方法存在问题。
如果下一个制表位在被隐藏的区域内,则在转换期间有人可能导航到即将被隐藏的区域内的元素是可行的。
如果发生这种情况,页面上的焦点将会丢失。根据浏览器的不同,这可能会导致焦点返回到页面顶部。这是非常令人沮丧的事情,也可能构成 WCAG 原则中逻辑选项卡顺序/稳健性的失败。
实现隐藏动画的最佳方法是什么?
由于焦点问题,我建议使用以下过程来隐藏带有转换的内容:-
- 第二次激活导致区域隐藏的按钮/代码(在 fade-out 之前)在要隐藏的
<div>
内的所有交互元素上设置 tabindex="-1"
(或者如果它们是输入,则设置 disabled
属性)。
- 通过您使用的任何方式开始转换(即将 class 添加到将触发转换的项目)。
- 转换完成后,在项目上设置
display: none
。
- 如果您希望
<div>
再次可见,请执行完全相反的操作。
这样做可以确保没有人会不小心切换到 div 并失去焦点。这有助于依赖键盘进行导航的每个人,而不仅仅是屏幕 reader 用户。
下面是一个关于如何实现这一点的非常粗略的示例。它可以根据容器的 ID 重复使用,因此希望能为您提供一个良好的起点来编写更健壮(并且不那么丑陋!呵呵)的东西
我已添加评论以尽我所能解释。我已将过渡设置为 2 秒,以便您可以检查并查看事物的顺序。
最后,我添加了一些 CSS 和 JS 来解释那些表示由于对运动敏感而更喜欢减少运动的人。在这种情况下,动画时间设置为 0。
粗略示例说明隐藏项目以管理 tabindex 并在再次可见时恢复 tabindex。
var content = document.getElementById('contentDiv');
var btn = document.getElementById('btn_toggle');
var animationDelay = 2000;
//We should account for people with vestibular motion disorders etc. if they have indicated they prefer reduced motion. We set the animation time to 0 seconds.
var motionQuery = matchMedia('(prefers-reduced-motion)');
function handleReduceMotionChanged() {
if (motionQuery.matches) {
animationDelay = 0;
} else {
animationDelay = 2000;
}
}
motionQuery.addListener(handleReduceMotionChanged);
handleReduceMotionChanged();
//the main function for setting the tabindex to -1 for all children of a parent with given ID (and reversing the process)
function hideOrShowAllInteractiveItems(parentDivID){
//a list of selectors for all focusable elements.
var focusableItems = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[tabindex]:not([disabled])', '[contenteditable=true]:not([disabled])'];
//build a query string that targets the parent div ID and all children elements that are in our focusable items list.
var queryString = "";
for (i = 0, leni = focusableItems.length; i < leni; i++) {
queryString += "#" + parentDivID + " " + focusableItems[i] + ", ";
}
queryString = queryString.replace(/,\s*$/, "");
var focusableElements = document.querySelectorAll(queryString);
for (j = 0, lenj = focusableElements.length; j < lenj; j++) {
var el = focusableElements[j];
if(!el.hasAttribute('data-modified')){ // we use the 'data-modified' attribute to track all items that we have applied a tabindex to (as we can't use tabindex itself).
// we haven't modified this element so we grab the tabindex if it has one and store it for use later when we want to restore.
if(el.hasAttribute('tabindex')){
el.setAttribute('data-oldTabIndex', el.getAttribute('tabindex'));
}
el.setAttribute('data-modified', true);
el.setAttribute('tabindex', '-1'); // add `tabindex="-1"` to all items to remove them from the focus order.
}else{
//we have modified this item so we want to revert it back to the original state it was in.
el.removeAttribute('tabindex');
if(el.hasAttribute('data-oldtabindex')){
el.setAttribute('tabindex', el.getAttribute('data-oldtabindex'));
el.removeAttribute('data-oldtabindex');
}
el.removeAttribute('data-modified');
}
}
}
btn.addEventListener('click', function(){
contentDiv.className = contentDiv.className !== 'show' ? 'show' : 'hide';
if (contentDiv.className === 'show') {
content.setAttribute('aria-hidden', false);
setTimeout(function(){
contentDiv.style.display = 'block';
hideOrShowAllInteractiveItems('contentDiv');
},0);
}
if (contentDiv.className === 'hide') {
content.setAttribute('aria-hidden', true);
hideOrShowAllInteractiveItems('contentDiv');
setTimeout(function(){
contentDiv.style.display = 'none';
},animationDelay); //using the animation delay set based on the users preferences.
}
});
@keyframes in {
0% { transform: scale(0); opacity: 0; visibility: hidden; }
100% { transform: scale(1); opacity: 1; visibility: visible; }
}
@keyframes out {
0% { transform: scale(1); opacity: 1; visibility: visible; }
100% { transform: scale(0); opacity: 0; visibility: hidden; }
}
#contentDiv {
background: grey;
color: white;
padding: 16px;
margin-bottom: 10px;
}
#contentDiv.show {
animation: in 2s ease both;
}
#contentDiv.hide {
animation: out 2s ease both;
}
/*****We should account for people with vestibular motion disorders etc. if they have indicated they prefer reduced motion. ***/
@media (prefers-reduced-motion) {
#contentDiv.show,
#contentDiv.hide{
animation: none;
}
}
<div id="contentDiv" class="show">
<p>Some information to be hidden</p>
<input />
<button>a button</button>
<button tabindex="1">a button with a positive tabindex that needs restoring</button>
</div>
<button id="btn_toggle"> Hide Div </button>
如果无法实施 Graham Ritchie 之前提供的解决方案,您需要确保:
- 模态的所有 个可聚焦子项在模态隐藏时接收
tabindex="-1"
和
aria-hidden
已从父项中删除(设置为 false
)并且所有这些可聚焦的子项都已 tabindex
删除。
注意: 避免为 tabindex
使用正值(即 tabindex="1"
),因为它会扰乱页面的焦点顺序(通常遵循 DOM 的顺序并且应该遵循页面的阅读顺序)。最好的是只使用 tabindex="0"
以自然焦点顺序添加元素,并使用 tabindex="-1"
将其从焦点顺序中删除(但仍然可以使用 JavaScript .focus()
方法,如果需要的话)。
我们在页面上有一个模式,当隐藏时,我们希望不让键盘用户能够跳入内容,也不想让屏幕阅读器阅读。
为了处理这个问题,我在父级 DIV 上进行了设置,以便在隐藏时具有以下内容:
<div aria-hidden="true" tabindex="-1">
[child HTML/content]
<div>
不幸的是,这不起作用。您仍然可以进入内容并阅读内容(至少通过 Chrome 并使用 VoiceOver)。
理想情况下,我们还可以设置 display: none
-- 我可以做到这一点 -- 但目前我们依赖于一些 CSS 过渡动画,因此需要设置在动画之后以编程方式。
然而,在走这条路之前,我最初的理解是 aria-hidden 和 tabindex 应该解决这个问题,我是否遗漏了什么?
简答
使用没有转换的 display:none
将是最好的选择,并且不需要 aria-hidden
。
如果需要转场,那就转场,然后在转场后设置 display: none
属性。
注意失去焦点,但如果转换超过 100 毫秒,则必须进行大量焦点管理以解决设置延迟 display:none
。
更长的答案
aria-hidden="true"
从辅助功能树中删除一个项目及其子项。但是,它不会阻止可以接收焦点(即 <input>
)的子项接收焦点。
tabindex="-1"
不会从已经可聚焦的子元素中移除焦点。
解决所有问题的最简单方法是删除过渡并简单地切换显示 属性。这不仅解决了您的焦点问题,而且还消除了 aria-hidden
的需要,使事情变得更简单。
话虽如此,转换可能是您规范的一部分并且不可避免。如果是这种情况,则需要考虑一些事项。
在我们的评论讨论和您的问题中,您提到在转换完成后使用 setTimeout
将显示 属性 设置为 none。
根据您的设计,此方法存在问题。
如果下一个制表位在被隐藏的区域内,则在转换期间有人可能导航到即将被隐藏的区域内的元素是可行的。
如果发生这种情况,页面上的焦点将会丢失。根据浏览器的不同,这可能会导致焦点返回到页面顶部。这是非常令人沮丧的事情,也可能构成 WCAG 原则中逻辑选项卡顺序/稳健性的失败。
实现隐藏动画的最佳方法是什么?
由于焦点问题,我建议使用以下过程来隐藏带有转换的内容:-
- 第二次激活导致区域隐藏的按钮/代码(在 fade-out 之前)在要隐藏的
<div>
内的所有交互元素上设置tabindex="-1"
(或者如果它们是输入,则设置disabled
属性)。 - 通过您使用的任何方式开始转换(即将 class 添加到将触发转换的项目)。
- 转换完成后,在项目上设置
display: none
。 - 如果您希望
<div>
再次可见,请执行完全相反的操作。
这样做可以确保没有人会不小心切换到 div 并失去焦点。这有助于依赖键盘进行导航的每个人,而不仅仅是屏幕 reader 用户。
下面是一个关于如何实现这一点的非常粗略的示例。它可以根据容器的 ID 重复使用,因此希望能为您提供一个良好的起点来编写更健壮(并且不那么丑陋!呵呵)的东西
我已添加评论以尽我所能解释。我已将过渡设置为 2 秒,以便您可以检查并查看事物的顺序。
最后,我添加了一些 CSS 和 JS 来解释那些表示由于对运动敏感而更喜欢减少运动的人。在这种情况下,动画时间设置为 0。
粗略示例说明隐藏项目以管理 tabindex 并在再次可见时恢复 tabindex。
var content = document.getElementById('contentDiv');
var btn = document.getElementById('btn_toggle');
var animationDelay = 2000;
//We should account for people with vestibular motion disorders etc. if they have indicated they prefer reduced motion. We set the animation time to 0 seconds.
var motionQuery = matchMedia('(prefers-reduced-motion)');
function handleReduceMotionChanged() {
if (motionQuery.matches) {
animationDelay = 0;
} else {
animationDelay = 2000;
}
}
motionQuery.addListener(handleReduceMotionChanged);
handleReduceMotionChanged();
//the main function for setting the tabindex to -1 for all children of a parent with given ID (and reversing the process)
function hideOrShowAllInteractiveItems(parentDivID){
//a list of selectors for all focusable elements.
var focusableItems = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[tabindex]:not([disabled])', '[contenteditable=true]:not([disabled])'];
//build a query string that targets the parent div ID and all children elements that are in our focusable items list.
var queryString = "";
for (i = 0, leni = focusableItems.length; i < leni; i++) {
queryString += "#" + parentDivID + " " + focusableItems[i] + ", ";
}
queryString = queryString.replace(/,\s*$/, "");
var focusableElements = document.querySelectorAll(queryString);
for (j = 0, lenj = focusableElements.length; j < lenj; j++) {
var el = focusableElements[j];
if(!el.hasAttribute('data-modified')){ // we use the 'data-modified' attribute to track all items that we have applied a tabindex to (as we can't use tabindex itself).
// we haven't modified this element so we grab the tabindex if it has one and store it for use later when we want to restore.
if(el.hasAttribute('tabindex')){
el.setAttribute('data-oldTabIndex', el.getAttribute('tabindex'));
}
el.setAttribute('data-modified', true);
el.setAttribute('tabindex', '-1'); // add `tabindex="-1"` to all items to remove them from the focus order.
}else{
//we have modified this item so we want to revert it back to the original state it was in.
el.removeAttribute('tabindex');
if(el.hasAttribute('data-oldtabindex')){
el.setAttribute('tabindex', el.getAttribute('data-oldtabindex'));
el.removeAttribute('data-oldtabindex');
}
el.removeAttribute('data-modified');
}
}
}
btn.addEventListener('click', function(){
contentDiv.className = contentDiv.className !== 'show' ? 'show' : 'hide';
if (contentDiv.className === 'show') {
content.setAttribute('aria-hidden', false);
setTimeout(function(){
contentDiv.style.display = 'block';
hideOrShowAllInteractiveItems('contentDiv');
},0);
}
if (contentDiv.className === 'hide') {
content.setAttribute('aria-hidden', true);
hideOrShowAllInteractiveItems('contentDiv');
setTimeout(function(){
contentDiv.style.display = 'none';
},animationDelay); //using the animation delay set based on the users preferences.
}
});
@keyframes in {
0% { transform: scale(0); opacity: 0; visibility: hidden; }
100% { transform: scale(1); opacity: 1; visibility: visible; }
}
@keyframes out {
0% { transform: scale(1); opacity: 1; visibility: visible; }
100% { transform: scale(0); opacity: 0; visibility: hidden; }
}
#contentDiv {
background: grey;
color: white;
padding: 16px;
margin-bottom: 10px;
}
#contentDiv.show {
animation: in 2s ease both;
}
#contentDiv.hide {
animation: out 2s ease both;
}
/*****We should account for people with vestibular motion disorders etc. if they have indicated they prefer reduced motion. ***/
@media (prefers-reduced-motion) {
#contentDiv.show,
#contentDiv.hide{
animation: none;
}
}
<div id="contentDiv" class="show">
<p>Some information to be hidden</p>
<input />
<button>a button</button>
<button tabindex="1">a button with a positive tabindex that needs restoring</button>
</div>
<button id="btn_toggle"> Hide Div </button>
如果无法实施 Graham Ritchie 之前提供的解决方案,您需要确保:
- 模态的所有 个可聚焦子项在模态隐藏时接收
tabindex="-1"
和 aria-hidden
已从父项中删除(设置为false
)并且所有这些可聚焦的子项都已tabindex
删除。
注意: 避免为 tabindex
使用正值(即 tabindex="1"
),因为它会扰乱页面的焦点顺序(通常遵循 DOM 的顺序并且应该遵循页面的阅读顺序)。最好的是只使用 tabindex="0"
以自然焦点顺序添加元素,并使用 tabindex="-1"
将其从焦点顺序中删除(但仍然可以使用 JavaScript .focus()
方法,如果需要的话)。