Базовый обзор того, как создавать цветоадаптивные, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <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
- Дудл-аватары
Базовый обзор того, как создавать цветоадаптивные, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <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
- Дудл-аватары