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

Диалог на основе dialog

Иногда пояснение удобно открыть поверх страницы, чтобы человек мог прочитать его и вернуться к прежнему действию. Нативный элемент dialog предоставляет механизм диалога, а вызов showModal делает его модальным: остальные части документа временно недоступны для взаимодействия. Это больше, чем окрашенный прямоугольник с большим z-index, поэтому изучать открытие нужно вместе с закрытием и фокусом.

До этого момента каталог использовал только HTML и CSS. В данном уроке добавим небольшой точно ограниченный JavaScript. Результат — дополнительное пояснение в модальном окне и возврат к кнопке открытия. Обычный блок помощи останется на странице даже без выполнения скрипта и без поддержки API: новый механизм не должен превращать основные ссылки в зависимость от диалога.

Обычное пояснение остаётся основой

В index.html вставьте следующий полный блок перед существующим details.lab-details. Первое пояснение является обычной секцией с заголовком. Диалог содержит дополнительную короткую формулировку, а не единственное место хранения необходимых адресов. Реальные оглавления по-прежнему находятся в карточках каталога.

<section class="lab-advice" id="course-advice" aria-labelledby="advice-title">
<h2 id="advice-title">Как выбрать первый курс</h2>
<p>Начните со статического сайта, затем изучите выпуск и обнаружение страниц. Оглавления остаются обычными ссылками.</p>
<button id="open-course-dialog" type="button" hidden>Открыть пояснение в диалоге</button>
</section>
<dialog id="course-dialog" class="lab-dialog" aria-labelledby="dialog-title" hidden>
<h2 id="dialog-title">Первый учебный маршрут</h2>
<p>Markdown поможет собрать страницу из файлов. Деплоймент объяснит выпуск, а техническое SEO — обнаружение адресов.</p>
<form method="dialog">
<button type="submit" autofocus>Вернуться к каталогу</button>
</form>
</dialog>

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

Имя диалога связано с его собственным заголовком через aria-labelledby. Кнопка закрытия находится в форме с method="dialog". Такой метод закрывает диалог, а не отправляет заявку серверу. Эта форма отдельная от прежней учебной GET-формы: их назначение и поведение не смешиваются. Атрибут autofocus задаёт ожидаемое первое действие после открытия.

Скрипт подключается один раз

В head добавьте одно подключение нового файла. Оно не заменяет stylesheet и не требует дополнительного обработчика готовности документа, поскольку внешний обычный скрипт указан с defer.

<script src="ui.js" defer></script>

Создайте ui.js рядом с index.html со следующим полным содержимым. В этом уроке других скриптов в проекте нет. Не переносите код в каждую карточку и не подключайте файл второй раз: повторная установка поведения не является запасным способом запуска.

const opener = document.querySelector('#open-course-dialog');
const dialog = document.querySelector('#course-dialog');
if (opener && dialog && typeof dialog.showModal === 'function') {
  dialog.hidden = false;
  opener.hidden = false;
  opener.addEventListener('click', () => {
    if (!dialog.open) dialog.showModal();
  });
  dialog.addEventListener('close', () => opener.focus());
}

Первые строки находят конкретные элементы документа. Условие требует и их наличие, и функцию showModal. Только после этой проверки снимается первоначальное скрытие. Закрытый нативный диалог остаётся невидимым без open, поэтому снятие hidden само по себе не означает открытие окна. Кнопка становится доступной, поскольку её обработчик теперь установлен в поддерживаемом окружении.

Обработчик click вызывает метод только у закрытого диалога. Проверка dialog.open избегает лишнего повторного открытия в нашем простом состоянии. Метод работает с подключённым к документу элементом; если перестраивать HTML во время взаимодействия, эту предпосылку пришлось бы учитывать отдельно. Небольшой пример не обещает управлять произвольными динамическими диалогами.

Открытие, закрытие и фокус

