Главная » Статьи » Как создать доступный компонент с автокомплитером с помощью Vue.js

Как создать доступный компонент с автокомплитером с помощью Vue.js

Как создать доступный компонент с автокомплитером с помощью Vue.js

От автора: помните, мы создавали во Vue js компоненты с автокомплитером? Его могут использовать большинство людей, но не люди с ограниченными возможностями, которые используют вспомогательные технологии. Это потому что мы не сделали его семантическим для этих технологий, чтобы они понимали, что наш компонент это больше чем просто поле ввода. В этой статье мы узнаем, как с помощью ARIA атрибутов сделать автокомплитер полностью доступным.

Accessible Rich Internet Applications (ARIA)

Вы когда-нибудь пытались перемещаться по сайту с помощью вспомогательных технологий? В большинстве ОС есть встроенные решения. В MacOS можно открыть VoiceOver нажатием cmd + F5, в Windows можно запустить Narrator нажатием Win + Ctrl + Enter.

Если запустить одно из средств, описанных выше, и перейти на этот компонент с автокомплитером, то нам скажут, что это просто текстовое поле (нам не объявят варианты).

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

Лейблы имеют значение

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

Быстро настроим наш компонент в application и используем VoiceOver для взаимодействия.

<template>
<div id="app"> <div> <label>Choose a fruit:</label> <autocomplete :items="[ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']" /> </div>
</div>
</template>

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

Если добавить атрибуты aria-label или aria-labelledby, мы дадим пользователю знать, для чего предназначено поле.

Давайте добавим prop для атрибута aria-labelledby автокомплитера. Вы можете использовать aria-label, но так как у большинства компонентов с автокомплитером лейбл расположен рядом, я использую это:

<script>
export default { ... props { ... ariaLabelledBy: { type: String, required: true, }, };
};
</script>
<template> ... <input type="text" v-model="search" @input="onChange" :aria-labelledby="ariaLabelledBy" /> ...
</template>

Я сделал атрибут обязательным, чтобы его нельзя было забыть добавить. Если в вашем приложении нет компонентов, вокруг которых есть лейбл, то умнее будет использовать атрибут aria-label.

Нужно лишь добавить id к лейблу и предоставить его как prop:

<template>
<div id="app"> <div> <label id="fruitLabel">Choose a fruit:</label> <autocomplete :items="[ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']" aria-labelled-by="fruitlabel" /> </div>
</div>
</template>

Теперь вспомогательное ПО может сказать нам, что поле ожидает выбора фруктов:

ARIA атрибуты

Хотя лейблы и могут значительно улучшить юзабилити, их не достаточно. Пользователь все еще не знает, что это поле с автокомплитером. Чтобы это понять, нам понадобятся другие атрибуты ARIA.

Начнем с объяснения работы атрибута role.

Роли определяют тип элемента. По ссылке можно посмотреть все типы ролей.

Для автокомплитера больше подходит combobox: «Комбинированный виджет, содержащий однострочный текстовый блок и другой элемент, например, список или сетку, которые могут динамически появляться, чтобы помочь пользователю выбрать значение текстового блока.»

Так как при вводе текста наш компонент будет показывать список возможных значений, нам необходимо задать атрибут aria-autocomplete на текстовом элементе.

Атрибут aria-autocomplete принимает 3 значения. Значение inline делает так, что автозавершение значения проходит внутри текстового поля. Значение list делает так, что предлагаемые значения будут представлены в отдельном теге, который будет отображаться рядом с текстовым полем. Значение both отображает список значений, в котором одно значение выбирается автоматически и отображается внутри текстового поля.

Наш список вариантов находится в отдельном теге, поэтому мы возьмем значение list.

Сам атрибут никак не узнает, где наш список значений в документе. Это нужно определить через атрибут aria-controls.

Также необходимо идентифицировать автокомплитер через атрибут aria-haspopup, а к контейнеру добавить aria-expanded, когда виден список результатов.

Последнее, но не менее важное: необходимо добавить атрибут role к input со значением searchbox, ul со значением listbox и всем li со значением role.

С этими атрибутами вспомогательное ПО может понять, что мы представляем пользователю комбобокс со списком предлагаемых значений.

