可访问的手风琴:隐藏面板时如何不关注可聚焦元素?
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");
}
简答
假设您没有设置任何正 tabindex
s 那么上面的方法就可以了。但是,如果您确实有一个 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, adipisicing elit. Ipsum, reiciendis!</p>
</details>
处理焦点
执行此操作的简单方法是使用 JavaScript 在动画完成后更改显示 属性(并在开始前取消隐藏)使用 display: none
。
因此,如果您的动画是 1 秒,您只需在添加任何触发高度动画的 class 之前设置 display: block
。
要关闭你触发高度动画(删除你的class)并使用setTimeout
1秒然后触发display: none
。
显然这确实存在问题,因为当它达到高度 0 时,有人可能最终进入手风琴面板,然后当您设置 display: none
时,页面焦点位置将会丢失。
另一种方法是按照您的建议设置 tabindex="-1"
,因为您可以在关闭手风琴的那一刻设置。
以下示例取自我在动画部分设置 tabindex
时给出的答案。
它考虑的比你需要的多(积极的 tabindex
s,使用 [=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>
我正在尝试创建一个易于访问的手风琴,目前正在测试键盘导航。当我开始在手风琴中切换时,它会专注于链接、按钮、复选框等。即使面板是隐藏的。
我知道可聚焦元素是目标,因为我的面板使用高度: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");
}
简答
假设您没有设置任何正 tabindex
s 那么上面的方法就可以了。但是,如果您确实有一个 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, adipisicing elit. Ipsum, reiciendis!</p>
</details>
处理焦点
执行此操作的简单方法是使用 JavaScript 在动画完成后更改显示 属性(并在开始前取消隐藏)使用 display: none
。
因此,如果您的动画是 1 秒,您只需在添加任何触发高度动画的 class 之前设置 display: block
。
要关闭你触发高度动画(删除你的class)并使用setTimeout
1秒然后触发display: none
。
显然这确实存在问题,因为当它达到高度 0 时,有人可能最终进入手风琴面板,然后当您设置 display: none
时,页面焦点位置将会丢失。
另一种方法是按照您的建议设置 tabindex="-1"
,因为您可以在关闭手风琴的那一刻设置。
以下示例取自我在动画部分设置 tabindex
时给出的答案。
它考虑的比你需要的多(积极的 tabindex
s,使用 [=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>