javascript

Статус оплаты обновился, но скринридер об этом не сообщил

  • четверг, 12 ноября 2026 г. в 00:00:09
https://habr.com/ru/articles/1106536/

Разберём страницу оплаты: сначала появляется текст «Оплата подтверждена», затем — ссылка на закрытый Telegram-канал. Визуально всё работает. Но пользователь экранного диктора может не узнать об изменениях: фокус остался на кнопке, а обновлённый текст находится в другой части страницы.

Для таких сообщений подходит role="status". Эта роль задаёт live region с неявным aria-live="polite": сообщение может быть объявлено без переноса фокуса, когда скринридер сможет сделать это без прерывания текущего действия. Контейнер должен присутствовать на странице и иметь нужную роль до обновления текста. Рекомендации W3C

Сообщение о состоянии и ссылку лучше разделить:

<p id="payment-status" role="status" aria-atomic="true"></p>
<a id="channel-link" hidden>Перейти в Telegram-канал</a>

У role="status" уже есть неявное aria-atomic="true". Здесь атрибут указан явно: W3C рекомендует это из-за различий поддержки в некоторых окружениях. Он указывает, что при изменении следует представить сообщение целиком. Описание техники ARIA22

Когда сервер возвращает новое состояние заказа, обновляем интерфейс. Этот код должен выполняться после создания элементов — например, из скрипта, подключённого с defer:

const statusNode = document.querySelector("#payment-status");
const linkNode = document.querySelector("#channel-link");

function renderPayment({ paid, inviteUrl }) {
  const ready = paid && Boolean(inviteUrl);
  const message = !paid
    ? "Ожидаем подтверждения оплаты."
    : ready
      ? "Ссылка для входа готова. Она находится ниже."
      : "Оплата подтверждена. Готовим ссылку на канал.";

  if (ready) {
    linkNode.href = inviteUrl;
    linkNode.hidden = false;
  } else {
    linkNode.hidden = true;
    linkNode.removeAttribute("href");
  }

  if (statusNode.textContent !== message) {
    statusNode.textContent = message;
  }
}

Это фрагмент отображения трёх состояний: ожидаем оплату, оплата подтверждена, ссылка готова. paid — булево значение, inviteUrl — проверенная сервером ссылка или null. Функция получает актуальное состояние одного заказа от своего бэкенда. Сам возврат с платёжной страницы подтверждением оплаты не считается.

Отказы, возвраты и ошибки запроса здесь не показаны. В рабочем интерфейсе для них нужны отдельные состояния: ошибка получения статуса не означает, что платёж не прошёл.

Контейнер статуса остаётся в DOM. При повторном ответе сервера с тем же состоянием его текст не перезаписывается. Это убирает ненужные изменения live region; поведение озвучивания всё равно нужно проверять в выбранной связке браузера и скринридера.

Ссылка находится вне live region. Статус сообщает о результате, ссылка позволяет перейти к следующему действию. При её появлении код не перемещает фокус: пользователь продолжает работать там, где находился.

Для ручной проверки нужно разнести изменения во времени:

  1. Включить скринридер и оставить фокус на кнопке проверки оплаты.

  2. Передать состояние paid: true, inviteUrl: null.

  3. После объявления сообщения передать состояние с готовой ссылкой.

  4. Проверить, что новое сообщение доступно без переноса фокуса.

  5. Несколько раз передать тот же ответ и проверить отсутствие лишних объявлений.

  6. Перейти к ссылке клавишей Tab и открыть её.

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

В платёжном интерфейсе недостаточно обновить текст на экране. Изменение должно быть доступно пользователю, а следующий шаг — достижим с клавиатуры.