<template> <div class="autocomplete" role="combobox" aria-haspopup="listbox" aria-owns="autocomplete-results" :aria-expanded="isOpen" > <input type="text" @input="onChange" v-model="search" @keyup.down="onArrowDown" @keyup.up="onArrowUp" @keyup.enter="onEnter" aria-multiline="false" role="searchbox" aria-autocomplete="list" aria-controls="autocomplete-results" aria-activedescendant="" :aria-labelledby="ariaLabelledBy" /> <ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results" role="listbox" > <li class="loading" v-if="isLoading"> Loading results... </li> <li v-else v-for="(result, i) in results" :key="i" @click="setResult(result)" class="autocomplete-result" :class="{ 'is-active': i === arrowCounter }" role="option" > </li> </ul> </div>
</template>

Поддержка стрелок

Помните, как мы добавили поддержку клавиатуры в компонент с автокомплитером? Стрелками также нужно управлять через атрибуты ARIA.

Чтобы вспомогательное ПО знало, какое значение выбрано при использовании стрелок, необходимо установить 2 атрибута:

Атрибут aria-activedescendant должен быть задан на input, он будет хранить ID опции.

Атрибут aria-selected должен быть задан на li в опцию, визуально выделенную как выбранную.

Очень важно обновить обработчики в компоненте, чтобы вспомогательное ПО правильно определяло активную опцию. Нам необходимо следить за событием keydown, а не keyup.

Весь код представлен ниже или же его можно посмотреть в этом codepen.

<script> export default { name: 'autocomplete', props: { items: { type: Array, required: false, default: () => [], }, isAsync: { type: Boolean, required: false, default: false, }, ariaLabelledBy: { type: String, required: true } }, data() { return { isOpen: false, results: [], search: '', isLoading: false, arrowCounter: 0, activedescendant: '' }; }, methods: { onChange() { this.$emit('input', this.search); if (this.isAsync) { this.isLoading = true; } else { this.filterResults(); } }, filterResults() { this.results = this.items.filter((item) => { return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1; }); }, setResult(result) { this.search = result; this.isOpen = false; }, onArrowDown(evt) { if (this.isOpen) { if (this.arrowCounter < this.results.length) { this.arrowCounter = this.arrowCounter + 1; this.setActiveDescendent(); } } }, onArrowUp() { if (this.isOpen) { if (this.arrowCounter > 0) { this.arrowCounter = this.arrowCounter -1; this.setActiveDescendent(); } } }, onEnter() { this.search = this.results[this.arrowCounter]; this.arrowCounter = -1; }, handleClickOutside(evt) { if (!this.$el.contains(evt.target)) { this.isOpen = false; this.arrowCounter = -1; } }, setActiveDescendant() { this.activedescendant = this.getId(this.arrowCounter); }, getId(index) { return `result-item-${index}`; }, isSelected(i) { return i === this.arrowCounter; }, }, watch: { items: function (val, oldValue) { // actually compare them if (val.length !== oldValue.length) { this.results = val; this.isLoading = false; } }, }, mounted() { document.addEventListener('click', this.handleClickOutside) }, destroyed() { document.removeEventListener('click', this.handleClickOutside) } };
</script>
</script>
<template> <div class="autocomplete" role="combobox" aria-haspopup="listbox" aria-owns="autocomplete-results" :aria-expanded="isOpen" > <input type="text" @input="onChange" @focus="onFocus" v-model="search" @keydown.down="onArrowDown" @keydown.up="onArrowUp" @keydown.enter="onEnter" role="searchbox" aria-autocomplete="list" aria-controls="autocomplete-results" :aria-labelledby="ariaLabelledBy" :aria-activedescendant="activedescendant" /> <ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results" role="listbox" > <li class="loading" v-if="isLoading" > Loading results... </li> <li v-else v-for="(result, i) in results" :key="i" @click="setResult(result)" class="autocomplete-result" :class="{ 'is-active': isSelected(i) }" role="option" :id="getId(i)" :aria-selected="isSelected(i)" > </li> </ul> </div>
</template>

Шпаргалка по доступности автокомплитера

Шпаргалка по всем ARIA атрибутам, необходимым для доступности автокомплитера.

Источник: https://alligator.io/

Редакция: Команда webformyself.