简体   繁体   English

AngularJS - NVDA 屏幕阅读器找不到子元素的名称

[英]AngularJS - NVDA screen reader not finding names of child elements

Apologies for the bare-bones HTML here...为这里的基本 HTML 道歉......

I've got some AngularJS components that are rendering this HTML for a multiselectable dropdown:我有一些 AngularJS 组件正在为一个多选下拉列表呈现这个 HTML:

<ul role="listbox">
    <li>
        <div ng-attr-id="ui-select-choices-row-{{ $select.generatedId }}-{{$index}}" class="ui-select-choices-row ng-scope" ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}" role="option" ng-repeat="opt in $select.items" ng-if="$select.open" ng-click="$select.select(opt,$select.skipFocusser,$event)" tabindex="0" id="ui-select-choices-row-0-1" style="">
            <a href="" class="ui-select-choices-row-inner" uis-transclude-append="">
                <span ng-class="{'strikethrough' : rendererInactive(opt)}" title="ALBANY" aria-label="ALBANY" class="ng-binding ng-scope">ALBANY</span>
            </a>
        </div>
        (a hundred or so more options in similar divs)
    </li>
</ul>

What we need is for screen reading software to speak aloud each option as it's highlighted via arrow key navigation.我们需要的是让屏幕阅读软件大声朗读每个选项,因为它通过箭头键导航突出显示。 As it is now, NVDA says "blank" when keying through the list.就像现在一样,NVDA 在键入列表时说“空白”。 If, in the directive we're using to create this HTML, I add role="presentation" to the <ul> , then NVDA will recite the entire list of options as soon as the dropdown opens, but not individually for each arrow key keystroke (and after hitting Escape to make it stop talking, keying through the options says "blank" again).如果在我们用来创建此 HTML 的指令中,我将role="presentation"添加到<ul> ,那么 NVDA 将在下拉菜单打开后立即背诵整个选项列表,但不会单独为每个箭头键击键(在点击 Escape 使其停止说话后,通过选项再次键入“空白”)。

I keep thinking that the listbox and option roles are in the correct places, but is something else in the structure preventing the screen reader from finding the values correctly?我一直认为listboxoption角色位于正确的位置,但是结构中是否有其他东西阻止屏幕阅读器正确找到值?

This answer got quite long, the first 3 points are most likely the problem, the rest are other considerations / observations这个答案很长,前 3 点很可能是问题,其余是其他考虑/观察

There are a few things that are likely to cause this issue, although without seeing the generated HTML rather than the Angular Source there could be others.有一些事情可能会导致此问题,尽管没有看到生成的 HTML 而不是 Angular 源,可能还有其他事情。

Most likely culprit is that your anchors are not valid.最有可能的罪魁祸首是您的锚点无效。 You cannot have a blank href ( href="" ) for it to be valid.您不能有一个空白的 href ( href="" ) 才能使其有效。 Looking at your source code could you not remove this and adjust your CSS or change it to a <div> ?查看您的源代码,您不能删除它并调整您的 CSS 或将其更改为<div>吗?

Second most likely culprit is that role="option" should be on the direct children on role="listbox" .第二个最可能的罪魁祸首是role="option"应该在role="listbox"直接子级上。 Move it to your <li> s and make them selectable with tabindex="-1" (see below point on tabindex="0" ) instead.将其移动到您的<li>并使用tabindex="-1" (参见下面关于tabindex="0" )使它们可选。 (in fact why not simply remove the surrounding <div> and apply all of your angular directives to the <li> directly). (事实上​​,为什么不简单地删除周围的<div>并将所有角度指令直接应用于<li> )。

Third most likely culprit is the fact that aria-label is not needed and may in fact be interfering, a screen reader will read the text within your <span> without this.第三个最可能的罪魁祸首是不需要aria-label并且实际上可能会产生干扰,屏幕阅读器将在没有这个的情况下读取<span>的文本。 Golden rule - do not use aria unless you can't portray the information another way.黄金法则 - 除非您无法以另一种方式描绘信息,否则不要使用aria

You also need to add aria-selected="true" (or false) to each <li role="option"> to indicate whether an item is selected or not.您还需要为每个<li role="option">添加aria-selected="true" (或 false) 以指示是否选择了某个项目。

Also you should add aria-multiselectable="true" to the <ul> to indicate it is a multi select.此外,您应该将aria-multiselectable="true"<ul>以指示它是一个多选。

While you are at it, remove the title attribute, it doesn't add anything useful here.当您使用它时,请删除title属性,它不会在此处添加任何有用的内容。

aria-activedescendant="id" should be used to indicate which item is currently focused. aria-activedescendant="id"应该用于指示当前关注哪个项目。

Be careful with tabindex="0" - I can't see if this is applied to everything but really it should be tabindex="-1" and you programatically manage focus as otherwise users could tab to items that they aren't meant to.小心tabindex="0" - 我看不出这是否适用于所有内容,但实际上它应该是tabindex="-1"并且您以编程方式管理焦点,否则用户可以选择他们不打算使用的项目. tabindex="0" should be on the main <ul> . tabindex="0"应该在主<ul>

