Базовый обзор того, как создавать адаптивные к цвету, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <dialog>
.
В этой публикации я хочу поделиться своими мыслями о том, как создавать адаптивные к цвету, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <dialog>
. Попробуйте демо-версию и посмотрите исходный код !
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Элемент <dialog>
отлично подходит для отображения контекстной информации или действий на странице. Подумайте, когда пользовательский опыт может выиграть от действия на одной странице вместо многостраничного: например, если форма небольшая или от пользователя требуется только подтвердить или отменить действие.
Элемент <dialog>
недавно стал стабильным во всех браузерах:
Я обнаружил, что элементу не хватает нескольких вещей, поэтому в этом GUI Challenge я добавляю ожидаемые мной элементы пользовательского опыта разработчика: дополнительные события, исчезновение света, пользовательские анимации, а также мини- и мега-типы.
Разметка
Элемент <dialog>
обладает скромными возможностями. Он автоматически скрывается и имеет встроенные стили для наложения вашего контента.
<dialog>
…
</dialog>
Мы можем улучшить этот базовый уровень.
Традиционно элемент диалога имеет много общего с модальным окном, и часто их названия взаимозаменяемы. Я позволил себе использовать элемент диалога как для небольших всплывающих диалоговых окон (mini), так и для полностраничных диалоговых окон (mega). Я назвал их mega и mini, слегка адаптировав оба варианта для разных вариантов использования. Я добавил атрибут modal-mode
, позволяющий указать тип:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Не всегда, но обычно элементы диалогового окна используются для сбора информации о взаимодействии. Формы внутри элементов диалогового окна создаются для совместного использования . Рекомендуется заключать содержимое диалогового окна в элемент формы, чтобы JavaScript мог получить доступ к данным, введённым пользователем. Кроме того, кнопки внутри формы, использующие method="dialog"
могут закрывать диалоговое окно без JavaScript и передавать данные.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
Мега диалог
Мегадиалоговое окно содержит три элемента внутри формы: <header>
, <article>
и <footer>
. Они служат семантическими контейнерами, а также целевыми стилями для представления диалогового окна. Заголовок даёт название модальному окну и содержит кнопку закрытия. Статья предназначена для ввода данных и информации в форме. Нижний колонтитул содержит <menu>
с кнопками действий.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
<header>
<h3>Dialog title</h3>
<button onclick="this.closest('dialog').close('close')"></button>
</header>
<article>...</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
Первая кнопка меню имеет autofocus
и встроенный обработчик событий onclick
. Атрибут autofocus
получает фокус при открытии диалогового окна, и я считаю, что лучше всего размещать его на кнопке отмены, а не на кнопке подтверждения. Это гарантирует, что подтверждение будет преднамеренным, а не случайным.
Мини-диалог
Мини-диалоговое окно очень похоже на мега-диалоговое окно, только в нём отсутствует элемент <header>
. Это позволяет сделать его меньше и более встраиваемым.
<dialog id="MiniDialog" modal-mode="mini">
<form method="dialog">
<article>
<p>Are you sure you want to remove this user?</p>
</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
Элемент «Диалог» обеспечивает прочную основу для полноценного элемента области просмотра, который может собирать данные и отслеживать взаимодействие с пользователем. Эти базовые элементы могут стать основой для интересных и эффективных взаимодействий на вашем сайте или в приложении.
Доступность
Элемент диалога имеет очень хорошую встроенную доступность. Вместо того, чтобы добавлять эти функции, как я обычно делаю, многие из них уже есть.
Восстановление фокуса
Как мы делали вручную при создании компонента боковой навигации , важно, чтобы при открытии и закрытии компонента фокус попадал на соответствующие кнопки открытия и закрытия. При открытии боковой навигации фокус перемещается на кнопку закрытия. При нажатии кнопки закрытия фокус возвращается к кнопке, которая её открыла.
Для элемента dialog это встроенное поведение по умолчанию:
К сожалению, если вы хотите анимировать появление и исчезновение диалогового окна, эта функция будет потеряна. В разделе, посвящённом JavaScript, я восстановлю эту функцию.
Захват фокуса
Элемент dialog управляет inert
в документе. До появления inert
для отслеживания перехода фокуса с элемента использовался JavaScript, который в этот момент перехватывал фокус и возвращал его обратно.
После inert
любые части документа могут быть «заморожены» таким образом, что они больше не будут фокусироваться и не будут взаимодействовать с мышью. Вместо того, чтобы удерживать фокус, он направляется в единственную интерактивную часть документа.
Открыть и автоматически сфокусировать элемент
По умолчанию элемент диалогового окна устанавливает фокус на первый фокусируемый элемент в разметке диалогового окна. Если этот элемент не подходит пользователю по умолчанию, используйте атрибут autofocus
. Как уже говорилось, я считаю, что лучше всего размещать его на кнопке отмены, а не на кнопке подтверждения. Это гарантирует, что подтверждение будет преднамеренным, а не случайным.
Закрытие с помощью клавиши Escape
Важно сделать так, чтобы закрыть этот потенциально мешающий элемент было легко. К счастью, элемент диалога автоматически нажмёт клавишу ESC, избавив вас от необходимости оркестровки.
Стили
Существует простой и сложный пути стилизации элемента диалогового окна. Простой путь достигается за счёт того, что свойство display диалогового окна не меняется, а учитывается его ограничения. Я же выбрал сложный путь, чтобы реализовать пользовательские анимации открытия и закрытия диалогового окна, перехватывая свойство display
и многое другое.
Стилизация с помощью Open Props
Чтобы ускорить адаптивную цветопередачу и обеспечить общую согласованность дизайна, я беззастенчиво использовал свою библиотеку CSS-переменных Open Props . Помимо бесплатных переменных, я также импортирую файл нормализации и несколько кнопок , которые Open Props предоставляет в качестве опциональных импортов. Этот импорт помогает мне сосредоточиться на настройке диалогового окна и демо-версии, не требуя при этом множества стилей для их поддержки и придания им привлекательного вида.
Стилизация элемента <dialog>
Владение выставочной недвижимостью
По умолчанию поведение отображения и скрытия элемента диалога переключает свойство display с block
на none
. К сожалению, это означает, что его нельзя анимировать, только при появлении и исчезновении. Я хотел бы реализовать анимацию и при появлении, и для этого первым шагом будет установка собственного свойства display :
dialog {
display: grid;
}
Изменяя значение свойства display, а следовательно, и контролируя его, как показано в приведенном выше фрагменте CSS, необходимо управлять значительным количеством стилей для обеспечения корректного пользовательского опыта. Во-первых, по умолчанию диалоговое окно закрыто. Вы можете визуально представить это состояние и запретить диалоговому окну взаимодействовать с ним с помощью следующих стилей:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Теперь диалоговое окно невидимо и с ним нельзя взаимодействовать, когда оно закрыто. Позже я добавлю JavaScript-код для управления атрибутом inert
диалогового окна, чтобы пользователи клавиатуры и программ чтения с экрана также не могли получить доступ к скрытому диалоговому окну.
Придание диалоговому окну адаптивной цветовой темы
Хотя color-scheme
подбирает для вашего документа адаптивную цветовую тему, предоставляемую браузером в соответствии с системными настройками (светлой или тёмной), мне хотелось настроить элемент диалогового окна более детально. Open Props предоставляет несколько цветов поверхности , которые автоматически подстраиваются под системные настройки (светлой или тёмной), аналогично использованию color-scheme
. Они отлично подходят для создания слоёв в дизайне, и мне нравится использовать цвет для визуальной поддержки такого внешнего вида слоёв. Цвет фона — var(--surface-1)
; чтобы разместить его поверх этого слоя, используйте var(--surface-2)
:
dialog {
…
background: var(--surface-2);
color: var(--text-1);
}
@media (prefers-color-scheme: dark) {
dialog {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
Позже для дочерних элементов, таких как верхний и нижний колонтитулы, будут добавлены адаптивные цвета. Я считаю, что для диалогового элемента они лишние, но очень важны для создания привлекательного и качественного дизайна диалогового окна.
Адаптивное изменение размера диалоговых окон
Диалоговое окно по умолчанию делегирует свой размер содержимому, что, как правило, отлично. Моя цель — ограничить max-inline-size
читабельным размером ( --size-content-3
= 60ch
) или 90% от ширины области просмотра. Это гарантирует, что диалоговое окно не будет растягиваться от края до края на мобильном устройстве и не будет настолько широким на экране настольного компьютера, что его будет трудно читать. Затем я добавляю max-block-size
, чтобы диалоговое окно не превышало высоту страницы. Это также означает, что нам нужно указать, где находится прокручиваемая область диалогового окна, если это высокий элемент диалога.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
Заметьте, что я дважды указал max-block-size
? В первом случае используется 80vh
— физическая единица измерения области просмотра. Мне действительно нужно, чтобы диалоговое окно отображалось в относительном потоке для международных пользователей, поэтому во втором объявлении я использую логическую, более новую и лишь частично поддерживаемую единицу dvb
, которая появится, когда станет стабильнее.
Мега-диалоговое позиционирование
Чтобы упростить позиционирование элемента диалогового окна, стоит разбить его на две части: полноэкранный фон и контейнер диалогового окна. Фон должен закрывать всё, создавая эффект тени, который подчёркивает, что диалоговое окно находится на переднем плане, а содержимое за ним недоступно. Контейнер диалогового окна может свободно центрироваться относительно этого фона и принимать любую форму, соответствующую его содержимому.
Следующие стили фиксируют элемент диалога в окне, растягивая его до каждого угла, и используют margin: auto
для центрирования содержимого:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
Стили мобильного мегадиалога
На небольших экранах я немного иначе оформляю это мегамодальное окно на всю страницу. Я устанавливаю нижнее поле равным 0
, что позволяет разместить содержимое диалогового окна в нижней части экрана. С помощью нескольких настроек стиля я могу превратить диалоговое окно в список действий, расположенный ближе к большим пальцам пользователя:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
Позиционирование мини-диалога
При использовании более крупной области просмотра, например, на настольном компьютере, я решил разместить мини-диалоговые окна над вызывающим их элементом. Для этого мне нужен JavaScript. Вы можете найти используемый мной приём здесь , но, на мой взгляд, он выходит за рамки этой статьи. Без JavaScript мини-диалоговое окно отображается в центре экрана, как и мегадиалоговое окно.
Сделай это попсовым
Наконец, добавьте немного изюминку диалоговому окну, чтобы оно выглядело как мягкая поверхность, возвышающаяся над страницей. Мягкость достигается за счёт скругления углов диалогового окна. Глубина достигается с помощью одного из тщательно проработанных теневых реквизитов Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Настройка псевдоэлемента фона
Я решил не особо увлекаться фоном, добавив лишь эффект размытия с помощью backdrop-filter
к мегадиалоговому окну:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Я также решил применить переход к backdrop-filter
в надежде, что браузеры разрешат переход элемента фона в будущем:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Дополнительные элементы стиля
Я называю этот раздел «дополнительно», потому что он больше связан с моей демонстрацией элемента диалога, чем с элементом диалога в целом.
Удерживание свитка
Когда диалоговое окно отображается, пользователь все равно может прокручивать страницу за ним, что мне не нужно:
Обычно я бы выбрал overscroll-behavior
, но, согласно спецификации , он не влияет на диалоговое окно, поскольку это не порт прокрутки, то есть не скроллер, поэтому ему ничего не мешает. Я мог бы использовать JavaScript для отслеживания новых событий из этого руководства, таких как «закрыто» и «открыто», и включения/выключения свойства overflow: hidden
в документе, или дождаться, пока :has()
станет стабильной во всех браузерах:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Теперь, когда открыто мегадиалоговое окно, в html-документе отображается overflow: hidden
.
Макет <form>
Помимо того, что это очень важный элемент для сбора информации о взаимодействии с пользователем, я использую его здесь для размещения элементов заголовка, нижнего колонтитула и статьи. В этом макете я хочу представить дочерний элемент статьи как прокручиваемую область. Я добился этого с помощью grid-template-rows
. Элементу статьи задано значение 1fr
, а сама форма имеет ту же максимальную высоту, что и элемент диалога. Установка этой фиксированной высоты и фиксированного размера строки позволяет ограничить элемент статьи и прокручивать его при выходе за пределы:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Стилизация диалогового окна <header>
Этот элемент предназначен для предоставления заголовка для содержимого диалогового окна и размещения кнопки закрытия, которую легко найти. Он также имеет цвет поверхности, чтобы визуально создать впечатление, что он находится за содержимым статьи диалогового окна. Эти требования приводят к появлению контейнера Flexbox, вертикально выровненных элементов с отступами по краям, а также отступов и зазоров для размещения заголовка и кнопок закрытия:
dialog > form > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
background: var(--surface-2);
padding-block: var(--size-3);
padding-inline: var(--size-5);
}
@media (prefers-color-scheme: dark) {
dialog > form > header {
background: var(--surface-1);
}
}
Оформление кнопки закрытия заголовка
Поскольку в демоверсии используются кнопки Open Props, кнопка закрытия оформлена в виде круглой кнопки с иконкой, как показано ниже:
dialog > form > header > button {
border-radius: var(--radius-round);
padding: .75ch;
aspect-ratio: 1;
flex-shrink: 0;
place-items: center;
stroke: currentColor;
stroke-width: 3px;
}
Стилизация диалогового окна <article>
Элемент article играет особую роль в этом диалоге: это пространство, предназначенное для прокрутки в случае длинного или высокого диалога.
Для этого родительский элемент form устанавливает для себя определённые максимальные значения, которые накладывают ограничения на элемент article, если он становится слишком высоким. Установите overflow-y: auto
, чтобы полосы прокрутки отображались только при необходимости, ограничьте прокрутку с помощью overscroll-behavior: contain
, а остальное будет реализовано пользовательскими стилями представления:
dialog > form > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: light) {
dialog > form > article {
background: var(--surface-1);
}
}
Стилизация диалогового окна <footer>
Футер предназначен для размещения меню с кнопками действий. Flexbox используется для выравнивания содержимого по краю строчной оси футера, а также для создания отступов между кнопками.
dialog > form > footer {
background: var(--surface-2);
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: dark) {
dialog > form > footer {
background: var(--surface-1);
}
}
Стилизация меню нижнего колонтитула диалога
Элемент menu
используется для размещения кнопок действий в диалоговом окне. Он использует макет Flexbox с обёрткой и gap
для обеспечения пространства между кнопками. Элементы меню имеют отступы, например, <ul>
. Я также удаляю этот стиль, поскольку он мне не нужен.
dialog > form > footer > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
}
dialog > form > footer > menu:only-child {
margin-inline-start: auto;
}
Анимация
Элементы диалоговых окон часто анимируются, поскольку они появляются и исчезают в окне. Добавление в диалоговые окна вспомогательного движения для появления и исчезновения помогает пользователям ориентироваться в потоке.
Обычно элемент диалогового окна может быть анимирован только вовнутрь, но не наружу. Это связано с тем, что браузер переключает свойство display
элемента. Ранее в руководстве для display было задано значение grid, и никогда не устанавливалось значение none. Это открывает возможность анимации вовнутрь и наружу.
Open Props предлагает множество анимаций по ключевым кадрам , что делает оркестровку простой и понятной. Вот цели анимации и многоуровневый подход, который я использовал:
- Переход по умолчанию — это уменьшение движения, простое постепенное увеличение и уменьшение непрозрачности.
- Если движение в порядке, добавляются анимации скольжения и масштабирования.
- Адаптивный мобильный макет для мегадиалогового окна настроен так, чтобы выдвигаться.
Безопасный и осмысленный переход по умолчанию
Хотя в Open Props есть ключевые кадры для плавного появления и исчезновения, я предпочитаю использовать многоуровневый подход с переходами по умолчанию, а анимацию по ключевым кадрам — в качестве потенциального улучшения. Ранее мы уже задали видимость диалогового окна с помощью непрозрачности, установив значение 1
или 0
в зависимости от атрибута [open]
. Чтобы перейти от 0% к 100%, укажите браузеру необходимую длительность и плавность:
dialog {
transition: opacity .5s var(--ease-3);
}
Добавление движения к переходу
Если пользователь не возражает против движения, большие и мини-диалоги должны выдвигаться при входе и уменьшаться при выходе. Этого можно добиться с помощью медиазапроса prefers-reduced-motion
и нескольких свойств Open:
@media (prefers-reduced-motion: no-preference) {
dialog {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
dialog[open] {
animation: var(--animation-slide-in-up) forwards;
}
}
Адаптация анимации выхода для мобильных устройств
Ранее в разделе о стилизации стиль мегадиалога был адаптирован для мобильных устройств, чтобы больше походить на список действий, как будто небольшой лист бумаги выскользнул из нижней части экрана и всё ещё прикреплён к ней. Анимация выхода с уменьшением масштаба не очень хорошо вписывается в новый дизайн, и мы можем адаптировать её с помощью пары медиазапросов и нескольких OpenProps:
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
JavaScript
С помощью JavaScript можно добавить несколько вещей:
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
Эти дополнения обусловлены потребностью в легком закрытии (щелчком по фону диалогового окна), анимации и некоторых дополнительных событиях для лучшего расчета времени при получении данных формы.
Добавление светового сигнала
Эта задача проста и отлично подходит для элемента диалога, который не анимирован. Взаимодействие достигается путём отслеживания щелчков по элементу диалога и использования всплывающей подсказки событий для определения того, что было нажато. Функция close()
будет выполняться только в том случае, если это самый верхний элемент:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
Обратите внимание на dialog.close('dismiss')
. Вызывается событие и предоставляется строка. Эту строку может получить другой код JavaScript, чтобы получить информацию о том, как был закрыт диалог. Вы увидите, что я также предоставляю строки закрытия каждый раз при вызове функции из различных кнопок, чтобы предоставить приложению контекст для взаимодействия с пользователем.
Добавление событий закрытия и закрытия
Элемент диалогового окна имеет событие закрытия: оно генерируется сразу после вызова функции диалогового close()
. Поскольку мы анимируем этот элемент, было бы полезно иметь события до и после анимации, чтобы при необходимости получить данные или сбросить форму диалогового окна. Здесь я использую его для управления добавлением атрибута inert
к закрытому диалоговому окну, а в демоверсии — для изменения списка аватаров, если пользователь отправил новое изображение.
Для этого создайте два новых события: closing
и closed
. Затем прослушивайте встроенное событие «close» в диалоговом окне. После этого переведите диалоговое окно в inert
и отправьте событие closing
. Следующая задача — дождаться завершения анимации и переходов в диалоговом окне, а затем отправить событие closed
.
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
export default async function (dialog) {
…
dialog.addEventListener('close', dialogClose)
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
Функция animationsComplete
, которая также используется в компоненте «Создание тост-сообщения» , возвращает обещание, основанное на завершении анимации и переходов. Именно поэтому dialogClose
— асинхронная функция : она может await
возврата обещания и уверенно перейти к событию «закрыто».
Добавление событий открытия и открытия
Эти события добавить не так просто, поскольку встроенный элемент диалога не предоставляет событие открытия, как в случае закрытия. Я использую MutationObserver для получения информации об изменении атрибутов диалога. В этом наблюдателе я буду отслеживать изменения атрибута открытия и соответствующим образом управлять пользовательскими событиями.
Аналогично тому, как мы создавали события закрытия и закрытия, создайте два новых события с именами opening
и opened
. Ранее мы прослушивали событие закрытия диалога, а теперь используем созданный наблюдатель мутаций для отслеживания атрибутов диалога.
…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
export default async function (dialog) {
…
dialogAttrObserver.observe(dialog, {
attributes: true,
})
}
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
Функция обратного вызова Mutation Observer будет вызвана при изменении атрибутов диалогового окна, предоставив список изменений в виде массива. Переберите изменения атрибутов, найдя значение attributeName, соответствующее attributeName
open. Затем проверьте, есть ли у элемента этот атрибут: это покажет, открыто ли диалоговое окно. Если оно открыто, удалите атрибут inert
и установите фокус либо на элемент, запрашивающий autofocus
, либо на первый найденный элемент- button
в диалоговом окне. Наконец, аналогично событиям закрытия и закрытия, немедленно отправьте событие открытия, дождитесь завершения анимации, а затем отправьте событие открытия.
Добавление удаленного события
В одностраничных приложениях диалоговые окна часто добавляются и удаляются в зависимости от маршрутов или других потребностей и состояния приложения. Это может быть полезно для очистки событий или данных при удалении диалогового окна.
Этого можно добиться с помощью другого наблюдателя мутаций. На этот раз вместо наблюдения за атрибутами элемента диалога мы будем наблюдать за дочерними элементами элемента body и следить за удалением элементов диалога.
…
const dialogRemovedEvent = new Event('removed')
export default async function (dialog) {
…
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
}
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
Обратный вызов Mutation Observer вызывается при каждом добавлении или удалении дочерних элементов из тела документа. Отслеживаются мутации для removedNodes
, имеющих nodeName
диалогового окна. Если диалоговое окно было удалено, события click и close удаляются для освобождения памяти, а затем отправляется пользовательское событие removed.
Удаление атрибута загрузки
Чтобы предотвратить воспроизведение анимации закрытия диалогового окна при добавлении на страницу или при её загрузке, к диалоговому окну был добавлен атрибут загрузки. Следующий скрипт дожидается завершения анимации диалогового окна, а затем удаляет этот атрибут. Теперь диалоговое окно может свободно появляться и исчезать, и мы фактически скрыли отвлекающую анимацию.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Подробнее о проблеме предотвращения анимации ключевых кадров при загрузке страницы можно узнать здесь .
Все вместе
Вот dialog.js
целиком, теперь, когда мы объяснили каждый раздел по отдельности:
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Использование модуля dialog.js
Экспортированная из модуля функция ожидает вызова и передачи элемента диалога, которому необходимо добавить эти новые события и функциональные возможности:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
Таким образом, оба диалоговых окна обновлены: добавлены функции закрытия, исправлена загрузка анимации и добавлено больше событий для работы.
Прослушивание новых пользовательских событий
Каждый обновленный элемент диалога теперь может прослушивать пять новых событий, например:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Вот два примера обработки этих событий:
const dialogOpening = ({target:dialog}) => {
console.log('Dialog opening', dialog)
}
const dialogClosed = ({target:dialog}) => {
console.log('Dialog closed', dialog)
console.info('Dialog user action:', dialog.returnValue)
if (dialog.returnValue === 'confirm') {
// do stuff with the form values
const dialogFormData = new FormData(dialog.querySelector('form'))
console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))
// then reset the form
dialog.querySelector('form')?.reset()
}
}
В демоверсии, созданной мной с использованием элемента диалога, я использую событие закрытия и данные формы для добавления нового элемента аватара в список. Время выбрано удачно: диалоговое окно уже завершило анимацию закрытия, а затем некоторые скрипты начинают анимироваться в новом аватаре. Благодаря новым событиям управление пользовательским интерфейсом стало более плавным.
Обратите внимание на dialog.returnValue
: он содержит строку закрытия, переданную при вызове события dialog close()
. В событии dialogClosed
крайне важно знать, был ли диалог закрыт, отменён или подтверждён. Если диалог подтверждён, скрипт получает значения формы и сбрасывает её. Сброс полезен, поскольку при повторном отображении диалоговое окно будет пустым и готовым к новой отправке.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы поступили? 🙂
Давайте разнообразим наши подходы и изучим все способы развития в Интернете.
Создайте демо, пришлите мне ссылку в Твиттер , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
- @GrimLink с диалогом 3-в-1 .
- @mikemai2awesome с отличным ремиксом , который не меняет свойство
display
. - @geoffrich_ с Svelte и отличным лаком Svelte FLIP .
Ресурсы
- Исходный код на Github
- Doodle-аватары