使用 :focus-within 时更新 aria-expanded 属性的最佳方法 [英] Best way to update aria-expanded attribute when using :focus-within
问题描述
我正在开发一个组合框,为此,我想使用新的 :focus-within
伪选择器来管理显示与组合框关联的可扩展列表框.
:focus-within 是一个很好的解决方案,而且效果很好.我遇到的唯一问题是弄清楚如何更新列表框上的 aria-expanded
属性.由于伪选择器,所有隐藏/显示功能都发生在浏览器中,我不确定可以在 Javascript 中使用的钩子来确定项目何时可见或不可见,以便更新该属性.
有谁知道一个优雅的解决方案吗?我不想复制 :focus-within 正在处理的焦点/模糊逻辑,只是为了更新这个属性.我也担心他们可能会不同步.有没有办法用 :focus-within
或类似的东西来监视元素?
.list {显示:无;}.combobox-container:focus-within .list {显示:块;}
<div role="combobox" aria-expanded="false" aria-owns="listbox" aria-haspopup="listbox"><标签>富<input type="text" aria-autocomplete="list" aria-controls="listbox"/>标签>
<ul class="list" id="listbox" role="listbox" tabindex="0" aria-multiselectable="true"><!-- 用于自动完成的项目.li 标签内的可聚焦锚点.--><li><a href="#">Javascript</a></li><li><a href="#">HTML</a></li><li><a href="#">CSS</a></li></section>
因此,这会立即使您的解决方案无法访问,因为许多屏幕阅读器用户仍在使用 JAWs
和 Internet Explorer.
此外,您还遇到了一个问题,虽然这作为演示工作,但在现实世界中,将通过 AJAX 或通过过滤的预加载列表填充自动完成列表.
这意味着列表将始终在您聚焦 <input>
时显示,即使未在组合框中输入任何内容(这不是预期的行为).
这是少数可以接受仅依赖 JavaScript 的情况之一(有一个回退,即表单仍然可以在没有 JavaScript 的情况下提交).
当您返回一些建议然后使用
以下示例显示了实现此目的的 CSS.+
操作符是关键,它是 Adjacent Sibling Combinator 选择父元素中的下一个兄弟元素.
CSS: .combobox-container div[aria-expanded="true"]+.list
对于下面的示例,我已经做了这样的设置,一旦您在框中键入超过 1 个字符,它就会将 aria-expanded
属性更改为 true
(然后返回再次,如果输入为空) - 这让它感觉更像是一个真实世界"的例子.
旁注:您不需要向
添加 tabindex
,预期的行为是直接使用 Tab第一个建议的项目,我在下面的例子中删除了它.
//忽略这一点,这是我对代码片段的标准 jQuery 替换if(typeof $=="undefined"){!function(b,c,d,e,f){f=b['add'+e]函数 i(a,d,i){for(d=(a&&a.nodeType?[a]:''+a===a?b.querySelectorAll(a):c),i=d.长度;i--;c.unshift.call(this,d[i]));}$=function(a){return/^f/.test(typeof a)?/in/.test(b.readyState)?setTimeout(function(){$(a);},9):a():new i(a);};$[d]=i[d]={on:function(a,b){return this.each(function(c){f?c['add'+e](a,b,false):c.attachEvent('on'+a,b)})},off:function(a,b){return this.each(function(c){f?c['remove'+e](a,b):c.detachEvent('on'+a,b)})},each:function(a,b){for(var c=this,d=0,e=c.length;d<e;++d){a.call(b||c[d],c[d],d,c)}return c},splice:c.splice}}(document,[],'prototype','EventListener');var props=['add','remove','toggle','has'],maps=['add','remove','toggle','contains'];props.forEach(function(prop,index){$.prototype[prop+'Class']=function(a){return this.each(function(b){if(a){b.classList[maps[index]](a);}});};});$.prototype.hasClass=function(a){return this[0].classList.contains(一种);};}$.prototype.find=function(selector){return $(selector,this);};$.prototype.parent=function(){return(this.length==1)?$(this[0].parentNode):[];};$.prototype.findWithin=function(a){console.log("THIS IS",this[0],a);return this[0].getElementsByClassName(a);};$.prototype.first=function(){return $(this[0]);};$.prototype.focus=function(){return this[0].focus();};$.prototype.css=function(a,b){if(typeof(a)==='object'){for(var prop in a){this.each(function(c){c.style[prop]=a[prop];});}return this;}else{return b===[]._?this[0].style[a]:this.each(function(c){c.style[a]=b;});}};$.prototype.text=function(a){return a===[]._?this[0].textContent:this.each(function(b){b.textContent=a;});};$.prototype.html=function(a){return a===[]._?this[0].innerHTML:this.each(function(b){b.innerHTML=a;});};$.prototype.attr=function(a,b){return b===[]._?this[0].getAttribute(a):this.each(function(c){c.setAttribute(a,b);});};$.param=function(obj,prefix){var str=[];for(var p in obj){var k=prefix?prefix+"["+p+"]":p,v=obj[p];str.push(typeof v=="object"?$.param(v,k):encodeURIComponent(k)+"="+encodeURIComponent(v));}return str.join("&");};$.prototype.append=function(a){return this.each(function(b){b.appendChild(a[0]);});};$.ajax=function(a,b,c,d){var xhr=new XMLHttpRequest();var type=(typeof(b)==='object')?1:0;var gp=['GET','POST'];xhr.open(gp[type],a,true);if(type==1){xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");}xhr.responseType=(typeof(c)==='string')?c:'';var cb=(!type)?b:c;xhr.onerror=function(){cb(this,true);};xhr.onreadystatechange=function(){if(this.readyState==4){if(this.status>=200&&this.status<400){cb(this,false);}else{cb(this,true);}}};if(type){xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');xhr.send($.param(b));}其他{xhr.send();}xhr=null;};//只是演示的一部分,不用于生产用途$('input').on('keyup', function(e){if($(this)[0].value.length > 0){$('div[role=combobox]').attr('aria-expanded', true);返回;}$('div[role=combobox]').attr('aria-expanded', false);返回;});
.list {显示:无;}.combobox-container div[aria-expanded="true"]+.list{显示:块;边框:2px 实心 #333;}
<div role="combobox" aria-expanded="false" aria-owns="listbox" aria-haspopup="listbox"><标签>富<input type="text" aria-autocomplete="list" aria-controls="listbox"/>标签>
<ul class="list" id="listbox" role="listbox" aria-multiselectable="true"><!-- 用于自动完成的项目.li 标签内的可聚焦锚点.--><li><a href="#">Javascript</a></li><li><a href="#">HTML</a></li><li><a href="#">CSS</a></li></section>
I'm working on a combobox, and in doing so, I want to use the new :focus-within
pseudo selector to manage displaying the expandable listbox that's associated with the combobox.
:focus-within is a great solution and works like a charm. The only problem I'm having is figuring out how to keep the aria-expanded
attribute on the listbox updated. Because all of the hide/show functionality is happening in browser-land due to the pseudo-selector, I'm unsure of a hook I can use in Javascript to determine when the item is visible or not in order to update that property.
Is there an elegant solution anyone knows about? I'd hate to have to replicate the logic for focus/blur that :focus-within is handling right now just to update this attribute. I'm also concerned they may get out of sync. There any way to spy on an element with :focus-within
or something like that?
.list {
display: none;
}
.combobox-container:focus-within .list {
display: block;
}
<section class="combobox-container">
<div role="combobox" aria-expanded="false" aria-owns="listbox" aria-haspopup="listbox">
<label> Foo
<input type="text" aria-autocomplete="list" aria-controls="listbox" />
</label>
</div>
<ul class="list" id="listbox" role="listbox" tabindex="0" aria-multiselectable="true">
<!-- items for autocomplete. focusable anchors inside li tags. -->
<li><a href="#">Javascript</a></li>
<li><a href="#">HTML</a></li>
<li><a href="#">CSS</a></li>
</ul>
</section>
Accessibility guidelines for a combobox
focus-within only has 84% browser coverage
For this reason that instantly makes your solution inaccessible as a lot of screen reader users still use JAWs
with Internet Explorer.
Additionally you have the problem that while this works as a demo, in the real world an auto-complete list will be populated via AJAX or via a preloaded list that is filtered.
This means that the list will always be shown the second you focus the <input>
, even when nothing has been typed into the combobox (which is not expected behaviour).
This is one of the few circumstances where relying solely on JavaScript is acceptable (with a fallback that the form can still be submitted without JavaScript).
Instead of trying to use :focus-within
you can instead use JavaScript to toggle aria-expanded="true"
when you return some suggestions and then use standard CSS3 selectors to show and hide the results.
The below example shows the CSS to achieve this. The +
operator is the key, it is the Adjacent Sibling Combinator that selects the next sibling within a parent element.
CSS: .combobox-container div[aria-expanded="true"]+.list
For the example below I have made it so that once you type more than 1 character into the box it will change the aria-expanded
attribute to true
(and back again if the input is empty) - this makes it feel more like a 'real world' example.
Side note: You do not need to add a tabindex
to the <ul>
, the expected behaviour is to tab directly to the first suggested item, I have removed that in the example below.
//ignore this, this is my standard jQuery replacement for snippets
if(typeof $=="undefined"){!function(b,c,d,e,f){f=b['add'+e]
function i(a,d,i){for(d=(a&&a.nodeType?[a]:''+a===a?b.querySelectorAll(a):c),i=d.length;i--;c.unshift.call(this,d[i]));}
$=function(a){return /^f/.test(typeof a)?/in/.test(b.readyState)?setTimeout(function(){$(a);},9):a():new i(a);};$[d]=i[d]={on:function(a,b){return this.each(function(c){f?c['add'+e](a,b,false):c.attachEvent('on'+a,b)})},off:function(a,b){return this.each(function(c){f?c['remove'+e](a,b):c.detachEvent('on'+a,b)})},each:function(a,b){for(var c=this,d=0,e=c.length;d<e;++d){a.call(b||c[d],c[d],d,c)}
return c},splice:c.splice}}(document,[],'prototype','EventListener');var props=['add','remove','toggle','has'],maps=['add','remove','toggle','contains'];props.forEach(function(prop,index){$.prototype[prop+'Class']=function(a){return this.each(function(b){if(a){b.classList[maps[index]](a);}});};});$.prototype.hasClass=function(a){return this[0].classList.contains(a);};}
$.prototype.find=function(selector){return $(selector,this);};$.prototype.parent=function(){return(this.length==1)?$(this[0].parentNode):[];};$.prototype.findWithin=function(a){console.log("THIS IS",this[0],a);return this[0].getElementsByClassName(a);};$.prototype.first=function(){return $(this[0]);};$.prototype.focus=function(){return this[0].focus();};$.prototype.css=function(a,b){if(typeof(a)==='object'){for(var prop in a){this.each(function(c){c.style[prop]=a[prop];});}
return this;}else{return b===[]._?this[0].style[a]:this.each(function(c){c.style[a]=b;});}};$.prototype.text=function(a){return a===[]._?this[0].textContent:this.each(function(b){b.textContent=a;});};$.prototype.html=function(a){return a===[]._?this[0].innerHTML:this.each(function(b){b.innerHTML=a;});};$.prototype.attr=function(a,b){return b===[]._?this[0].getAttribute(a):this.each(function(c){c.setAttribute(a,b);});};$.param=function(obj,prefix){var str=[];for(var p in obj){var k=prefix?prefix+"["+p+"]":p,v=obj[p];str.push(typeof v=="object"?$.param(v,k):encodeURIComponent(k)+"="+encodeURIComponent(v));}
return str.join("&");};$.prototype.append=function(a){return this.each(function(b){b.appendChild(a[0]);});};$.ajax=function(a,b,c,d){var xhr=new XMLHttpRequest();var type=(typeof(b)==='object')?1:0;var gp=['GET','POST'];xhr.open(gp[type],a,true);if(type==1){xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");}
xhr.responseType=(typeof(c)==='string')?c:'';var cb=(!type)?b:c;xhr.onerror=function(){cb(this,true);};xhr.onreadystatechange=function(){if(this.readyState===4){if(this.status>=200&&this.status<400){cb(this,false);}else{cb(this,true);}}};if(type){xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');xhr.send($.param(b));}else{xhr.send();}
xhr=null;};
//only part of the demo, not for production use
$('input').on('keyup', function(e){
if($(this)[0].value.length > 0){
$('div[role=combobox]').attr('aria-expanded', true);
return;
}
$('div[role=combobox]').attr('aria-expanded', false);
return;
});
.list {
display: none;
}
.combobox-container div[aria-expanded="true"]+.list{
display: block;
border:2px solid #333;
}
<section class="combobox-container">
<div role="combobox" aria-expanded="false" aria-owns="listbox" aria-haspopup="listbox">
<label> Foo
<input type="text" aria-autocomplete="list" aria-controls="listbox" />
</label>
</div>
<ul class="list" id="listbox" role="listbox" aria-multiselectable="true">
<!-- items for autocomplete. focusable anchors inside li tags. -->
<li><a href="#">Javascript</a></li>
<li><a href="#">HTML</a></li>
<li><a href="#">CSS</a></li>
</ul>
</section>
这篇关于使用 :focus-within 时更新 aria-expanded 属性的最佳方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!