Перейти к содержанию

Доступный интерфейс переписки

Оглавление серии

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

Состояние stages/15 содержит полный HTML, CSS и клиент предыдущих шагов. Браузер и вспомогательные технологии при подготовке не запускались, статус not_run. Поэтому ниже представлены ожидаемые эффекты и сценарии самостоятельной проверки, а не заявление о пройденном аудите совместимости. Для реального выпуска понадобится проверка в целевых сочетаниях браузера, клавиатуры и программы чтения экрана.

Подпись принадлежит полю

Элементы label связаны с select и textarea через for и id. Видимая подпись сохраняется после ввода, в отличие от placeholder, который исчезает. Человек понимает назначение каждого элемента и может нажать подпись, чтобы перейти к полю. Стандартные button и select уже имеют клавиатурное поведение, поэтому не создаём кликабельные div, которым пришлось бы вручную добавлять роль, фокус и обработку клавиш.

Полный public/index.html задаёт структуру страницы. Элемент main выделяет основной материал, а заголовок внутри относится к учебному приложению; это не заголовок самой статьи ProfessorWeb.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Линия — учебный чат</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <main>
    <h1>Линия</h1>
    <p>Только локальные вымышленные участники; вход не проверяет личность.</p>
    <form id="session">
      <label for="actor">Учебный участник</label>
      <select id="actor">
        <option value="alice">Алиса, обращение 17</option>
        <option value="bob">Борис, обращение 18</option>
        <option value="agent">Оператор</option>
      </select>
      <button>Открыть сессию</button>
    </form>
    <label for="room">Обращение</label>
    <select id="room" disabled></select>
    <p id="status" role="status" aria-live="polite">Сессия не открыта</p>
    <p id="presence"></p>
    <ol id="messages" aria-label="Переписка"></ol>
    <p id="announcement" class="sr-only" aria-live="polite"></p>
    <form id="composer">
      <label for="text">Сообщение</label>
      <textarea id="text" maxlength="1000" required></textarea>
      <button id="send" disabled>Отправить</button>
    </form>
    <p id="pending" role="status"></p>
  </main>
  <script type="module" src="/client.js"></script>
</body>
</html>

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

Состояние должно быть слышно и видно

Абзац status имеет роль status и aria-live="polite". Его текст меняется при открытии, входе в комнату, закрытии и отклонении команды. Вежливое объявление позволяет вспомогательной технологии сообщить изменение без немедленного прерывания текущего чтения. При этом тот же текст остаётся видимым всем пользователям. Цвет или состояние disabled кнопки не являются единственным источником информации о происходящем.

В журнале сохраняем обычный список. Не помечаем всю историю как непрерывную живую область: при входе сервер может вернуть двести записей, и их последовательное объявление затруднит работу. Для новой живой реплики используется отдельный короткий абзац announcement, визуально скрытый CSS. Клиент обновляет его только для поступившего события, а showMessage(message, false) при загрузке истории не создаёт десятки одинаковых объявлений.

Полный файл public/style.css оставляет текст и элементы обычными, добавляет заметный фокус и класс для доступного скрытого уведомления.

body { margin: 0; font: 1rem/1.5 system-ui, sans-serif; color: #172b3a; }
main { max-width: 42rem; margin: 2rem auto; padding: 0 1rem; }
label, textarea { display: block; }
textarea { box-sizing: border-box; width: 100%; min-height: 6rem; }
button, select, textarea { font: inherit; margin: .5rem 0; }
button { padding: .4rem .8rem; }
:focus-visible { outline: 3px solid #0758a5; outline-offset: 3px; }
#messages { min-height: 5rem; padding-left: 1.8rem; overflow-wrap: anywhere; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

Класс sr-only скрывает небольшой участок визуально, сохраняя его в доступном представлении. Не используем display: none, поскольку такой элемент обычно исключается из дерева доступности и live-область не сможет сообщить изменение. Сам скрытый текст должен оставаться кратким: полную реплику человек может прочитать в журнале, а уведомление сообщает автора и факт появления нового сообщения.

Фокус следует действию человека

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

При смене комнаты интерфейс очищает журнал и временно отключает отправку до joined. Причина ожидания объясняется текстом, поэтому пользователь с клавиатурой не должен угадывать, почему кнопка стала недоступной. Native disabled исключает кнопку из обычной последовательности фокуса; это нормальное поведение при явно сообщённом ожидании, но не способ бесследно спрятать ошибку. Для исправимой ошибки ввода нужны понятная причина и доступное поле.

Состояние pending показывается отдельным текстом. В учебном интерфейсе оно сообщает число ожидающих подтверждения или наличие уведомления SSE. В полноценном чате лучше разделить эти значения, поскольку они относятся к разным задачам и могут перезаписать друг друга. Доступное сообщение должно быть точным не только по роли ARIA, но и по предметному смыслу: надпись «Есть обновление» не заменяет объяснение неопределённой отправки.

Сценарий чтения и восстановления

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

Затем рассмотрите разрыв связи. Кнопка выключается, статус сообщает закрытие, при восстановлении приходит история. Старые реплики не объявляются как новые, поскольку загрузка вызывает showMessage с false. Новое живое сообщение после joined вызывает короткое announcement. Такой сценарий важнее проверки одного статического HTML-файла: доступность зависит от порядка событий, а не только от наличия label в исходнике.

Повторяемый текст объявления может не всегда восприниматься вспомогательной технологией как новое изменение, если два события от одного автора имеют одинаковую строку. Для production можно включить различимый номер, краткий фрагмент сообщения либо управлять очисткой live-области. Решение нужно проверять с целевыми средствами чтения, а не считать гарантированным по одной записи aria-live. Учебный вариант показывает место механизма и его границу.

Длинные слова и ссылки могут расширять строку, поэтому журнал использует overflow-wrap: anywhere. Поле имеет ширину контейнера и минимальную высоту, но остаётся обычной textarea с доступным вводом перевода строки. Не перехватываем Enter без явного договора: человек может ожидать новую строку. Отправка выполняется стандартной кнопкой формы, а изменение горячих клавиш требует понятной подсказки и согласованного поведения.

Семантика main рассмотрена в ARIA Authoring Practices, а сообщения состояния — в WAI-ARIA 1.2. После этого состояния технические переходы имеют видимое объяснение и доступные сигналы. Остаётся сформулировать остановку ресурсов и пределы, в которых этот локальный чат можно сопровождать.