Базовый обзор того, как создавать цветоадаптивные, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <dialog>
.
В этом посте я хочу поделиться своими мыслями о том, как создавать цветоадаптивные, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <dialog>
. Попробуйте демо-версию и просмотрите исходный код !
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Элемент <dialog>
отлично подходит для контекстной информации или действий на странице. Подумайте, когда пользовательский опыт может выиграть от одного и того же действия на странице вместо действия на нескольких страницах: возможно, потому, что форма небольшая или единственное действие, требуемое от пользователя, — это подтверждение или отмена.
Элемент <dialog>
недавно стал стабильным во всех браузерах:
Я обнаружил, что элементу не хватает некоторых вещей, поэтому в этом вызове GUI я добавляю ожидаемые элементы взаимодействия с разработчиком: дополнительные события, закрытие света, пользовательскую анимацию, а также мини- и мега-тип.
Разметка
Основные характеристики элемента <dialog>
скромны. Элемент будет автоматически скрыт, и в него будут встроены стили для наложения вашего контента.
<dialog> … </dialog>
Мы можем улучшить этот базовый уровень.
Традиционно элемент диалога имеет много общего с модальным элементом, и часто их имена взаимозаменяемы. Здесь я позволил себе использовать элемент диалога как для небольших всплывающих диалоговых окон (мини), так и для полностраничных диалогов (мега). Я назвал их «мега» и «мини», причем оба диалога слегка адаптированы для разных случаев использования. Я добавил атрибут 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>
Элемент диалога обеспечивает прочную основу для полноценного элемента области просмотра, который может собирать данные и взаимодействие с пользователем. Эти основы могут обеспечить очень интересные и эффективные взаимодействия на вашем сайте или в приложении.
Доступность
Элемент диалога имеет очень хорошую встроенную доступность. Вместо добавления этих функций, как я обычно делаю, многие из них уже есть.
Восстановление фокуса
Как мы это делали вручную при создании компонента Sidenav , важно, чтобы при открытии и закрытии чего-либо правильно фокусировался на соответствующих кнопках открытия и закрытия. Когда эта боковая панель открывается, фокус переключается на кнопку закрытия. При нажатии кнопки закрытия фокус возвращается к кнопке, которая ее открыла.
Для элемента диалога это встроенное поведение по умолчанию:
К сожалению, если вы хотите анимировать диалог, эта функциональность теряется. В разделе JavaScript я восстановлю эту функциональность.
Захват фокуса
Элемент диалога управляет inert
для вас документом. До появления inert
JavaScript использовался для отслеживания выхода фокуса из элемента, после чего он перехватывал и возвращал его обратно.
После inert
любые части документа могут быть «заморожены» настолько, что они больше не будут объектами фокуса или интерактивными с помощью мыши. Вместо захвата фокуса фокус направляется на единственную интерактивную часть документа.
Открыть и автоматически сфокусировать элемент
По умолчанию элемент диалога назначит фокус первому фокусируемому элементу в разметке диалога. Если это не лучший элемент для пользователя по умолчанию, используйте атрибут autofocus
. Как описано ранее, я считаю, что лучше всего размещать это на кнопке отмены, а не на кнопке подтверждения. Это гарантирует, что подтверждение будет преднамеренным, а не случайным.
Закрытие с помощью клавиши Escape
Важно облегчить закрытие этого потенциально мешающего элемента. К счастью, элемент диалога будет обрабатывать клавишу Escape за вас, освобождая вас от бремени оркестрации.
Стили
Существует простой путь к стилизации элемента диалога и сложный путь. Самый простой путь достигается за счет отказа от изменения свойства отображения диалогового окна и работы с его ограничениями. Я иду по сложному пути, чтобы предоставить пользовательские анимации для открытия и закрытия диалога, использования свойства display
и многого другого.
Стилизация с открытым реквизитом
Чтобы ускорить адаптивные цвета и обеспечить общую согласованность дизайна, я беззастенчиво ввел свою библиотеку переменных CSS Open Props . В дополнение к бесплатно предоставляемым переменным я также импортирую файл нормализации и несколько кнопок , обе из которых Open Props предоставляет в качестве необязательного импорта. Этот импорт помогает мне сосредоточиться на настройке диалога и демонстрации, не нуждаясь при этом в большом количестве стилей для их поддержки и придания им хорошего вида.
Стилизация элемента <dialog>
Владение свойством отображения
Поведение по умолчанию для отображения и скрытия элемента диалогового окна переключает свойство display с block
на none
. К сожалению, это означает, что его нельзя анимировать, только внутрь. Я хотел бы анимировать как внутрь, так и наружу, и первым шагом является установка моего собственного свойства отображения :
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
в надежде, что браузеры позволят переносить элемент backdrop в будущем:
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); } }
Стилизация кнопки закрытия заголовка
Поскольку в демо-версии используются кнопки «Открыть реквизиты», кнопка «Закрыть» превращается в кнопку с круглым значком, например:
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>
Элемент статьи играет в этом диалоге особую роль: это пространство, предназначенное для прокрутки в случае длинного или длинного диалога.
Для этого родительский элемент формы установил для себя некоторые максимумы, которые ограничивают достижение этого элемента статьи, если он становится слишком высоким. Установите 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
используется для размещения кнопок действий для диалогового окна. Он использует макет флексбокса с 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
элемента. Ранее руководство устанавливало для отображения сетку и никогда не устанавливало для нее значение «нет». Это открывает возможность включать и выводить анимацию.
Open Props поставляется с множеством анимаций по ключевым кадрам , что делает оркестровку простой и понятной. Вот цели анимации и многоуровневый подход, который я использовал:
- Уменьшенное движение — это переход по умолчанию, простое постепенное появление и исчезновение непрозрачности.
- Если движение в порядке, добавляется анимация скольжения и масштабирования.
- Адаптивный мобильный макет мегадиалога настроен на выдвижение.
Безопасный и содержательный переход по умолчанию
Хотя Open Props поставляется с ключевыми кадрами для постепенного появления и исчезновения, я предпочитаю этот многоуровневый подход переходов по умолчанию с анимацией ключевых кадров в качестве потенциального обновления. Ранее мы уже задавали видимость диалога непрозрачностью, присваивая значения 1
или 0
в зависимости от атрибута [open]
. Чтобы перейти от 0% к 100%, сообщите браузеру, как долго и какое замедление вы хотите:
dialog { transition: opacity .5s var(--ease-3); }
Добавление движения к переходу
Если пользователя устраивает движение, как мега-, так и мини-диалоги должны сдвигаться вверх при входе и уменьшаться при выходе. Этого можно добиться с помощью медиа-запроса prefers-reduced-motion
и нескольких открытых реквизитов:
@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; } }
Адаптация анимации выхода для мобильных устройств
Ранее в разделе стилей стиль мегадиалога адаптирован для мобильных устройств и больше похож на лист действий, как если бы небольшой лист бумаги выскользнул из нижней части экрана и все еще прикреплен к нижней части. Анимация выхода в масштабе не очень хорошо вписывается в этот новый дизайн, и мы можем адаптировать ее с помощью пары медиа-запросов и некоторых открытых реквизитов:
@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
. Затем прослушайте встроенное событие закрытия в диалоговом окне. Отсюда установите диалог на 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 , чтобы получить представление об изменении атрибутов диалога. В этом наблюдателе я буду следить за изменениями атрибута open и соответствующим образом управлять пользовательскими событиями.
Аналогично тому, как мы запускали события закрытия и закрытия, создайте два новых события с именами 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) } }) })
Функция обратного вызова наблюдателя мутаций будет вызываться при изменении атрибутов диалога, предоставляя список изменений в виде массива. Перебирайте изменения атрибута в поисках открытого attributeName
. Затем проверьте, имеет ли элемент атрибут или нет: это сообщает, стал ли диалог открытым. Если он был открыт, удалите атрибут 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) } }) }) })
Обратный вызов наблюдателя мутаций вызывается всякий раз, когда дочерние элементы добавляются или удаляются из тела документа. Конкретные наблюдаемые мутации относятся к removedNodes
, имеющим имя nodeName
диалога. Если диалоговое окно было удалено, события щелчка и закрытия удаляются, чтобы освободить память, и отправляется пользовательское удаленное событие.
Удаление атрибута загрузки
Чтобы анимация диалога не воспроизводила анимацию выхода при добавлении на страницу или при загрузке страницы, в диалог был добавлен атрибут загрузки. Следующий скрипт ожидает завершения анимации диалога, а затем удаляет атрибут. Теперь диалог можно свободно анимировать, и мы эффективно скрыли отвлекающую анимацию.
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
: содержит строку закрытия, передаваемую при вызове события диалога close()
. В событии dialogClosed
очень важно знать, был ли диалог закрыт, отменен или подтвержден. Если это подтверждено, сценарий затем захватывает значения формы и сбрасывает форму. Сброс полезен тем, что при повторном отображении диалогового окна оно оказывается пустым и готовым к новой отправке.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете.
Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
- @GrimLink с диалогом 3-в-1 .
- @mikemai2awesome с хорошим ремиксом , который не меняет свойство
display
. - @geoffrich_ с Svelte и красивым лаком Svelte FLIP .
Ресурсы
- Исходный код на Github
- Дудл-аватары