Создание компонента тоста

Базовый обзор того, как создать адаптивный и доступный компонент тоста.

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

Демо

Если вы предпочитаете видео, вот версия этого поста на YouTube:

Обзор

Тосты — это неинтерактивные, пассивные и асинхронные короткие сообщения для пользователей. Как правило, они используются в качестве шаблона обратной связи интерфейса для информирования пользователя о результатах действия.

Взаимодействия

Всплывающие уведомления отличаются от уведомлений, оповещений и подсказок тем, что они неинтерактивны; их не следует закрывать или сохранять. Уведомления предназначены для более важной информации, синхронных сообщений, требующих взаимодействия, или сообщений системного уровня (в отличие от сообщений уровня страницы). Всплывающие уведомления более пассивны, чем другие стратегии уведомлений.

Разметка

Элемент <output> — хороший выбор для тоста, поскольку он анонсируется для программ чтения с экрана. Корректный HTML-код обеспечивает надёжную основу для улучшения с помощью JavaScript и CSS, а JavaScript будет много.

Тост

<output class="gui-toast">Item added to cart</output>

Его можно сделать более инклюзивным, добавив role="status" . Это обеспечивает запасной вариант, если браузер не назначает элементам <output> неявную роль согласно спецификации.

<output role="status" class="gui-toast">Item added to cart</output>

Контейнер для тостов

Одновременно может отображаться несколько тостов. Для организации нескольких тостов используется контейнер. Этот контейнер также управляет положением тостов на экране.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

Макеты

Я решил прикрепить уведомления к inset-block-end просмотра, и если будут добавлены дополнительные уведомления, они будут размещаться с этого края экрана.

Контейнер графического интерфейса

Контейнер тостов выполняет всю работу по макетированию для отображения тостов. Он fixed к области просмотра и использует логическое свойство inset для указания краёв, к которым его нужно прикрепить, а также небольшой padding от того же края block-end .

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Скриншот с размером и отступом блока DevTools, наложенными на элемент .gui-toast-container.

Помимо позиционирования в области просмотра, контейнер тоста представляет собой сетку, которая позволяет выравнивать и распределять тосты. Элементы выравниваются по центру всей группой с помощью justify-content и по отдельности с помощью justify-items . Оставьте небольшой gap , чтобы тосты не соприкасались.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Скриншот с наложением CSS-сетки на группу уведомлений, на этот раз с выделением пространства и зазоров между дочерними элементами уведомлений.

GUI-тост

У каждого тоста есть padding , сглаженные углы с border-radius и функция min() для адаптации размера к мобильным устройствам и компьютерам. Адаптивный размер в следующем CSS-коде предотвращает расширение тоста более чем на 90% от области просмотра или 25ch .

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Скриншот одного элемента .gui-toast с отступами и радиусом рамки.

Стили

После настройки макета и позиционирования добавьте CSS, который поможет адаптироваться к настройкам и взаимодействиям пользователя.

Контейнер для тостов

Всплывающие уведомления неинтерактивны, нажатие или свайп по ним ничего не даёт, но в настоящее время они потребляют события указателя. Предотвратите кражу кликов уведомлениями с помощью следующего CSS-кода.

.gui-toast-group {
  pointer-events: none;
}

GUI-тост

Придайте тостам светлую или темную адаптивную тему с помощью пользовательских свойств, HSL и предпочитаемого медиа-запроса.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Анимация

Новое уведомление должно появляться на экране с анимацией. Для компенсации уменьшения движения по умолчанию значения параметра translate устанавливаются равными 0 , а значение параметра Motion обновляется до заданной длины в медиазапросе настроек движения. Анимация доступна всем пользователям, но только у некоторых пользователей уведомление перемещается на определённое расстояние.

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

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

Затем элемент тоста настраивает переменные и организует ключевые кадры.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

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

import Toast from './toast.js'

Toast('My first toast')

Создание тостовой группы и тостов

При загрузке модуля toast из JavaScript он должен создать контейнер toast и добавить его на страницу. Я решил добавить элемент перед body , это снизит вероятность проблем с наложением z-index поскольку контейнер находится выше контейнера для всех элементов body.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Скриншот группы тостов между тегами head и body.

Функция init() вызывается внутри модуля, сохраняя элемент как Toaster :

const Toaster = init()

HTML-элемент тоста создается с помощью функции createToast() . Функция принимает текст для тоста, создает элемент <output> , добавляет к нему классы и атрибуты, задает текст и возвращает узел.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Управление одним или несколькими тостами

JavaScript теперь добавляет в документ контейнер для уведомлений и готов к добавлению созданных уведомлений. Функция addToast() управляет обработкой одного или нескольких уведомлений. Сначала проверяется количество уведомлений и возможность их перемещения, а затем эта информация используется для добавления уведомления в конец документа или создания эффектной анимации, которая позволяет другим уведомлениям «освободить место» для нового.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

При добавлении первого уведомления Toaster.appendChild(toast) добавляет уведомление на страницу, запуская анимацию CSS: анимация появления, ожидание 3s , анимация исчезновения. flipToast() вызывается при наличии уже существующих уведомлений, используя технику FLIP Пола Льюиса . Идея заключается в вычислении разницы в положении контейнера до и после добавления нового уведомления. Представьте, что вы отмечаете текущее положение тостера, его положение в будущем, а затем анимируете его переход из текущего положения в текущее.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

CSS-сетка поднимает макет. При добавлении нового уведомления она помещает его в начало и размещает между остальными. При этом веб-анимация используется для анимации контейнера из старого положения.

Собираем весь JavaScript вместе

При вызове Toast('my first toast') создается тост, добавляется на страницу (возможно, даже контейнер анимируется для размещения нового тоста), возвращается обещание , и созданное уведомление отслеживается на предмет завершения анимации CSS (три анимации ключевых кадров) для разрешения обещания.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

Мне показалось, что запутанная часть этого кода находится в функции Promise.allSettled() и сопоставлении toast.getAnimations() . Поскольку я использовал несколько анимаций ключевых кадров для уведомления, чтобы быть уверенным в том, что все они завершены, необходимо запросить каждую из них из JavaScript и наблюдать за выполнением каждого из их finished обещаний. allSettled делает это за нас, разрешая себя как завершённый после выполнения всех своих обещаний. Использование await Promise.allSettled() означает, что следующая строка кода может уверенно удалить элемент и считать, что уведомление завершило свой жизненный цикл. Наконец, вызов resolve() выполняет высокоуровневое обещание Toast, чтобы разработчики могли очистить его или выполнить другие действия после отображения уведомления.

export default Toast

Наконец, функция Toast экспортируется из модуля для импорта и использования другими скриптами.

Использование компонента Toast

Использование тоста или интерфейса разработчика тоста осуществляется путем импорта функции Toast и ее вызова со строкой сообщения.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Если разработчику нужно выполнить очистку или что-то еще после отображения уведомления, он может использовать async и await .

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Заключение

Теперь, когда вы знаете, как я это сделал, как бы вы поступили? 🙂

Давайте разнообразим наши подходы и изучим все способы разработки в интернете. Создайте демо, пришлите мне ссылку в Твиттер , и я добавлю её в раздел ремиксов сообщества ниже!

Ремиксы сообщества