问题描述
我们在页面上有一个模式,在隐藏时,我们不想不让键盘用户能够跳到内容中,也不想让屏幕阅读器阅读。
为解决这个问题,我在父DIV上进行了设置,以便在隐藏时具有以下内容:
<div aria-hidden="true" tabindex="-1">
[child HTML/content]
<div>
不幸的是,这不起作用。您仍然可以使用Tab键浏览内容,然后读取内容(至少通过Chrome和使用VoiceOver)。
想法,我们还可以设置display: none
,我可以做到这一点,但是目前我们依赖某些CSS过渡动画,因此需要在动画之后以编程方式进行设置。 / p>
但是,在走这条路线之前,我对aria-hidden和tabindex应该解决这个问题的初衷尚不了解吗?
解决方法
如果无法实现由Graham Ritchie提早提供的解决方案,则需要确保:
-
模态
- 所有可聚焦的子代在模态被隐藏时收到
tabindex="-1"
, -
aria-hidden
均已从父级中移除(设置为false
),所有这些可聚焦子级均已移除tabindex
。
注意:避免为tabindex
(即tabindex="1"
)使用正值,因为它会干扰页面的焦点顺序(通常遵循页面的焦点顺序) DOM,并且应遵循页面的阅读顺序)。最好的做法是只使用tabindex="0"
来按自然焦点顺序添加元素,而使用tabindex="-1"
从焦点顺序中删除元素(但仍然可以在其上使用JavaScript .focus()
方法,如果需要)。
简短答案
在不进行过渡的情况下使用display:none
是最佳选择,并且不需要aria-hidden
。
如果需要进行转换,请进行转换,然后在转换后设置display: none
属性。
尽管过渡时间超过100毫秒,但请注意不要丢失焦点,您必须进行大量的焦点管理才能解决设置display:none
的延迟。
更长的答案
aria-hidden="true"
从可访问性树中删除项目及其子项。但是,这不会阻止能够获得焦点(即<input>
)的孩子获得焦点。
tabindex="-1"
不会从已经可以聚焦的子元素中移除焦点。
解决所有问题的最简单方法是删除过渡并只需切换display属性。这不仅解决了您的重点问题,而且消除了对aria-hidden
的需求,使事情变得更简单。
话虽如此,过渡可能是您的规格之一,并且是不可避免的。如果是这种情况,则需要考虑一些事项。
在我们的评论讨论中以及您的问题中,您提到使用setTimeout
将转换完成后的display属性设置为none。
这种方法取决于您的设计。
如果下一个制表位在要隐藏的区域内,则在过渡期间有人可以导航到要隐藏的区域内的元素是可行的。
如果发生这种情况,将失去对页面的关注。根据浏览器的不同,这可能导致焦点返回到页面顶部。在WCAG原则的逻辑制表符顺序/鲁棒性下,这将是非常令人沮丧的事情,并且也可能构成失败。
在皮革上实现动画的最佳方法是什么?
由于存在焦点问题,我建议采用以下过程来隐藏带有过渡的内容:-
- 第二个导致隐藏区域的按钮/代码被激活(淡出),在
tabindex="-1"
中所有要隐藏的交互式元素上设置了<div>
(或者如果它们是设置disabled
属性的输入)。 - 通过任何使用方式开始转换(即,将类添加到将触发转换的项目中)。
- 转换完成后,在项目上设置
display: none
。 - 如果希望再次显示
<div>
,请执行相反的操作。
这样做可以确保没有人会偶然进入div并失去焦点。这可以帮助所有依靠键盘进行导航的人,而不仅仅是屏幕阅读器用户。
下面是一个如何实现此目标的非常粗糙的示例。可以根据容器的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>