Доступные сообщения валидации формы с помощью ARIA и Vue.js

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

Как программы чтения с экрана перемещаются по формам и читают их

Разработчик должен понимать различия между тем, как зрячий пользователь взаимодействует с формой, и как воспринимает ее пользователь программы чтения с экрана. Они используют «скрытое» измерение веб-страницы, известное как «дерево доступности». Accessibility tree - это структура, которая позволяет программам чтения с экрана получать информацию из браузера.

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

Доступ к сообщениям об ошибках и инструкциям

А как насчет доступа к сообщениям об ошибках валидации или инструкций для полей? Если поместить их в элемент, который не может быть выделен фокусом ввода (например, <div> или <p>), то программа чтения с экрана пропустит их. Как это исправить?

Способ 1: Что содержится в имени?

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

Для этого используется директива Vue v-show. Она скрывает сообщение, пока не возникнет ошибка валидации. В том числе и от программ чтения с экрана.

Способ 2: Использовать aria-describedby

Но что, если нужно, чтобы в элементе <label> не было сообщений об ошибках? В таком случае лучше использовать атрибуты ARIA. Они влияют на то, как экранные дикторы интерпретируют страницы по дереву доступности.

ARIA предоставляет атрибут, который позволяет разработчикам связать другие HTML-элементы с полями формы - aria-describedby. Для предоставления инструкций для поля формы добавьте к нему атрибут aria-describedby с id каждого элемента, который вы хотите связать с input.

<label for="first_name">First Name:</label>

<input id="first_name" type="text" aria-describedby="first_name-instructions">

<div id="first_name-instructions">maximum length 30 characters</div>

Теперь, когда мы явно связали дополнительные инструкции с полем, добавим сообщения об ошибках:

<div id="first_name-error">
Please enter a valid project name.
</div>

<label for="first_name">First Name:</label>

<div id="first_name-instructions">maximum length 30 characters</div>

<input id="first_name" name="first_name" type="text" aria-describedby="first_name-instructions first_name-error">

С помощью одного простого атрибута мы добавили сообщение об ошибке и связали его с полем формы.

Vue.js упрощает реализацию

Теперь нужно добиться, чтобы сообщение об ошибке не отображалось и читалось постоянно, а только когда возникла ошибка. Для этого мы используем библиотеку Vuelidate.

<div id="first_name-error" v-show="first_name.$error">
Please enter a valid project name.
</div>

<label for="first_name">First Name:</label>

<div id="first_name-instructions">maximum length 30 characters</div>

<input id="first_name" name="first_name" type="text" v-model="$v.first_name.$model" :aria-invalid="$v.first_name.$invalid" aria-describedby="first_name-instructions first_name-error">

Теперь у нас есть сообщение об ошибке, которое связано с полем ввода. И оно будет скрыто, если ошибка валидации не произошла.

Но так как мы используем v-show, то сообщение будет скрыто и от программ чтения с экрана. Здесь мы сталкиваемся с особенностью атрибута aria-describedby. По умолчанию связанный элемент будет считываться, даже если он будет скрыть.

Поэтому нам нужно сделать aria-describedby динамическим, чтобы он добавлял идентификатор сообщения об ошибке только тогда, когда она возникла. Это просто реализовать благодаря Vue.js:

signup-form.html

<div id="first_name-error" v-show="first_name.$error">
Please enter a valid first name
</div>

<label for="first_name">First Name:</label>

<div id="first_name-instructions">maximum length 30 characters</div>

<input id="first_name" name="first_name" type="text" v-model="$v.first_name.$model" :aria-invalid="$v.first_name.$invalid"  :aria-describedby="describedBy('first_name')">

main.js

methods: {

 
    // генерация связанных с aria-describedby идентификаторов 	

    describedBy(field) {

        const inst = `${field}-instructions`


        // field.$error - это логическое вычисляемое свойство, возвращаемое Vuelidate
        // если возникла ошибка, valErr становится равным идентификатору поля. В противном случае это пустая строка.

        const valErr = field.$error
        ? `${field}-error`
        : ''

    
        //обрезаем и заменяем двойные пробелы одиночными
        let refString =  ` $ {valErr} ${inst}`.replace(/s+/g,' ').trim()

        return refString

    }
 

    // базовый конструктор сообщений об ошибках

    vMessage(v, field) {

        let message = ''

        let errors = []


        if ($v.$error)) {

            // получаем тип ошибки из $params Vuelidate
            let errorTypeKeys = Object.keys($v["$params"])


            // составляем массив ошибок

            for (const key of errorTypeKeys) {
                if ($v[key] === false) {
                    errors.push(key)
                }
            }

        
            //создаем из массива разделенную запятыми строку

            let errorString = errors.length > 1
            ? errors.join(', ')
            : errors[0]

        
            // преобразуем в читаемое сообщение

            errorString = errorString
                .replace('required', 'This is a required field')
                .replace('url', 'The url is invalid')
                .replace('email', 'The email address is invalid')
                .replace('minLength', 'Input does not meet minimum length')

            message = `${errorString}.`
        }

        return messsage

    }

}

Теперь у нас есть динамический атрибут aria-describedby, который связан с выводом метода describedBy(). Он принимает имя поля в качестве параметра. Затем определяет, является ли ввод поля действительным, и возвращает список идентификаторов.

Если возникла ошибка и <input> выделен фокусом ввода, то атрибут aria-describedby будет ссылаться на сообщение об ошибке и на инструкции. В противном случае программа чтения с экрана объявит только инструкции (содержимое <label> будет зачитано в любом случае).

Некоторые предостережения

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

Например, и JAWS, и NVDA поддерживают режим формы (фокуса) и aria-describedby. А Voiceover поддерживает атрибут aria-describedby, но у него нет режима фокуса или формы. NVDA наиболее надежно работает с Firefox, а Voiceover - с Safari.

Нужно учитывать, что в дополнение к чтению скрытых связанных элементов, aria-describedby игнорирует семантику. Он считывает список связанных элементов как непрерывную строку.

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

Будущее: aria-errormessage

В конце 2017 года в ARIA 1.1 был добавлен атрибут aria-errormessage, предназначенный для сообщений об ошибках валидации. Когда атрибут получит поддержку программами чтения с экрана и браузерами, он  будет использоваться вместе с  aria-invalid, чтобы обеспечить более согласованный метод считывания сообщений об ошибке.

Тестирование

Единственный способ убедиться в правильной работе формы – это протестировать ее через программу для чтения с экрана.

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