AngularJS - программа чтения с экрана NVDA не находит имен дочерних элементов - PullRequest
0 голосов
/ 04 марта 2020

Извинения за пустые места HTML здесь ...

У меня есть 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>

Нам нужно, чтобы программа для чтения с экрана произносила вслух каждую опцию, поскольку она подсвечивается с помощью клавиш со стрелками. Как и сейчас, NVDA говорит «пусто» при вводе списка. Если в директиве, которую мы используем для создания HTML, я добавлю role="presentation" к <ul>, то NVDA будет читать весь список опций, как только откроется раскрывающийся список, но не отдельно для каждой клавиши со стрелкой. нажатие клавиши (и после нажатия Escape, чтобы заставить его перестать говорить, при наборе вариантов снова появляется «пусто»).

Я продолжаю думать, что роли listbox и option находятся в правильных местах, но что-то еще в структуре мешает программе чтения с экрана правильно найти значения?

1 Ответ

0 голосов
/ 04 марта 2020

Этот ответ получился довольно длинным, первые 3 балла, скорее всего, проблема, остальные - другие соображения / наблюдения

Есть несколько вещей, которые могут вызвать эту проблему , хотя не видя сгенерированный HTML, а не Angular Source, могут быть и другие.

Скорее всего, виновником является то, что ваши якоря недействительны. Вы не можете иметь пустой HREF (href=""), чтобы он был действительным. Глядя на свой исходный код, не могли бы вы удалить это и настроить CSS или изменить его на <div>?

Второй наиболее вероятный виновник в том, что role="option" должен быть на прямом потомке на role="listbox". Переместите его на <li> s и сделайте его выбираемым с помощью tabindex="-1" (см. Ниже пункт на tabindex="0"). (на самом деле, почему бы просто не удалить окружающие <div> и применить все ваши директивы angular непосредственно к <li>).

Третьим наиболее вероятным виновником является тот факт, что aria-label не требуется и может фактически мешать, программа чтения с экрана будет читать текст в вашем <span> без этого. Золотое правило - не используйте aria, если вы не можете изобразить информацию другим способом.

Вам также необходимо добавить aria-selected="true" (или false) к каждому <li role="option">, чтобы указать, выбран ли элемент или нет.

Также вы должны добавить aria-multiselectable="true" к <ul>, чтобы указать, что это множественный выбор.

Пока вы на нем, удалите атрибут title, он не Не добавляйте ничего полезного.

aria-activedescendant="id" следует использовать, чтобы указать, какой элемент в данный момент находится в фокусе.

Будьте осторожны с tabindex="0" - я не вижу, применяется ли это к все, но на самом деле это должно быть tabindex="-1", и вы программно управляете фокусом, иначе пользователи могут переходить к элементам, для которых они не предназначены. tabindex="0" должно быть на главном <ul>.

Из-за сложной природы множественного выбора вам будет намного лучше использовать группу флажков, поскольку они предоставляют множество функциональных возможностей бесплатно, но это это всего лишь предложение.

Следующий пример , который я нашел на 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>

Также вы увидите, что gov.uk использует шаблон флажка (внутри фильтра организации слева на связанном страница) для их множественного выбора (с фильтром - то, что вы можете рассмотреть с 100 различными вариантами, так как они выделили некоторые ключевые проблемы в этой статье ).

Как видите (и я еще не закончил), есть над чем подумать.

Надеюсь, я вас не сильно напугал, и первые несколько моментов решат проблему, с которой вы изначально столкнулись. спросил о!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...