问题描述
我正在尝试创建可访问的手风琴,目前正在测试键盘导航。当我开始浏览手风琴时,即使面板是隐藏的,它也会关注链接,按钮,复选框等。
我知道可聚焦元素是目标对象,因为我的面板使用height:0而不是display:none。我正在使用高度进行过渡。
我能想到的唯一解决方案是在面板隐藏时选择面板中的所有可聚焦元素,并对它们应用tabindex =“-1”。这很奇怪还是有更好的方法让我解决这个问题?
类似这样的东西:
focusableElms = panel.querySelectorAll("a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex='0']");
var focusableElm;
for (a = (focusableElms.length - 1); a >= 0; a--) {
focusableElm = focusableElms[a];
focusableElm.setAttribute("tabindex","-1");
}
解决方法
简短答案
假设您没有设置任何肯定的tabindex
,那么以上方法将起作用。但是,如果您确实有一个tabindex
设置(您实际上不应该设置),则它会更复杂。
另一件事是使用<details>
,而<summary>
将使您的应用程序更易于访问。
速战速决
就像@CBroe在评论中提到的那样,使用transitionend
会比setTimeout
更好。我一直生活在石器时代,以为它没有很好的支持,但总是在caniuse.com上看错了项目。
更长的答案
首先让我们获得合适的HTML,因为它为我们提供了一些现代浏览器中的强大功能。
详细信息和摘要
<details>
和<summary>
自动为您提供大量功能。它们自动关联控件(等效于aria-controls
),它们是纯净的标记,它们在大多数浏览器中自动具有打开和关闭功能,作为JavaScript失败等时的后备方式。
我之前已经介绍过这些,因此您可以read more about <details>
and <summary>
in this answer I gave.
<details>
<summary>Item 1</summary>
<p>Lorem ipsum dolor sit amet consectetur,adipisicing elit. Ipsum,reiciendis!</p>
</details>
<details>
<summary>Item 2</summary>
<p>Lorem ipsum dolor sit amet consectetur,reiciendis!</p>
</details>
处理焦点
最简单的方法是在动画完成后(并在开始播放之前取消隐藏),使用JavaScript使用display: none
来更改显示属性。
因此,如果您的动画是1秒,则只需在添加任何触发高度动画的类之前设置display: block
。
要关闭,您将触发高度动画(删除班级)并使用setTimeout
1秒钟,然后触发display: none
。
显然这确实存在问题,因为有人可能最终在进入高度0时进入手风琴面板,然后在设置display: none
时,页面焦点位置将会丢失。
另一种方法是按照您的建议设置tabindex="-1"
,因为您可以在关闭手风琴的那一刻进行设置。
下面的示例摘自我在动画部分设置tabindex
时给出的答案。
它考虑了超出您需要的范围(正tabindex
,使用prefers-reduced-motion
关闭动画,可以使用content-editable
的事实等),但应该为您提供信息您需要。
边缘有些粗糙,但是应该为您提供良好的接地。
我在代码中添加了很多注释,因此希望您能理解适合您的部分,并使其适合使用<details>
和<summary>
来完成解决方案。
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>