可访问的手风琴:隐藏面板时如何不关注可聚焦元素?

Accessible Accordion: How do I not focus on a focusable element when the panel is hidden?

我正在尝试创建一个易于访问的手风琴,目前正在测试键盘导航。当我开始在手风琴中切换时,它会专注于链接、按钮、复选框等。即使面板是隐藏的。

我知道可聚焦元素是目标,因为我的面板使用高度:0 而不是显示: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");
}

Accessible Accordion Codepen

简答

假设您没有设置任何正 tabindexs 那么上面的方法就可以了。但是,如果您确实有一个 tabindex 集(您实际上不应该这样做),那么它会有点复杂。

另一件事是使用 <details><summary> 将使您的应用程序更易于访问。

速记

正如@CBroe 在评论中提到的,使用 transitionendsetTimeout 更好。我一直生活在石器时代,认为它没有很好的支持,但总是在 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, adipisicing elit. Ipsum, reiciendis!</p>
    </details>

处理焦点

执行此操作的简单方法是使用 JavaScript 在动画完成后更改显示 属性(并在开始前取消隐藏)使用 display: none

因此,如果您的动画是 1 秒,您只需在添加任何触发高度动画的 class 之前设置 display: block

要关闭你触发高度动画(删除你的class)并使用setTimeout 1秒然后触发display: none

显然这确实存在问题,因为当它达到高度 0 时,有人可能最终进入手风琴面板,然后当您设置 display: none 时,页面焦点位置将会丢失。

另一种方法是按照您的建议设置 tabindex="-1",因为您可以在关闭手风琴的那一刻设置。

以下示例取自我在动画部分设置 tabindex 时给出的答案。

它考虑的比你需要的多(积极的 tabindexs,使用 [=33= 关闭动画],可以使用 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>