关于如何构建自适应且易于访问的 Toast 组件的基础概览。
在这篇博文中,我想分享有关如何构建 Toast 组件的一些想法。试用演示版。
如果您更喜欢视频,可以观看此帖子的 YouTube 版本:
概览
Toast 是面向用户的非互动式被动异步短消息。 通常,它们用作界面反馈模式,用于告知用户操作结果。
互动次数
Toast 不同于通知、提醒和提示,因为它们不是互动式的;它们不应被关闭或保持显示状态。通知用于更重要的信息、需要互动的同步消息或系统级消息(与网页级消息相对)。 与其他通知策略相比,Toast 更加被动。
Markup
<output>
元素非常适合用于 Toast,因为屏幕阅读器会播报它。正确的 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>
Toast 容器
可以同时显示多个 Toast。为了编排多个 Toast,使用了一个容器。此容器还负责处理屏幕上 Toast 的位置。
<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>
布局
我选择将 Toast 消息固定到视口的 inset-block-end
,如果添加更多 Toast 消息,它们会从该屏幕边缘堆叠。
GUI 容器
Toast 容器负责完成所有用于显示 Toast 的布局工作。它相对于视口为 fixed
,并使用逻辑属性 inset
来指定要固定到哪些边缘,以及来自同一 block-end
边缘的少量 padding
。
.gui-toast-group {
position: fixed;
z-index: 1;
inset-block-end: 0;
inset-inline: 0;
padding-block-end: 5vh;
}
除了在视口内定位自身之外,Toast 容器还是一个可以对 Toast 进行对齐和分布的网格容器。项目以 justify-content
为一组居中,并以 justify-items
单独居中。
添加少量 gap
,以免 Toast 相互接触。
.gui-toast-group {
display: grid;
justify-items: center;
justify-content: center;
gap: 1vh;
}
GUI Toast
单个 Toast 具有一些 padding
、一些带有 border-radius
的较柔和的边角,以及一个 min()
函数,可帮助调整移动设备和桌面设备的大小。以下 CSS 中的自适应尺寸可防止 Toast 宽度超过视口的 90% 或 25ch
。
.gui-toast {
max-inline-size: min(25ch, 90vw);
padding-block: .5ch;
padding-inline: 1ch;
border-radius: 3px;
font-size: 1rem;
}
样式
设置布局和定位后,添加有助于适应用户设置和互动的 CSS。
Toast 容器
Toast 不具有互动性,点按或滑动它们不会执行任何操作,但它们目前会消耗指针事件。使用以下 CSS 防止 Toast 窃取点击。
.gui-toast-group {
pointer-events: none;
}
GUI Toast
为 Toast 提供具有自定义属性、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%;
}
}
动画
新 Toast 应在进入屏幕时以动画形式呈现。
为了适应减少的运动,默认情况下会将 translate
值设置为 0
,但会将运动值更新为运动偏好设置媒体查询中的长度。所有用户都会看到一些动画,但只有部分用户会看到 Toast 移动一段距离。
以下是用于 Toast 动画的关键帧。CSS 将在一个动画中控制 Toast 的进入、等待和退出。
@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 来根据用户事件协调 Toast 的创建、添加和销毁。Toast 组件的开发者体验应尽可能简单,便于上手,如下所示:
import Toast from './toast.js'
Toast('My first toast')
创建 Toast 群组和 Toast
当 Toast 模块从 JavaScript 加载时,必须创建一个 Toast 容器并将其添加到网页中。我选择在 body
之前添加元素,这样可以避免 z-index
堆叠问题,因为容器位于所有正文元素的容器之上。
const init = () => {
const node = document.createElement('section')
node.classList.add('gui-toast-group')
document.firstElementChild.insertBefore(node, document.body)
return node
}
init()
函数在模块内部调用,将元素存储为 Toaster
:
const Toaster = init()
Toast HTML 元素的创建是通过 createToast()
函数完成的。该函数需要一些文本作为 Toast 的内容,创建一个 <output>
元素,使用一些类和属性修饰该元素,设置文本,然后返回节点。
const createToast = text => {
const node = document.createElement('output')
node.innerText = text
node.classList.add('gui-toast')
node.setAttribute('role', 'status')
return node
}
管理一个或多个 Toast
JavaScript 现在会向文档添加一个用于包含 Toast 的容器,并准备好添加创建的 Toast。addToast()
函数用于编排一个或多个 Toast 的处理。首先检查 Toast 的数量以及动画是否正常,然后使用此信息附加 Toast 或执行一些精美的动画,使其他 Toast 看起来像是为新 Toast“腾出空间”。
const addToast = toast => {
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Toaster.children.length && motionOK
? flipToast(toast)
: Toaster.appendChild(toast)
}
添加第一个 Toast 时,Toaster.appendChild(toast)
会向网页添加一个触发 CSS 动画的 Toast:动画进入、等待 3s
、动画退出。
当存在现有 Toast 时,系统会调用 flipToast()
,采用 Paul Lewis 提出的 FLIP 技术。其思路是计算添加新 Toast 之前和之后容器位置的差异。您可以将此视为标记烤面包机现在的位置、将要到达的位置,然后从烤面包机之前的位置到当前位置进行动画处理。
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 网格负责布局的提升。添加新 Toast 时,网格会将其放在开头,并与其他 Toast 隔开。与此同时,使用 Web 动画将容器从旧位置动画化。
将所有 JavaScript 代码整合在一起
调用 Toast('my first toast')
时,系统会创建一个 Toast,并将其添加到网页中(可能还会为容器添加动画效果,以容纳新的 Toast),然后返回一个 Promise,并监听所创建 Toast 的 CSS 动画完成情况(三个关键帧动画),以进行 Promise 解析。
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()
映射。由于我为 Toast 使用了多个关键帧动画,为了确信所有动画都已完成,必须通过 JavaScript 请求每个动画,并观察每个动画的 finished
promise 是否已完成。allSettled
为我们完成这项工作,并在其所有 promise 都已兑现后自行解析为已完成。使用 await Promise.allSettled()
意味着下一行代码可以放心地移除该元素,并假设 Toast 已完成其生命周期。最后,调用 resolve()
会兑现高级别的 Toast promise,以便开发者在显示 Toast 后进行清理或其他工作。
export default Toast
最后,Toast
函数从模块中导出,以供其他脚本导入和使用。
使用 Toast 组件
使用 Toast 或 Toast 的开发者体验,需要导入 Toast
函数并使用消息字符串调用该函数。
import Toast from './toast.js'
Toast('Wizard Rose added to cart')
如果开发者想在显示 Toast 消息后执行清理工作或其他操作,可以使用 async 和 await。
import Toast from './toast.js'
async function example() {
await Toast('Wizard Rose added to cart')
console.log('toast finished')
}
总结
现在您已经知道我是如何做到的,那么您会怎么做呢?🙂
让我们丰富方法,了解在网络上构建内容的所有方式。 制作演示视频,通过 Twitter 向我发送链接,我会将其添加到下方的社区混音部分!
社区混音作品
- @_developit 使用 HTML/CSS/JS:演示和代码
- Joost van der Schee 的 HTML/CSS/JS:演示和代码