从键盘和屏幕阅读器正确隐藏内容 [英] Properly hiding content from both keyboard and screen reader

查看:45
本文介绍了从键盘和屏幕阅读器正确隐藏内容的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们在页面上有一个模式,当隐藏时,我们希望让键盘用户能够进入内容,也不允许屏幕阅读器阅读.

为了处理这个问题,在父 DIV 上,我进行了设置,以便在隐藏时,它具有以下内容:

不幸的是,这不起作用.您仍然可以使用 Tab 键进入内容并阅读内容(至少通过 Chrome 和使用 VoiceOver).

理想情况下,我们还可以设置 display: none——我可能可以这样做——但目前我们依赖于一些 CSS 过渡动画,因此需要设置它动画后以编程方式.

然而,在走这条路之前,我最初的理解是 aria-hidden 和 tabindex 应该解决这个问题吗?

解决方案

简短回答

使用 display:none 没有过渡将是最好的选择,并且不需要 aria-hidden.

如果您需要进行过渡,请先进行过渡,然后在过渡后设置 display: none 属性.

注意失去焦点,但如果过渡超过 100 毫秒,则必须进行大量焦点管理以解决设置 display:none 的延迟.

更长的答案

aria-hidden="true" 从可访问性树中删除项目及其子项.但是,它不会阻止可以接收焦点(即 )的子节点接收焦点.

tabindex=-1" 不会从已经可聚焦的子元素中移除焦点.

解决所有问题的最简单方法是移除过渡并简单地切换显示属性.这不仅解决了你的注意力问题,而且还消除了对 aria-hidden 的需求,让事情变得更简单.

话虽如此,转换可能是您规范的一部分并且不可避免.如果是这种情况,则需要考虑一些事项.

在我们在评论和您的问题中的讨论中,您提到使用 setTimeout 在转换完成后将 display 属性设置为 none.

这种方法存在问题,具体取决于您的设计.

如果下一个制表位在被隐藏的区域内,那么在转换期间有人可能导航到即将被隐藏的区域内的元素是可行的.

如果发生这种情况,页面上的焦点就会丢失.根据浏览器的不同,这可能会导致焦点返回到页面顶部.这是非常令人沮丧的事情,并且也可能在 WCAG 原则中的逻辑选项卡顺序/稳健性下构成失败.

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

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

  1. 第二个导致区域被隐藏的按钮/代码被激活(预淡出)在 中的所有交互元素上设置 tabindex=-1"要隐藏的 div>(或者如果它们是输入,则设置 disabled 属性).
  2. 通过您使用的任何方式开始转换(即,将类添加到将触发转换的项目中).
  3. 转换完成后,在项目上设置display: none.
  4. 如果您想让

    再次可见,请执行完全相反的操作.

通过这样做,您可以确保没有人会意外进入 div 并失去焦点.这对依赖键盘进行导航的每个人都有帮助,而不仅仅是屏幕阅读器用户.

如何实现这一点的一个非常粗略的例子如下.它可以根据容器的 ID 重用,因此希望能给你一个好的起点来写一些更健壮的东西(而且不那么丑!嘿嘿)

我已添加注释以尽我所能进行解释.我已将过渡设置为 2 秒,以便您可以检查并查看事物的顺序.

最后,我添加了一些 CSS 和 JS 来解释那些表示由于运动敏感性而更喜欢减少运动的人.在这种情况下,动画时间设置为 0.

隐藏项目以管理 tabindex 和恢复 tabindex 如果再次可见的粗略示例.

var content = document.getElementById('contentDiv');var btn = document.getElementById('btn_toggle');var animationDelay = 2000;//我们应该考虑前庭运动障碍等的人,如果他们表示他们更喜欢减少运动.我们将动画时间设置为 0 秒.var motionQuery = matchMedia('(prefers-reduced-motion)');函数 handleReduceMotionChanged() {如果(motionQuery.matches){动画延迟 = 0;} 别的 {动画延迟 = 2000;}}motionQuery.addListener(handleReduceMotionChanged);handleReduceMotionChanged();//用于将具有给定 ID 的父级的所有子级的 tabindex 设置为 -1 的主要功能(并反转该过程)函数 hideOrShowAllInteractiveItems(parentDivID){//所有可聚焦元素的选择器列表.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])'];//构建一个查询字符串,它针对父 div ID 和我们的可聚焦项列表中的所有子元素.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 

