Инженерная практика

Диалог в интерфейсе: фокус, Escape и доступное закрытие

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

Посетитель открывает окно с требованиями к проекту. Визуально оно поверх страницы, но после Tab фокус уходит к ссылке в футере, скринридер не сообщает название окна, а закрыть его на телефоне можно только случайным нажатием на фон. Это проблемы пользовательского пути: красивое затемнение ещё не создаёт модальный диалог.

Сначала решите, нужно ли модальное окно

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

Перед реализацией запишите три действия: откуда окно открывается, что человек должен сделать внутри и куда возвращается после закрытия. Для подтверждения удаления начальный фокус на безопасном действии может быть полезнее, чем на разрушительной кнопке. Для большого информационного сообщения сначала важен контекст. Решение зависит от содержания, поэтому правило «всегда первый input» слишком грубое.

Минимальный пример с нативным dialog

Этот пример можно сохранить в отдельный HTML-файл. Кнопка открывает окно через showModal. Название связано с заголовком через aria-labelledby. Форма method="dialog" закрывает окно без сетевого запроса. В небольшом информационном окне начальный фокус направлен на видимую кнопку закрытия: посетитель получает понятный выход и может прочитать короткий текст перед ней.

TSX<button id="open-review" type="button">Требования к ревью</button> <dialog id="review-dialog" aria-labelledby="review-title"> <h2 id="review-title">Что подготовить к ревью</h2> <p>Ссылка на проект, команда запуска и один конкретный вопрос.</p> <form method="dialog"> <button id="close-review" autofocus>Вернуться к странице</button> </form> </dialog> <script> const opener = document.querySelector('#open-review'); const dialog = document.querySelector('#review-dialog'); opener.addEventListener('click', () => { if (!dialog.open) dialog.showModal(); }); </script>

Вызов showModal даёт браузеру информацию о модальности. Фон становится inert, а окно появляется в верхнем слое. Если просто изменить CSS на display: block или добавить open, такой же модальности не получится. Не добавляйте tabindex к самому dialog. Нативный элемент уже предоставляет нужную семантику; дополнительные атрибуты должны объяснять контент, а не дублировать её.

Escape, cancel и возврат к инициатору

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

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

Интеграция с React

В React храните ссылку на реальный HTMLDialogElement и открывайте его в обработчике действия либо синхронизируйте с состоянием через эффект. Если состояние приложения считает окно открытым, а Escape уже закрыл DOM-элемент, эти два источника расходятся. Обработайте close и приведите состояние в соответствие. Не записывайте open в JSX одновременно с независимыми вызовами showModal: сначала выберите, какая часть кода управляет жизненным циклом.

Длинный текст, прокрутка и маленький экран

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

Затемнение ::backdrop помогает обозначить контекст, но должно оставлять текст самого окна читаемым. Анимацию можно добавить после проверки основного сценария. Учитывайте prefers-reduced-motion и не откладывайте установку фокуса до окончания долгого перехода: клавиатурное действие должно иметь понятный результат сразу.

Проверка, которая оценивает весь путь

  1. Сфокусируйте кнопку открытия клавиатурой и нажмите Enter. Проверьте видимое окно, доступное имя и начальный фокус внутри.
  2. Нажимайте Tab и Shift+Tab. Ссылки на фоне не должны становиться активным местом взаимодействия.
  3. Закройте окно Escape. Фокус должен вернуться к инициатору, если он ещё доступен.
  4. Откройте снова и закройте видимой кнопкой. Проверьте этот путь на сенсорном устройстве.
  5. Повторите несколько открытий и закрытий. Убедитесь, что нет лишних обработчиков и исключений InvalidStateError.
  6. Проверьте масштаб, длинный текст и чтение скринридером. Автоматический аудит не показывает весь опыт взаимодействия.

Для теста возврата фокуса сначала явно сфокусируйте инициатор. Это устраняет зависимость от различий браузеров при клике мышью. Проверяйте activeElement и доступное имя, а не только наличие атрибута open. Для собственного div-диалога обязанностей больше: семантика, inert-фон, управление фокусом и клавиатурой требуют отдельной реализации и проверки.

Документация и следующая задача

Нативные методы и ограничения: MDN - dialog. Рекомендации для начального фокуса и возврата: WAI-ARIA - Dialog Modal Pattern. Проверьте пример в поддерживаемых браузерах своего продукта: эта статья не обещает одинакового поведения всех ассистивных технологий.

Следующая задача - доступная форма внутри окна. Подумайте, где показывается ошибка, как сохраняется ввод и как вернуться к её полю. Для обсуждения решения используйте критерии frontend-проекта и программу менторства по React.

Вопросы по теме

Делает ли атрибут open диалог модальным?
Нет. Для модального поведения используйте showModal. Сам по себе open отображает немодальный диалог.
Нужно ли вручную блокировать Tab вокруг нативного dialog?
Для модального dialog браузер управляет модальностью и делает фон inert. Проверьте поведение целевых браузеров; не добавляйте второй focus trap без конкретной причины.
Достаточно ли закрытия по Escape?
Нет. Пользователю на сенсорном устройстве или без физической клавиатуры нужна видимая кнопка закрытия.

Читайте также

Все статьи

Обсудим вашу подготовку?

Напишите, что уже умеете и с чем возникли трудности. Если есть проект, пришлите ссылку на него.

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

Что не получается, что уже пробовали. От 100 знаков, сейчас 0.
Где работали или учились, сколько лет, какой стек, сколько откликов и собеседований. От 100 знаков, сейчас 0.
PDF или DOCX, до 5 МБ. Файл видит только администратор.
GitHub, hh, LinkedIn, сайт. Можно несколько, через пробел или с новой строки.