构建消息框组件

简要介绍如何构建可访问的自适应消息框组件。

在这篇博文中,我想分享一下如何构建消息框组件的想法。试用 演示

<ph type="x-smartling-placeholder">
</ph>
演示

如果你更喜欢视频,可以参阅此博文的 YouTube 版本:

概览

消息框是为用户提供的非交互式、被动的异步简短消息。 它们通常用作通知用户的界面反馈模式 操作结果的相关信息。

互动次数

消息框与通知不同 提醒提示 它们不具备互动性;它们不会被关闭或保留。 通知用于提供更重要的信息, 需要互动或系统级消息(而非网页级)。 消息框比其他通知策略更被动。

Markup

通过 <output> 元素是适用于消息框的不错选择,因为它会在 读者。正确的 HTML 为我们使用 JavaScript 和 并且将会用到大量 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 如果添加了更多消息框,这些消息框就会从屏幕边缘堆叠在一起。

GUI 容器

消息框容器会完成呈现消息框的所有布局工作。时间是 将 fixed 附加到视口,并使用逻辑属性 inset 用于指定 以及距同一 block-end 边缘的一点 padding

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

屏幕截图:开发者工具框大小和内边距叠加在 .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 以适应用户 设置和互动。

消息框容器

消息框没有互动性,点按或滑动消息框没有任何作用, 它们目前都使用指针事件防止消息框窃取信息 点击。

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

然后,消息框元素会设置变量并编排关键帧。

.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 堆叠问题 所有 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> 元素,adorns 一些类和属性,设置文本,并返回节点。

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() Paul 演唱的《FLIP》 Lewis。其目的是计算 添加新消息框之前和之后的位置。 可以想象成在烤面包机现在的位置标记 然后以动画的形式呈现动画效果

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') 时,系统会创建一个消息框并将其添加到页面 (或许甚至会对容器进行动画处理以适应新的消息框), promise 返回创建的消息框 观看了 用于 promise 解析的 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 会观察到用于完成的 promise。 allSettled 它对我们有效吗?一旦其所有 promise 都自动完成, 已执行。使用 await Promise.allSettled() 表示 代码可以放心地移除该元素,并假设消息框已完成其 生命周期最后,调用 resolve() 会执行高级消息框 promise, 开发者可以在消息框显示后清理或执行其他操作。

export default Toast

最后,从模块中导出 Toast 函数,以供其他脚本 导入和使用。

使用消息框组件

使用消息框或消息框的开发者体验是通过导入 Toast 函数并通过消息字符串调用它。

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

如果开发者想要在消息框之后执行清理工作或执行其他操作, 它们可以使用异步 await

import Toast from './toast.js'

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

总结

现在您已经知道我是怎么做到的了,您该怎么做 ‽ 🙂?

让我们一起采用多样化的方法,学习所有在 Web 上构建应用的方法。 创建演示,在 Twitter 微博上添加链接,然后我会添加 到下面的社区混剪部分!

社区混剪作品