@keyframes in {0% { 变换:缩放(0);不透明度:0;可见性:隐藏;}100% { 变换:缩放(1);不透明度:1;可见性:可见;}}@关键帧出{0% { 变换:缩放(1);不透明度:1;可见性:可见;}100% { 变换:缩放(0);不透明度:0;可见性:隐藏;}}#contentDiv {背景:灰色;白颜色;填充:16px;底边距:10px;}#contentDiv.show {动画:在 2 秒内缓解两者;}#contentDiv.hide {动画:出 2 秒缓和;}/*****我们应该考虑患有前庭运动障碍等的人,如果他们表示他们更喜欢减少运动.***/@media(喜欢减少运动){#contentDiv.show,#contentDiv.hide{动画:无;}}

<p>一些需要隐藏的信息</p><输入/><按钮>一个按钮</按钮><button tabindex="1">一个需要恢复的带有正tabindex的按钮</button>

<button id="btn_toggle">隐藏 Div

We have a modal on a page that, when hidden, we want to not have keyboard users be able to tab into the content, nor have screen readers read.

To handle that, on the parent DIV, I've set up so that, when hidden, it has the following:

<div aria-hidden="true" tabindex="-1">
    [child HTML/content]
<div>

Unfortunately, this isn't working. You can still tab into the content and the content is read (at least via Chrome and using VoiceOver).

Ideall, we'd also set display: none-- which I may be able to do -- but at the moment we're dependant on some CSS transition animations so would need to set that programmatically after animation.

Prior to going that route, however, is there anything I am missing from my initial understanding that aria-hidden and the tabindex should be taking care of the issue?

解决方案

Short Answer

Using display:none without a transition would be the best option and would negate the need for aria-hidden.

If you are required to have a transition then do the transition and then set the display: none property after the transition.

Be careful for lost focus though if you transition is over 100ms, you have to do a lot of focus management to account for the delay in setting display:none.

Longer Answer

aria-hidden="true" removes an item and its children from the accessibility tree. However it does not stop children that can receive focus (i.e. an <input>) from receiving focus.

tabindex="-1" will not remove focus from children elements that are already focusable.

The simplest way to solve all problems would be to remove the transition and simply toggle the display property. This not only solves your focus problem but it also removes the need for aria-hidden, making things simpler.

With that being said a transition may be part of your spec and unavoidable. If that is the case there are a few things to consider.

In our discussions in the comments and within your question you mentioned using setTimeout to set the display property to none after the transition has completed.

There is an issue with this approach depending on your design.

If the next tab stop is within the area that is being hidden it is feasible that during the transition someone may navigate to an element within the area that is about to be hidden.

If this were to happen focus on the page would be lost. Depending on the browser this could result in focus returning to the top of the page. This is something that would be highly frustrating and also probably constitute a fail under logical tab order / robustness in WCAG principles.

What would be the best way to achieve an animation on hide?

Because of the focus issue I would recommend the following process for hiding the content with a transition:-

  1. The second the button / code that causes the area to be hidden is activated (pre fade-out) set tabindex="-1" on all interactive elements within the <div> that is to be hidden (or if they are inputs set the disabled attribute).
  2. Start the transition by whatever means you are using (i.e. add the class to the item that will trigger the transition).
  3. After the transition is complete set the display: none on the item.
  4. Do the exact opposite if you wish to make the <div> visible again.

By doing this you ensure that nobody can accidentally tab into the div and lose focus. This helps everyone who relies on keyboard for navigation, not just screen reader users.

A very rough example of how to achieve this is below. It can be reused based on an ID of a container so hopefully will give you a good starting place to write something a little more robust (and less ugly! hehe)

I have added comments to explain as best I can. I have set the transition to 2 seconds so you can inspect and see the order of things.

Finally I have included some CSS and JS to account for people who have indicated they prefer reduced motion due to motion sensitivity. In this case the animation time is set to 0.

Rough example accounting for hiding items to manage tabindex and restoring tabindex if made visible again.

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>

这篇关于从键盘和屏幕阅读器正确隐藏内容的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