正确隐藏键盘和屏幕上的内容 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 原则中逻辑选项卡顺序/稳健性的失败。

实现隐藏动画的最佳方法是什么?

由于焦点问题,我建议使用以下过程来隐藏带有转换的内容:-

  1. 第二次激活导致区域隐藏的按钮/代码(在 fade-out 之前)在要隐藏的 <div> 内的所有交互元素上设置 tabindex="-1" (或者如果它们是输入,则设置 disabled 属性)。
  2. 通过您使用的任何方式开始转换(即将 class 添加到将触发转换的项目)。
  3. 转换完成后,在项目上设置 display: none
  4. 如果您希望 <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 之前提供的解决方案,您需要确保:

  1. 模态的所有 个可聚焦子项在模态隐藏时接收 tabindex="-1"
  2. aria-hidden 已从父项中删除(设置为 false)并且所有这些可聚焦的子项都已 tabindex 删除。

注意: 避免为 tabindex 使用正值(即 tabindex="1"),因为它会扰乱页面的焦点顺序(通常遵循 DOM 的顺序并且应该遵循页面的阅读顺序)。最好的是只使用 tabindex="0" 以自然焦点顺序添加元素,并使用 tabindex="-1" 将其从焦点顺序中删除(但仍然可以使用 JavaScript .focus()方法,如果需要的话)。