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

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

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

Accessible Rich Internet Applications (ARIA)

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

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

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

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

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

Фреймворк VUE JS: быстрый старт, первые результаты

Получите бесплатный курс и создайте веб-приложение на трендовой Frontend-технологии VUE JS с полного нуля

Узнать подробнее

Быстро настроим наш компонент в 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: «Комбинированный виджет, содержащий однострочный текстовый блок и другой элемент, например, список или сетку, которые могут динамически появляться, чтобы помочь пользователю выбрать значение текстового блока.»

Фреймворк VUE JS: быстрый старт, первые результаты

Получите бесплатный курс и создайте веб-приложение на трендовой Frontend-технологии VUE JS с полного нуля

Узнать подробнее

Так как при вводе текста наш компонент будет показывать список возможных значений, нам необходимо задать атрибут 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.

Фреймворк VUE JS: быстрый старт, первые результаты

Получите бесплатный курс и создайте веб-приложение на трендовой Frontend-технологии VUE JS с полного нуля

Узнать подробнее

VUE JS. Быстрый старт

Практический курс по созданию веб-приложения на VUE JS с полного нуля

Получить

Метки:

Похожие статьи:

Комментарии Вконтакте:

Комментарии Facebook:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree