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

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

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

Демо

Если вы предпочитаете видео, вот версия этого поста на 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-сетки на группу всплывающих уведомлений, на этот раз выделено пространство и промежутки между дочерними элементами всплывающего уведомления.

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

Отдельный тост имеет 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;
}

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

Придайте всплывающим сообщениям светлую или темную адаптивную тему с настраиваемыми свойствами, 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 по умолчанию, но обновления значения движения до длины в медиазапросе предпочтений движения. У всех есть анимация, но только у некоторых пользователей тост перемещается на большое расстояние.

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

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

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

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

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

.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')

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

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

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-элемента Toast выполняется с помощью функции 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-сетка поднимает макет. Когда добавляется новый тост, Grid помещает его в начало и размещает между остальными. Между тем, для анимации контейнера из старой позиции используется веб-анимация .

Собираем весь 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')
}

Заключение

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

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

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