После нажатия ожидается модальное пояснение и фокус на внутренней кнопке. Браузер помещает диалог в top layer и делает остальную часть документа инертной для взаимодействия. Поэтому не нужно вручную выдавать фоновым ссылкам отрицательный tabindex или придумывать отдельную блокирующую рамку поверх них. Такой самодельный механизм потребовал бы восстановить множество состояний.

Закрыть окно можно явно видимой кнопкой формы. Нативный модальный диалог также допускает обычное действие отмены, например Escape в соответствующем окружении. Мы не отменяем событие cancel, поэтому сохраняем это поведение. Нажатие снаружи в данном простом варианте не следует объявлять обязательным способом закрытия: новый атрибут управления таким поведением здесь не используется.

Событие close возвращает фокус к известной кнопке открытия. Нативный механизм тоже хранит сведения о прежнем фокусе, однако наш пример явно показывает целевую точку возврата. Кнопка остаётся подключённой и доступной. В более сложном интерфейсе, где она могла исчезнуть или стать disabled, потребовалась бы другая логика выбора точки; это ограничение текущей страницы важно сохранить.

Поведение элемента и формы описано в MDN о dialog, а действие модального метода — в справочнике showModal. Формальная модель открытия и закрытия приведена в HTML Standard. Это первичные основания механизма, а не подтверждение выполненного нами браузерного опыта.

Оформление не создаёт модальность

Добавьте следующий полный блок в конец stylesheet. Здесь важное объявление для [hidden] сохраняет смысл явно скрытого HTML-состояния, даже если другое правило задаст элементу вид отображения. Оно относится к ограниченной обязанности и не служит способом усиливать все стили каталога.

@layer components {
.layout-lab [hidden] { display: none !important; }
.layout-lab .lab-advice { margin-block-start: 2rem; }
.layout-lab .lab-advice > * + *, .layout-lab .lab-dialog > * + * { margin-block-start: 1rem; }
.layout-lab .lab-dialog { inline-size: min(90%, 32rem); max-block-size: 80vh; overflow: auto; padding: 1.5rem; border: 1px solid var(--lab-accent); border-radius: .5rem; color: var(--lab-ink); background: var(--lab-surface); }
.layout-lab .lab-dialog::backdrop { background: rgb(0 0 0 / .65); }
.layout-lab :is(.lab-advice, .lab-dialog) button { font: inherit; padding: .5rem .75rem; cursor: pointer; }
@media print { .layout-lab .lab-dialog { display: none; } }
}

У диалога ожидается ограниченная ширина, внутренняя прокрутка при необходимости и тёмная поверхность. ::backdrop задаёт фон за модальным элементом. Его цвет не создаёт блокировку сам по себе: блокировка возникает из showModal. Если вместо метода просто добавить open, получится другое, немодальное состояние. Именно поэтому мы не открываем диалог прямой правкой CSS display.

Максимальная высота помогает не потерять кнопку на низком окне, однако требуется ручная проверка с увеличенным текстом и экранной клавиатурой, если появятся поля. Сейчас внутри только пояснение и кнопка, а не сложная форма. Не добавляем tabindex к самому dialog ради случайного исправления фокуса; выбранное первое действие уже обозначено на подходящем дочернем элементе.

Модальное состояние не означает, что браузер обязан запретить любую визуальную прокрутку фона тем способом, которого ожидает дизайнер. Это отдельное поведение окружения и требований интерфейса. Наш основной контракт — блокировка фонового взаимодействия и доступное закрытие. Не приписываем фону дополнительные гарантии, которых не проверяли в целевых браузерах.

Состояние без нового механизма

Для последующего ручного опыта временно отключите подключение ui.js. Ожидается обычная секция пояснения, скрытая кнопка открытия и отсутствие доступного пустого диалога. Ссылки карточек, таблица и учебная форма должны сохраняться. Затем восстановите подключение. Это проверяет реальную основу страницы, а не только условие поддержки внутри работающего скрипта.

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

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

Полное состояние этого шага находится в layout-lab/lesson-29 внутри исходников продолжения. Каждый снимок открывается отдельно; подключать CSS разных уроков одновременно не нужно.

Оглавление курса.