Диалоговое окно

Обзор

Используйте нативный элемент <dialog> для модальных и немодальных диалоговых окон. Он обеспечивает семантику специальных возможностей, обработку фокуса, рендеринг верхнего слоя и обработку Esc, которые пользовательские оверлеи должны перестраивать вручную.

Выберите тип оверлея

Выбрать первое подходящее правило. Если вы не уверены, используйте правило 1.

Правило 1. Оверлей прерывает действия пользователя и блокирует страницу (по умолчанию)

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

Используйте <dialog>, открытый как модальное окно, декларативно с command="show-modal" (см. Декларативное управление диалоговыми и всплывающими окнами) или из скрипта с showModal().

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

Браузер предоставляет: инертную страницу, ограничение фокуса, верхний слой, ::backdrop, Esc и закрытие жестом назад, а также возврат фокуса к вызывающему объекту.

  • НЕ добавляйте role="dialog", aria-modal, ловушку фокуса, переключение inert или обработчик Esc.
  • НЕ открывайте его с помощью тега show() или атрибута open.
  • РЕКОМЕНДУЕТСЯ останавливать прокрутку страницы за объявлением с помощью приведенного ниже кода CSS. Подробнее о том, как запретить прокрутку страницы под модальным диалоговым окном…
  • НЕ блокируйте прокрутку с помощью overflow: hidden или position: fixed в html или body.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

Не используйте для меню и подсказок (см. правило 3).

Правило 2. Пользователь продолжает работать на странице, пока панель открыта

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

Используйте <dialog popover> для панели верхнего слоя с закрытием всплывающего окна (auto закрывается при нажатии за пределами окна, manual – нет). Используйте dialog.show(), только если панель не должна находиться на верхнем слое.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

Страница остается интерактивной и не становится инертной.

  • РЕКОМЕНДУЕТСЯ добавлять видимую кнопку закрытия.
  • НЕ используйте showModal(), который блокирует страницу.

Не используйте для подтверждения блокировки (см. правило 1).

Правило 3. Оверлей не является диалоговым окном

Используется для меню, раскрывающихся списков, подсказок, всплывающих карточек и всплывающих уведомлений.

Используйте семантический элемент с атрибутом popover, а не <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

Выберите, как можно закрыть баннер

Значение по умолчанию зависит от того, как открыто диалоговое окно. Если правило closedby не задано или задано неверно, то showModal() работает как closerequest, а любой другой способ открытия – как none.

  • Только клавиша Esc и жест "Назад" (по умолчанию для модальных окон): ничего не задавайте или укажите closedby="closerequest".
  • Закрытие при нажатии за пределами окна. Используйте closedby="any".
  • Закрыть может только разработчик (например, форму с несохраненными изменениями). Используйте closedby="none" и добавьте подтверждение. См. раздел "Закрытие" ниже.

Правила

Доступное название и начальный фокус

  • РЕКОМЕНДУЕТСЯ присваивать название каждому диалоговому окну с помощью атрибута aria-labelledby, указывающего на заголовок, или aria-label, если видимого заголовка нет.
  • РЕКОМЕНДУЕТСЯ устанавливать атрибут autofocus для основного элемента управления или первого поля.
  • РЕКОМЕНДУЕТСЯ использовать autofocus для безопасного действия (например, "Отмена" или "Сохранить") в подтверждении необратимого действия.
  • НЕ пишите код, который возвращает фокус при закрытии. Браузер возвращает его вызывающей стороне.

От центра

Заключение

  • РЕКОМЕНДУЕТСЯ закрывать объявление с помощью кнопок <form method="dialog"> или command="close".
  • РЕКОМЕНДУЕТСЯ тщательно выбирать closedby: any (закрытие при нажатии за пределами окна и Esc), closerequest (только Esc, по умолчанию для модальных окон) или none (только закрытие, управляемое разработчиком).
  • ИСПОЛЬЗУЙТЕ command="request-close" или requestClose() для кнопок отмены, которые должны учитывать логику несохраненных изменений. Сначала срабатывает cancel, а closedby игнорируется.
  • РЕКОМЕНДУЕТСЯ использовать closedby как прогрессивное улучшение. Esc и жест "Назад" на мобильных устройствах уже закрывают модальные окна.