Due to the complex nature of multi-selects you would be much better using a group of checkboxes as they provide a lot of the functionality for free, but that is just a suggestion.由于多选的复杂性,您最好使用一组复选框,因为它们免费提供了许多功能,但这只是一个建议。

The following example I found on codepen.io covers 95% of everything if you use a checkbox instead and would be a good base for you to pick apart and adapt to your needs, as you can see checkboxes make life a lot easier as all the selected not selected functionality is built in.我在 codepen.io 上找到的以下示例涵盖了所有内容的 95%,如果您使用复选框,这将是您挑选并适应您的需求的良好基础,因为您可以看到复选框使生活变得更加轻松,因为所有选择未选择功能是内置的。

 (function($){ 'use strict'; const DataStatePropertyName = 'multiselect'; const EventNamespace = '.multiselect'; const PluginName = 'MultiSelect'; var old = $.fn[PluginName]; $.fn[PluginName] = plugin; $.fn[PluginName].Constructor = MultiSelect; $.fn[PluginName].noConflict = function () { $.fn[PluginName] = old; return this; }; // Defaults $.fn[PluginName].defaults = { }; // Static members $.fn[PluginName].EventNamespace = function () { return EventNamespace.replace(/^\\./ig, ''); }; $.fn[PluginName].GetNamespacedEvents = function (eventsArray) { return getNamespacedEvents(eventsArray); }; function getNamespacedEvents(eventsArray) { var event; var namespacedEvents = ""; while (event = eventsArray.shift()) { namespacedEvents += event + EventNamespace + " "; } return namespacedEvents.replace(/\\s+$/g, ''); } function plugin(option) { this.each(function () { var $target = $(this); var multiSelect = $target.data(DataStatePropertyName); var options = (typeof option === typeof {} && option) || {}; if (!multiSelect) { $target.data(DataStatePropertyName, multiSelect = new MultiSelect(this, options)); } if (typeof option === typeof "") { if (!(option in multiSelect)) { throw "MultiSelect does not contain a method named '" + option + "'"; } return multiSelect[option](); } }); } function MultiSelect(element, options) { this.$element = $(element); this.options = $.extend({}, $.fn[PluginName].defaults, options); this.destroyFns = []; this.$toggle = this.$element.children('.toggle'); this.$toggle.attr('id', this.$element.attr('id') + 'multi-select-label'); this.$backdrop = null; this.$allToggle = null; init.apply(this); } MultiSelect.prototype.open = open; MultiSelect.prototype.close = close; function init() { this.$element .addClass('multi-select') .attr('tabindex', 0); initAria.apply(this); initEvents.apply(this); updateLabel.apply(this); injectToggleAll.apply(this); this.destroyFns.push(function() { return '|' }); } function injectToggleAll() { if(this.$allToggle && !this.$allToggle.parent()) { this.$allToggle = null; } this.$allToggle = $("<li><label><input type='checkbox'/>(all)</label><li>"); this.$element .children('ul:first') .prepend(this.$allToggle); } function initAria() { this.$element .attr('role', 'combobox') .attr('aria-multiselect', true) .attr('aria-expanded', false) .attr('aria-haspopup', false) .attr('aria-labeledby', this.$element.attr("aria-labeledby") + " " + this.$toggle.attr('id')); this.$toggle .attr('aria-label', ''); } function initEvents() { var that = this; this.$element .on(getNamespacedEvents(['click']), function($event) { if($event.target !== that.$toggle[0] && !that.$toggle.has($event.target).length) { return; } if($(this).hasClass('in')) { that.close(); } else { that.open(); } }) .on(getNamespacedEvents(['keydown']), function($event) { var next = false; switch($event.keyCode) { case 13: if($(this).hasClass('in')) { that.close(); } else { that.open(); } break; case 9: if($event.target !== that.$element[0] ) { $event.preventDefault(); } case 27: that.close(); break; case 40: next = true; case 38: var $items = $(this) .children("ul:first") .find(":input, button, a"); var foundAt = $.inArray(document.activeElement, $items); if(next && ++foundAt === $items.length) { foundAt = 0; } else if(!next && --foundAt < 0) { foundAt = $items.length - 1; } $($items[foundAt]) .trigger('focus'); } }) .on(getNamespacedEvents(['focus']), 'a, button, :input', function() { $(this) .parents('li:last') .addClass('focused'); }) .on(getNamespacedEvents(['blur']), 'a, button, :input', function() { $(this) .parents('li:last') .removeClass('focused'); }) .on(getNamespacedEvents(['change']), ':checkbox', function() { if(that.$allToggle && $(this).is(that.$allToggle.find(':checkbox'))) { var allChecked = that.$allToggle .find(':checkbox') .prop("checked"); that.$element .find(':checkbox') .not(that.$allToggle.find(":checkbox")) .each(function(){ $(this).prop("checked", allChecked); $(this) .parents('li:last') .toggleClass('selected', $(this).prop('checked')); }); updateLabel.apply(that); return; } $(this) .parents('li:last') .toggleClass('selected', $(this).prop('checked')); var checkboxes = that.$element .find(":checkbox") .not(that.$allToggle.find(":checkbox")) .filter(":checked"); that.$allToggle.find(":checkbox").prop("checked", checkboxes.length === checkboxes.end().length); updateLabel.apply(that); }) .on(getNamespacedEvents(['mouseover']), 'ul', function() { $(this) .children(".focused") .removeClass("focused"); }); } function updateLabel() { var pluralize = function(wordSingular, count) { if(count !== 1) { switch(true) { case /y$/.test(wordSingular): wordSingular = wordSingular.replace(/y$/, "ies"); default: wordSingular = wordSingular + "s"; } } return wordSingular; } var $checkboxes = this.$element .find('ul :checkbox'); var allCount = $checkboxes.length; var checkedCount = $checkboxes.filter(":checked").length var label = checkedCount + " " + pluralize("item", checkedCount) + " selected"; this.$toggle .children("label") .text(checkedCount ? (checkedCount === allCount ? '(all)' : label) : 'Select a value'); this.$element .children('ul') .attr("aria-label", label + " of " + allCount + " " + pluralize("item", allCount)); } function ensureFocus() { this.$element .children("ul:first") .find(":input, button, a") .first() .trigger('focus') .end() .end() .find(":checked") .first() .trigger('focus'); } function addBackdrop() { if(this.$backdrop) { return; } var that = this; this.$backdrop = $("<div class='multi-select-backdrop'/>"); this.$element.append(this.$backdrop); this.$backdrop .on('click', function() { $(this) .off('click') .remove(); that.$backdrop = null; that.close(); }); } function open() { if(this.$element.hasClass('in')) { return; } this.$element .addClass('in'); this.$element .attr('aria-expanded', true) .attr('aria-haspopup', true); addBackdrop.apply(this); //ensureFocus.apply(this); } function close() { this.$element .removeClass('in') .trigger('focus'); this.$element .attr('aria-expanded', false) .attr('aria-haspopup', false); if(this.$backdrop) { this.$backdrop.trigger('click'); } } })(jQuery); $(document).ready(function(){ $('#multi-select-plugin') .MultiSelect(); });
 * { box-sizing: border-box; } .multi-select, .multi-select-plugin { display: inline-block; position: relative; } .multi-select > span, .multi-select-plugin > span { border: none; background: none; position: relative; padding: .25em .5em; padding-right: 1.5em; display: block; border: solid 1px #000; cursor: default; } .multi-select > span > .chevron, .multi-select-plugin > span > .chevron { display: inline-block; transform: rotate(-90deg) scale(1, 2) translate(-50%, 0); font-weight: bold; font-size: .75em; position: absolute; top: .2em; right: .75em; } .multi-select > ul, .multi-select-plugin > ul { position: absolute; list-style: none; padding: 0; margin: 0; left: 0; top: 100%; min-width: 100%; z-index: 1000; background: #fff; border: 1px solid rgba(0, 0, 0, 0.15); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); display: none; max-height: 320px; overflow-x: hidden; overflow-y: auto; } .multi-select > ul > li, .multi-select-plugin > ul > li { white-space: nowrap; } .multi-select > ul > li.selected > label, .multi-select-plugin > ul > li.selected > label { background-color: LightBlue; } .multi-select > ul > li.focused > label, .multi-select-plugin > ul > li.focused > label { background-color: DodgerBlue; } .multi-select > ul > li > label, .multi-select-plugin > ul > li > label { padding: .25em .5em; display: block; } .multi-select > ul > li > label:focus, .multi-select > ul > li > label:hover, .multi-select-plugin > ul > li > label:focus, .multi-select-plugin > ul > li > label:hover { background-color: DodgerBlue; } .multi-select.in > ul, .multi-select-plugin.in > ul { display: block; } .multi-select-backdrop, .multi-select-plugin-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 900; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <label id="multi-select-plugin-label" style="display:block;">Multi Select</label> <div id="multi-select-plugin" aria-labeledby="multi-select-plugin-label"> <span class="toggle"> <label>Select a value</label> <span class="chevron">&lt;</span> </span> <ul> <li> <label> <input type="checkbox" name="selected" value="0"/> Item 1 </label> </li> <li> <label> <input type="checkbox" name="selected" value="1"/> Item 2 </label> </li> <li> <label> <input type="checkbox" name="selected" value="2"/> Item 3 </label> </li> <li> <label> <input type="checkbox" name="selected" value="3"/> Item 4 </label> </li> </ul> </div>

Also you will see that gov.uk uses a checkbox pattern (within the organisation filter on the left on the linked page) for their multi-selects (with a filter - something you may consider with 100 different options as they have highlighted some key concerns in this article ).您还将看到gov.uk 使用复选框模式(在链接页面左侧的组织过滤器内)进行多选(使用过滤器 - 您可以考虑使用 100 个不同的选项,因为它们突出了一些关键问题在本文中)。

As you can see (and I wasn't finished) there is a lot to consider.正如你所看到的(我还没有完成),有很多事情需要考虑。

Hope I haven't scared you too much and the first few points solve the issue you originally asked about!希望我没有吓到您,前几点解决了您最初提出的问题!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM