Именование
DialogContent не задаёт себе имя сам. Назвать диалог — ваша задача, и это обязательно: диалог без имени недоступен, потому что скринридеры объявляют его просто как «диалог».
Рекомендуемый паттерн:
- видимый заголовок с
id, на который ссылаетсяaria-labelledbyнаDialogContent aria-labelнаDialogContent, если видимого заголовка нетaria-describedbyна предложение, которое объясняет последствия, чтобы его объявили вместе с именем
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';Работа с клавиатурой
| Клавиша | Действие |
|---|---|
| EnterSpace | На триггере: открывает диалог. Фокус получает сам диалог или дочерний элемент с autoFocus. |
| TabShift + Tab | Переносит фокус на следующий или предыдущий элемент управления внутри диалога. Страница за ним инертна, и фокус на неё не попадает. |
| Escape | Закрывает диалог и возвращает фокус на элемент, который его открыл. С closedBy='none' ничего не делает. |
Во вложенных диалогах Escape закрывает только верхний.
Поведение фокуса
- при открытии фокус должен попадать в диалог
- при закрытии фокус должен возвращаться на осмысленный триггер
- в стеке вложенных диалогов восстановление фокуса должно оставаться предсказуемым
Управление фокусом обеспечивает нативный <dialog>: пока диалог открыт, остальная страница инертна, а при закрытии фокус возвращается на элемент, который был в фокусе до открытия. В Safari клик мышью не переводит фокус на кнопку, поэтому после закрытия диалога, открытого мышью, фокус возвращается на body страницы; при активации с клавиатуры он возвращается на триггер.
При открытии фокус получает сам диалог, поэтому ничего внутри не подсвечивается, а скринридеры объявляют его заголовок; Tab переводит фокус на первый контрол. Чтобы сфокусировать дочерний элемент, поставьте на него autoFocus — например, на первое поле формы.
Семантика для скринридеров
- браузер представляет диалог как
role=dialogсaria-modal, а имя он получает из вашегоaria-labelledbyилиaria-label - триггер выставляет
aria-haspopup='dialog'иaria-expanded - содержимое должно восприниматься как единый модальный блок
- контролы закрытия должны оставаться явными, когда закрытие важно; у
DialogCloseнет метки по умолчанию, поэтому кнопке-иконке внутри него нуженaria-label; библиотека не поставляет встроенных строк
Семантика формы
Если в диалоге есть поля, относитесь к нему как к обычной границе формы и перепроверяйте отправку с клавиатуры, закрытие и возврат фокуса в реальном сценарии.
Кнопки в <form method="dialog"> закрывают диалог через onChangeOpen(false), а value кнопки отправки становится returnValue диалога — как у нативного <dialog>. Это работает и с closedBy='none', когда Escape и подложка диалог не закрывают.
Риски доступности
- открытие диалога без
aria-labelledbyилиaria-label - вложенные диалоги без повторной проверки восстановления фокуса
- длинное содержимое в диалоге без рабочей стратегии прокрутки
- закрытие важных сценариев только кликом снаружи