Поддержка screen readers

Поддержка экранных считывателей в масках ввода — один из наиболее сложных аспектов фронтенд-разработки, поскольку такие библиотеки, как Cleave.js, изменяют отображаемое значение поля без явного изменения пользовательского намерения. Это создает разрыв между тем, что видит пользователь, и тем, что интерпретирует assistive technology.

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


Поведение Cleave.js с точки зрения screen readers

Screen readers работают с DOM-значением input-элемента и событиями, такими как input, change, focus, blur. Cleave.js перехватывает ввод и модифицирует значение поля, что приводит к нескольким особенностям:

  • значение в DOM может отличаться от того, что вводит пользователь
  • каретка может перемещаться программно
  • форматирование происходит синхронно с вводом
  • дополнительные символы (пробелы, дефисы) появляются автоматически

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


Основная проблема доступности при маскировании ввода

Ключевая проблема заключается в том, что screen reader озвучивает уже отформатированное значение, а не «сырое» пользовательское представление.

Пример:

Пользователь вводит:

79991234567

Cleave.js отображает:

+7 (999) 123-45-67

Screen reader может озвучивать каждое изменение как:

  • «плюс семь открывающая скобка девять девять девять…»

Это приводит к перегрузке восприятия, особенно при быстром наборе.


ARIA-стратегии для совместимости

Cleave.js не добавляет ARIA-атрибуты автоматически, поэтому разработчик должен управлять ими вручную.

aria-label и aria-describedby

Для корректного описания поля важно явно задавать смысл вводимого значения:

<input
  id="phone"
  type="tel"
  aria-label="Номер телефона"
  aria-describedby="phone-help"
/>

<div id="phone-help">
  Введите номер в международном формате
</div>

Важно учитывать, что screen reader будет озвучивать уже форматированное значение, поэтому описание должно объяснять не формат, а цель поля.


Управление частыми обновлениями значения

Cleave.js обновляет значение input на каждое изменение. Для screen reader это может приводить к многократному озвучиванию одного и того же поля.

Решение заключается в минимизации влияния промежуточных изменений.

Использование aria-live (ограниченно)

Прямое применение aria-live к input нежелательно, но может использоваться для внешних отображений:

<input id="card" type="text" aria-label="Номер карты" />

<div aria-live="polite" id="card-display"></div>
const input = document.getElementById('card');
const display = document.getElementById('card-display');

const cleave = new Cleave(input, {
  creditCard: true,
  onValueChanged: function (e) {
    display.textContent = e.target.value;
  }
});

Такой подход отделяет визуальное форматирование input от озвучиваемого текста.


Разделение “сырых” и “форматированных” данных

Один из наиболее стабильных подходов к доступности — разделение представлений:

  • input содержит форматированное значение (Cleave.js)
  • отдельный скрытый или вспомогательный элемент содержит «логическое» значение

Пример реализации

<input id="phone" type="tel" aria-label="Телефон" />

<input
  type="hidden"
  id="phone-raw"
  name="phone_raw"
/>
const input = document.getElementById('phone');
const hidden = document.getElementById('phone-raw');

const cleave = new Cleave(input, {
  phone: true,
  phoneRegionCode: 'RU',
  onValueChanged: function (e) {
    hidden.value = e.target.rawValue;
  }
});

Screen readers взаимодействуют только с визуальным input, но форма отправляет чистое значение.


Контроль озвучивания во время ввода

В некоторых интерфейсах важно снизить количество перерисовок, чтобы screen reader не реагировал на каждое изменение.

Подходы:

1. Debounce обновлений вспомогательных элементов

let timeout;

onValueChanged: function (e) {
  clearTimeout(timeout);

  timeout = setTimeout(() => {
    display.textContent = e.target.value;
  }, 200);
}

Это уменьшает частоту обновлений, которые могут быть озвучены assistive tools.


2. Использование aria-hidden для декоративных элементов

Если Cleave.js используется вместе с визуальными масками:

<span aria-hidden="true" id="formatted-value"></span>

Это предотвращает двойное чтение одного и того же значения.


Поддержка клавиатурной навигации

Screen reader всегда работает в связке с клавиатурой, поэтому поведение каретки критично.

Cleave.js иногда изменяет позицию курсора после вставки форматирующих символов. Это может нарушать привычную модель навигации.

Рекомендации:

  • избегать чрезмерно агрессивного форматирования в onValueChanged
  • не модифицировать значение при каждом keydown, если возможно
  • использовать встроенные настройки Cleave.js вместо кастомной логики

Проблема повторного чтения значения

Некоторые screen readers воспринимают обновление value как новое событие и повторно озвучивают поле.

Это особенно заметно при:

  • вставке текста
  • автозаполнении
  • программных изменениях value

Минимизация эффекта

Использование стабилизации значения:

let lastValue = '';

onValueChanged: function (e) {
  if (e.target.value === lastValue) return;
  lastValue = e.target.value;
}

ARIA-invalid и валидация вместе с Cleave.js

При использовании Cleave.js вместе с валидацией важно корректно синхронизировать состояние ошибки.

<input id="card" aria-invalid="false" aria-describedby="card-error" />
<div id="card-error" role="alert"></div>
function validate(value) {
  return value.replace(/\s/g, '').length === 16;
}

onValueChanged: function (e) {
  const isValid = validate(e.target.rawValue);

  input.setAttribute('aria-invalid', String(!isValid));
  error.textContent = isValid ? '' : 'Неверный номер карты';
}

Screen readers должны получать только итоговое состояние, а не промежуточные изменения.


Особенности работы с автозаполнением

Browser autofill может вставлять значение уже в форматированное поле, после чего Cleave.js перерасчитывает его.

Это вызывает резкое обновление DOM, которое screen reader может озвучить как новое поле.

Подход:

  • обрабатывать onChange после autofill
  • проверять стабильность значения
  • избегать повторной инициализации Cleave.js на одном input

Совместимость с assistive technologies

При корректной настройке Cleave.js остаётся совместимым с:

  • NVDA
  • JAWS
  • VoiceOver

Однако поведение зависит от частоты обновления DOM и количества форматирующих символов.

Ключевая цель — не изменять семантику поля, а только его визуальное представление.


Практика минимально инвазивной интеграции

Наиболее устойчивый подход к screen reader совместимости заключается в следующей архитектуре:

  • Cleave.js отвечает только за визуальное форматирование
  • aria-атрибуты описывают смысл, а не формат
  • raw value хранится отдельно
  • обновления вспомогательных элементов минимизированы
  • DOM изменения не должны дублировать одну и ту же информацию

Такая модель снижает шум в озвучивании и сохраняет предсказуемость интерфейса для assistive технологий.