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

Карточка урока в нативном диалоге

Продолжим каталог, который уже читается через обычные ссылки. Добавим короткую карточку с объяснением учебного проекта. Она не заменяет ни статью, ни описание каталога. Цель этапа — открыть дополнительное содержимое как модальный диалог и согласовать его закрытие с клавиатурой. В этом случае нативный dialog берёт на себя значительную часть поведения, которое иначе пришлось бы писать вручную.

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

Модальность и управление фокусом

Кнопка открытия имеет type="button" и первоначально скрыта. Сценарий находит диалог и проверяет, является ли showModal функцией. Только после этого кнопка появляется и получает обработчик. Проверяется конкретная возможность, а не название браузера. Подмена строки User-Agent была бы слабым договором: версия движка, настройки и условия встраивания могут отличаться от ожидаемых.

Обработчик вызывает showModal, когда диалог ещё не открыт. Метод помещает элемент в верхний слой. Большой z-index у произвольного соседнего блока не является способом перекрыть этот слой. Затем браузер выполняет предусмотренные правила фокуса. В нашей небольшой карточке autofocus стоит на кнопке закрытия: это ближайшее осмысленное действие после прочтения текста.

Форма с method="dialog" закрывает диалог локально, без отправки HTTP-запроса. Значение кнопки можно использовать как returnValue, если карточка предлагает несколько вариантов. Здесь нам нужен только выход. Событие close возвращает фокус к существующей кнопке открытия. Это явно обозначает продолжение маршрута. При удалении кнопки из DOM потребуется другой заранее выбранный элемент, а не вызов focus у исчезнувшего объекта.

Заголовок связан с диалогом через aria-labelledby. Такое имя объясняет назначение окна, а внутренний абзац остаётся содержимым для чтения. Не задавайте tabindex всему dialog лишь для того, чтобы получить фокус: сначала выберите осмысленный интерактивный элемент или текстовый заголовок в соответствии с задачей. При длинном содержимом начальная позиция потребует отдельного решения.

В ожидаемом результате кнопка открывает карточку, фон временно недоступен, а кнопка закрытия и Escape позволяют вернуться к каталогу. Клик по затемнённому фону в этом примере намеренно не назначен способом закрытия. Наличие ::backdrop рисует фон, но само по себе не является обработчиком. Не обещайте читателю такое поведение, пока оно не реализовано.

Стиль ограничивает ширину окна размером экрана и оставляет внутренние отступы. Он дополняет семантику, но не создаёт модальность. Простое добавление атрибута open показало бы немодальный диалог; для нашего договора требуется именно showModal. Это различие полезно наблюдать на одном документе: внешний вид может быть похожим, а доступность фоновых элементов — разной.

Полный файл index.html текущего этапа:

<!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="styles.css"><script type="module" src="app.js"></script></head>
<body><a href="#main">Перейти к материалам</a><header><a href="index.html">Учебный каталог</a></header><main id="main"><h1>Учебные материалы</h1><p>Каталог «Полка»: статьи о контенте и интерфейсах.</p><button type="button" id="open-lesson" hidden>О проекте урока</button>
<dialog id="lesson-dialog" aria-labelledby="dialog-title"><h2 id="dialog-title">Учебная библиотека</h2><p>Три урока с постоянными ссылками. Карточка дополняет каталог.</p><form method="dialog"><button value="close" autofocus>Закрыть</button></form></dialog><ul class="cards"><li class="card" data-topic="content" data-minutes="20"><h2><a href="article.html#markdown">Markdown и структура урока</a></h2><p class="card-description">Разделяем содержание и оформление библиотеки.</p><p class="card-meta">Контент · 20 минут</p></li>
<li class="card" data-topic="interface" data-minutes="35"><h2><a href="article.html#navigation">Навигация учебного каталога</a></h2><p class="card-description">Сохраняем обычные ссылки и последовательность чтения.</p><p class="card-meta">Интерфейс · 35 минут</p></li>
<li class="card" data-topic="content" data-minutes="25"><h2><a href="article.html#urls">Постоянные адреса материалов</a></h2><p class="card-description">Меняем каталог, сохраняя адрес каждой статьи.</p><p class="card-meta">Контент · 25 минут</p></li></ul><p class="notice">Каждый урок доступен по обычной ссылке. Фильтры и эффекты дополняют чтение.</p></main><footer>Полка · учебный проект</footer></body></html>

Полный файл app.js текущего этапа:

document.documentElement.dataset.enhanced = 'true';
const lessonDialog = document.querySelector('#lesson-dialog');
const openLesson = document.querySelector('#open-lesson');
if (typeof lessonDialog.showModal === 'function') {
  openLesson.hidden = false;
  openLesson.addEventListener('click', () => {
    if (!lessonDialog.open) lessonDialog.showModal();
  });
  lessonDialog.addEventListener('close', () => openLesson.focus());
}

