Создание меню "вне холста" с <dialog> и веб-компонентами

Источник: «Build an off-canvas menu with <dialog> and web components»
Меню "вне холста" — распространённый паттерн в веб-дизайне. Его часто можно увидеть на мобильных сайтах, где вы кликаете на кнопку гамбургер, и меню выдвигается сбоку экрана, обычно перекрывая контент, находящийся за ним.

Оглавление

Основываясь на общих сведениях о меню "вне холста", мы рассмотрим шаги по созданию доступного меню "вне холста" с помощью веб-компонентов и элемента <dialog>. Мы рассмотрим, как интегрировать это меню в ваш сайт, чтобы оно не только повышало удобство использования, но и соответствовало стандартам доступности.

Соображения доступности для меню "вне холста"

Несмотря на то, что создание меню "вне холста" — довольно простая задача, если рассматривать её поверхностно, она может стать сложной, если учесть доступность. Необходимо учитывать такие моменты, как:

Когда речь идёт о доступности, первое правило — использовать родные API браузера. В данном случае мы будем использовать элемент <dialog>.

Это позволит нам показывать содержимое элемента "вне холста" при нажатии на кнопку, предоставлять ловушку фокуса и сообщать всё, что нужно, вспомогательным технологиям. Это также означает, что нам не придётся самостоятельно поддерживать и обновлять все эти функции. В следующем разделе мы кратко рассмотрим, как создать диалоговое окно.

HTML для элемента <dialog>

Для HTML это просто: добавьте на страницу элемент <dialog>, добавьте кнопку, открывающую диалоговое окно, и, наконец, добавьте кнопку внутри окна, с помощью которой можно его закрыть:

<html>
<head></head>
<body>
All your HTML here
<button class="open-dialog">Open Dialog</dialog>
<dialog>
<button class="close-dialog">Close Dialog</dialog>
</dialog>
</body>
</html>

Примечание: по умолчанию для закрытия диалога можно нажать клавишу ESC, если в нем нет кнопки закрытия.

JavaScript для элемента <dialog>

JavaScript также довольно прост. Мы создаём переменные для диалогового окна, для кнопки открытия и для кнопки закрытия. Затем мы добавляем слушатель событий для каждой из кнопок, вызывающий методы showModal() или close() для диалога:

const dialog = document.querySelector('dialog');
const buttonOpen = document.querySelector('.open-dialog')
const buttonClose = document.querySelector('.close-dialog')

buttonOpen.addEventListener('click', function() {
dialog.showModal()
});
buttonClose.addEventListener('click', function() {
dialog.close()
});

Вы можете посмотреть демонстрацию этого базового элемента dialog на CodePen:

See the Pen

Теперь, когда элемент <dialog> создан, давайте нажмём на кнопку и посмотрим, что произойдёт. Сейчас диалог отображается в центре экрана. Это ожидаемое поведение, поскольку, когда мы обычно думаем о диалоге, мы думаем о модальной всплывающей панели, показывающей увеличенную версию изображения.

Однако в нашем случае мы хотим, чтобы меню показывалось анимировано сбоку экрана. Поэтому давайте добавим немного CSS, чтобы поместить его за пределы экрана.

Добавление CSS для позиционирования диалога "вне холста"

.dialog-menu,
.dialog-menu[open]
{
position: fixed;
width: 400px;
max-width: 80%;
min-height: 100vh;
margin: 0;
margin-left: auto;
transform: translateX(100%);
transition: .3s;
}

В CSS есть несколько интересных моментов. По умолчанию, когда диалог виден, он центрируется по горизонтали и вертикали, как если бы вы использовали display: flex для контейнера и margin: auto для самого элемента.

Чтобы диалог не располагался по центру экрана, мы установим margin: 0 для всех сторон, а затем margin-left: auto. Это гарантирует, что диалог будет расположен как можно дальше в правой части экрана. Затем мы используем position: fixed и transform: translateX(100%), чтобы сдвинуть диалог за пределы экрана на величину, равную его ширине (100% от его x/width).

Нам нужно, чтобы позиция: была фиксированной, иначе у нас появится горизонтальная прокрутка той же величины, что и ширина диалога. (Примечание: Мы могли бы использовать overflow-x: hidden для элемента body, но это слишком радикально и может привести к нежелательным последствиям).

Ширина установлена в 400px, но ограничена максимальной шириной в 80%. Это гарантирует, что когда диалог будет виден, он не будет занимать весь экран, и пользователи не будут удивляться, куда делся контент!

Далее, для высоты элемента установим значение не менее 100% от высоты экрана, так что если в меню много пунктов, мы сможем прокручивать его, но если их немного, диалог всё равно будет выглядеть хорошо.

Для приятного UX мы установили время перехода: 0,3 с, чтобы создать анимированный эффект для диалога, появляющегося на экране и исчезающего с него. Однако это пока не даёт никакого эффекта, потому что диалоги, которые показываются или скрываются, меняют состояние с display: none на display: block, и вы не можете анимировать переход из этих состояний. Но не волнуйтесь, у нас уже есть решение!

CSS для открытого диалогового окна