Зацепление за крюк

  • РЕКОМЕНДУЕТСЯ считывать dialog.returnValue в прослушивателе close, чтобы узнать, какая кнопка была нажата, а затем сбрасывать ее с помощью dialog.returnValue = ''. Esc и другие подобные запросы не задают значение, а браузеры по-разному обрабатывают очистку предыдущего значения, поэтому без сброса может произойти утечка устаревшего значения. Считать, что пользователь '', если он закрыл окно, не выбрав ни один из вариантов.
  • РЕКОМЕНДУЕТСЯ использовать cancel для перехвата Esc и request-close, но не полагаться на preventDefault() для повторных нажатий Esc без активации пользователем.
  • РЕКОМЕНДУЕТСЯ использовать closedby="none" с явным шагом подтверждения для форм с несохраненными изменениями.
  • РЕКОМЕНДУЕТСЯ сообщать пользователю, почему диалоговое окно осталось открытым, если действие cancel было заблокировано. Для этого покажите видимое сообщение и сошлитесь на него с помощью атрибута aria-describedby из элемента управления, который получает фокус (например, "Отменить"), чтобы программа чтения с экрана прочитала его при перемещении фокуса. Без него при переходе к новой кнопке будет озвучиваться только ее название.
  • НЕ допускайте, чтобы элементы управления перемещались под указатель. Зарезервируйте место для сообщения (скройте его с помощью visibility, а не display или hidden) и добавьте кнопку удаления так, чтобы существующие кнопки не перемещались (например, первой в строке с выравниванием по правому краю). Тогда второй клик в том же месте не приведет к отмене изменений.

Немодальные диалоговые окна

  • РЕКОМЕНДУЕТСЯ использовать <dialog popover> (auto или manual) для панели верхнего слоя и dialog.show(), если верхний слой не нужен.
  • РЕКОМЕНДУЕТСЯ добавлять видимую кнопку закрытия в каждое немодальное диалоговое окно.

Фон и стиль

Специальные возможности

  • НЕ ДОБАВЛЯЙТЕ role="dialog", aria-modal, ловушку фокуса или переключатель inert в нативное модальное окно.
  • РЕКОМЕНДУЕТСЯ использовать стандартный элемент <button> с доступным названием для элементов управления закрытием.
  • РЕКОМЕНДУЕТСЯ использовать один уровень заголовков для названий диалоговых окон.

Резервные стратегии

closedby (dialog-closedby)

Без closedby модальное окно по-прежнему закрывается с помощью Esc и жеста "Назад". Не работает только нажатие за пределами окна, поэтому это постепенное сокращение. closedby="none" игнорируется аналогичным образом: Esc запускает cancel, а затем закрывает диалоговое окно. Добавьте защиту от несохраненных изменений в обработчик cancel, а не только в closedby="none", чтобы она также работала, когда closedby отсутствует. Если целевой версии Baseline требуется закрытие по клику вне области, добавьте этот код там, где отсутствует closedby:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

Рассматривайте ограничение прокрутки как прогрессивное улучшение. Не используйте определение функций: @supports сообщает, что свойство поддерживается, даже если контейнеры без прокрутки его игнорируют. Если это не применяется, диалоговое окно по-прежнему содержит прокрутку, когда его собственное содержимое переполняется, а showModal() по-прежнему делает страницу инертной. Не используйте overflow: hidden или position: fixed в html или body, так как это удаляет корневую полосу прокрутки и сдвигает макет.

Команды Invoker (command/commandfor)

Если целевой Baseline не включает команды Invoker, сохраните ту же разметку и условно загрузите полифил (то же самое, что и прогрессивное улучшение). Определяйте наличие интерфейса и никогда не загружайте его безусловно:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

Только правила 2 и 3 используют всплывающее окно.

Резервные варианты и поддержка Popover в браузерах

Popover API в основном представляет собой прогрессивное улучшение, но его определяющие характеристики – продвижение верхнего слоя, легкое отклонение и вызов popovertarget – не имеют эквивалента только для CSS. Для старых браузеров нужен полифил или резервный вариант, если вы не хотите его использовать.

Полифил. Чтобы обеспечить поддержку атрибута popover в старых браузерах, условно загрузите @oddbird/popover-polyfill. ОБЯЗАТЕЛЬНО. Проверьте наличие свойства popover в HTMLElement.prototype и загрузите полифил, только если встроенная поддержка отсутствует. Не загружайте его без необходимости.

С помощью сборщика или карты импорта:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Если вы не используете сборщик, импортируйте из CDN в <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Ограничение, связанное со стилями. Полифил не может определить настоящий псевдокласс :popover-open, поэтому вместо него применяется класс .\:popover-open. ОБЯЗАТЕЛЬНО. Объедините эти два правила с помощью :is() или :where(), иначе браузеры, не поддерживающие :popover-open, будут игнорировать правило целиком.

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

В качестве альтернативы для устаревшего резервного варианта без полифила используйте position: fixed и вручную рассчитайте координаты с помощью getBoundingClientRect() или используйте позиционирование по умолчанию с помощью inset: auto, если это допустимо для вашего случая.