Полный файл styles.css текущего этапа:

* { box-sizing: border-box; }
:root { color-scheme: light; --accent: #6d28d9; --paper: #fff; --ink: #20202a; }
body { margin: 0; color: var(--ink); background: #f7f6fb; font: 1rem/1.6 system-ui, sans-serif; }
header, main, footer { max-width: 72rem; margin-inline: auto; padding: 1.5rem; }
a { color: #5923b5; text-underline-offset: .18em; }
button { font: inherit; padding: .5rem .9rem; cursor: pointer; }
:focus-visible { outline: 3px solid #17693d; outline-offset: 4px; }
[hidden] { display: none !important; }
.cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); list-style: none; padding: 0; }
.card, aside { padding: 1.25rem; border: 1px solid #ddd8e8; border-radius: .7rem; background: white; }
.card h2 { margin-block-start: 0; }
.card-meta { color: #51495f; }
.notice { border-inline-start: .25rem solid var(--accent); padding-inline-start: 1rem; }
dialog { width: min(36rem, calc(100vw - 2rem)); border: 1px solid #b9afca; padding: 1.5rem; border-radius: .8rem; }
dialog::backdrop { background: rgb(20 15 30 / .5); }

Разбор изменения каталога

У нативного диалога также есть немодальная форма show. Она полезна в другой задаче, но не должна подменять выбранную модель лишь ради устранения визуальной ошибки. Если требуется одновременно работать с каталогом, лучше выбрать немодальную справку из следующего урока. Решение о блокировке фона начинается со смысла действия, а не с наличия привлекательного затемнения.

Фокус после закрытия должен возвращаться в контекст, откуда пришёл человек. В нашем маленьком примере кнопка не удаляется, поэтому прямой вызов focus имеет понятную цель. Если карточка урока открывается из отфильтрованного списка и исходная запись исчезла, потребуется выбрать ближайший устойчивый элемент. Такой случай нельзя решить универсальным сохранением ссылки на старый DOM-объект: объект может существовать в памяти, но уже не быть частью документа.

Для самостоятельного варианта замените короткий текст более длинным пояснением и добавьте обычную ссылку внутри окна. Рассмотрите порядок действий: начальный фокус, переход к ссылке, выход через кнопку, выход через Escape. Не вводите одновременно собственный focus trap. Сначала изучите нативное поведение и выявите конкретный пробел. Иначе два менеджера фокуса могут спорить за один маршрут и сделать элемент недоступным.

Разберём ещё одну распространённую ошибку: окно показывают через display: block, а затем добавляют затемнение фиксированным div. Такой код может создать похожую картинку, но не выражает модальность в браузере. Фон остаётся доступным клавиатуре, а управление Escape приходится писать отдельно. В этом проекте внешний вид следует за showModal, поэтому базовый механизм и оформленный фон имеют одну причину.

Не назначаем обработчик Enter всему документу для закрытия окна. Эта клавиша может принадлежать ссылке или полю внутри диалога. Форма method dialog уже даёт определённое действие кнопки, а остальная клавиатура сохраняет значение. Чем шире область обработчика, тем внимательнее нужно понимать его влияние на независимые элементы. Для текущего короткого окна глобальный перехват не нужен.

Состояние open читается у самого диалога. Дополнительный boolean в сценарии мог бы отстать после нативного Escape. Тогда код считал бы окно открытым, хотя браузер уже закрыл его, или наоборот. Если приложение всё же хранит управляемое состояние, оно должно синхронизировать close и cancel. Здесь оставляем один источник, чтобы механизм было легче изучить.

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

Условия для самостоятельного наблюдения

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

Условие Ожидаемое состояние и его граница
Метод отсутствует Кнопка дополнительной карточки не появляется, обычное введение и статьи остаются доступными.
Нажатие Enter Кнопка открытия вызывает тот же обработчик, что и действие мышью.
Повторное открытие Проверка open предотвращает лишний вызов для уже открытого окна.
Закрытие формой Запрос не отправляется; событие close продолжает клавиатурный маршрут.
Клавиша Escape Браузер запрашивает закрытие модального диалога; код не отменяет событие cancel.
Клик по затемнению Фон окрашен, однако специального закрытия по этому действию в исходниках нет.

Оглавление серии, курсы ProfessorWeb и полный архив исходников. Для этого урока выберите lesson-02.

Первичные источники: HTML dialog.

Редакционный статус: локальный черновик. Установка, учебные программы, браузерные сценарии, тесты и публикация не выполнялись. Описанные эффекты ожидаемые.