.dialog-menu[open] {
display: flex;
margin: 0;
margin-left: auto;
flex-direction: column;
transform: translateX(0);
transition: .3s;
}

.close-dialog {
margin-left: auto;
}

Когда диалоговое окно становится видимым, оно автоматически получает атрибут open. Мы будем использовать этот атрибут для стилизации, когда модальное окно становится видимым.

Нам нужно снова добавить margin: 0 и margin-left: auto, так как они сбрасываются при открытии диалога и получении атрибута open. Установим display: flex, а не display: block, задаваемый браузером. Я использую flex-direction: column next, а затем устанавливаю для кнопки .close-dialog margin-left: auto, чтобы она располагалась в правой части диалога (вы можете предпочесть использовать grid или какой-либо другой механизм).

Помните, мы установили transform: translateX в -100%, чтобы расположить его вне экрана. Теперь, чтобы он оказался на экране, мы вернём значение transform: translateX(0) и добавим время перехода, чтобы он анимировался при закрытии. Да, я знаю, анимация всё ещё не работает — мы займёмся этим дальше:

See the Pen

Анимация диалога "вне холста"

Мы не можем анимировать что-то из display: none в display: block и наоборот. Чтобы обойти это, выполним следующие действия, кликнув по кнопке, чтобы открыть диалог:

  1. Установим для <dialog> display: flex, чтобы его можно было анимировать.

  2. Используем setTimeout на небольшой промежуток времени, а затем вызываем метод showModal(), добавляющий атрибут [open]. Теперь, поскольку свойство display диалога имеет значение display: flex перед началом анимации, наш переход будет работать.

  3. Применяем CSS, относящийся к атрибуту [open], для анимации диалога.

    const dialogMenu = document.querySelector(".dialog-menu");
    const menuToggle = document.querySelector(".menu-toggle");

    menuToggle.addEventListener("click", () => {
    dialogMenu.style.display = "flex";
    setTimeout(() => {
    dialogMenu.showModal();
    }, 100);
    });

Когда нужно будет закрыть меню "вне холста", используем аналогичный подход.

Сначала добавим к диалогу класс .dialog-menu--closing. Затем мы можем использовать этот класс для запуска анимации отправки диалога за пределы экрана. Можно просто добавить этот класс в тот же CSS, который используется для стандартного .dialog-menu выше.

Далее используем ещё один setTimeout с очень коротким промежутком времени и используем его для:

const dialogMenu = document.querySelector(".dialog-menu");
const dialogMenuCloseButton = document.querySelector(".dialog__menu-close");
const menuToggle = document.querySelector(".menu-toggle");

dialogMenuCloseButton.addEventListener('click', () => {
dialogMenu.classList.add("dialog-menu--closing");
setTimeout(() => {
dialogMenu.close();
dialogMenu.style.display = "none";
dialogMenu.classList.remove("dialog-menu--closing");
}, 100);
}
.dialog-menu,
.dialog-menu[open].dialog-menu--closing
{
/* Тот же CSS, что и для .dialog-menu выше */
}

Теперь у нас есть симпатичный диалог "вне холста":

See the Pen

Создание меню в виде веб-компонента

В этом разделе мы будем использовать веб-компонент для создания меню, поскольку веб-компоненты идеально подходят для создания многократно используемых HTML-элементов. Важно отметить, что одно обновление меню автоматически распространяется и применяется по всему сайту, так что нам нужно обновить меню только один раз. То же самое можно сделать для многоразовых хедеров и футеров.

Наш веб-компонент — это простой компонент, создающий элемент nav и помещающий в него неупорядоченный список со ссылками для нашего меню. Мы прикрепляем его к теневому DOM и добавляем немного CSS:

class Menu extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}

connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
margin-top: var(--spacing);
}
li + li {
border-top: 1px solid white;
}
a {
display: block;
padding-block: 1rem;
text-decoration: none;
color: white;
}
</style>
<nav>
<ul>
<li><a href="https://example.com/about">About Us</a></li>
<li><a href="https://example.com/services">Our Services</a></li>
<li><a href="https://example.com/testimonials">Testimonials</a></li>
<li><a href="https://example.com/location">Directions</a></li>
<li><a href="https://example.com/contact">Contact Us</a></li>
</ul>
</nav>
`
;
}
}

customElements.define("custom-menu", Menu);

Теперь, когда мы создали веб-компонент, нужно поместить его внутрь элемента <dialog>:

<dialog class="dialog-menu">
<button class="close-dialog">Close Dialog</button>
<custom-menu></custom-menu>
</dialog>

Вот полная версия:

See the Pen

Чему мы научились

В этой статье мы узнали, как использовать нативные HTML-теги и веб API для создания перспективного сайта. Также узнали, что элемент <dialog> предназначен не только для создания модальных окон, но и что его можно анимировать из любого места и в любом месте сайта, если подойти творчески. Наконец, мы рассмотрели создание веб-компонентов, позволяющих создать компонент один раз и использовать его повсюду на веб-странице.

Дополнительные материалы

Предыдущая Статья

Нетерпеливая загрузка может быть вредна

Следующая Статья

Git: Поддерживайте чистоту ветви с помощью fixup и autosquash