Toast
<script setup lang="ts">
import { ToastAction, ToastDescription, ToastProvider, ToastRoot, ToastTitle, ToastViewport } from 'reka-ui'
import { ref } from 'vue'
const open = ref(false)
const eventDateRef = ref(new Date())
const timerRef = ref(0)
function oneWeekAway() {
const now = new Date()
const inOneWeek = now.setDate(now.getDate() + 7)
return new Date(inOneWeek)
}
function prettyDate(date: Date) {
return new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'short' }).format(date)
}
function handleClick() {
open.value = false
window.clearTimeout(timerRef.value)
timerRef.value = window.setTimeout(() => {
eventDateRef.value = oneWeekAway()
open.value = true
}, 100)
}
</script>
<template>
<ToastProvider>
<button
class="inline-flex items-center justify-center rounded-md border font-medium text-sm px-[15px] leading-[35px] h-[35px] bg-white text-stone-700 outline-none hover:bg-mauve3 shadow-sm focus:shadow-[0_0_0_2px] focus:shadow-black"
@click="handleClick"
>
Add to calendar
</button>
<ToastRoot
v-model:open="open"
class="bg-white rounded-lg shadow-sm border p-[15px] grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-slideIn data-[state=closed]:animate-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-swipeOut"
>
<ToastTitle class="[grid-area:_title] mb-[5px] font-medium text-slate12 text-sm">
Scheduled: Catch up
</ToastTitle>
<ToastDescription as-child>
<time
class="[grid-area:_description] m-0 text-slate11 text-xs leading-[1.3]"
:dateTime="eventDateRef.toISOString()"
>
{{ prettyDate(eventDateRef) }}
</time>
</ToastDescription>
<ToastAction
class="[grid-area:_action]"
as-child
alt-text="Goto schedule to undo"
>
<button class="inline-flex items-center justify-center rounded-md font-medium text-xs px-[10px] leading-[25px] h-[25px] bg-green-200 text-green-900 shadow-[inset_0_0_0_1px] shadow-green-700 hover:shadow-[inset_0_0_0_1px] hover:shadow-green-800 focus:shadow-[0_0_0_2px] focus:shadow-green-800">
Undo
</button>
</ToastAction>
</ToastRoot>
<ToastViewport class="[--viewport-padding:_25px] fixed bottom-0 right-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[2147483647] outline-none" />
</ToastProvider>
</template>
功能特点
功能特点
- 自动关闭。
- 在悬停、聚焦和窗口失焦时暂停关闭。
- 支持热键跳转到 Toast 视口。
- 支持通过滑动关闭手势。
- 暴露 CSS 变量用于滑动关闭手势动画。
- 可控或不可控。
安装
从命令行安装组件。
$ npm add reka-ui
$ pnpm add reka-ui
$ yarn add reka-ui
$ bun add reka-ui
结构
导入组件。
<script setup lang="ts">
import { ToastAction, ToastClose, ToastDescription, ToastProvider, ToastRoot, ToastTitle, ToastViewport } from 'reka-ui'
</script>
<template>
<ToastProvider>
<ToastRoot>
<ToastTitle />
<ToastDescription />
<ToastAction />
<ToastClose />
</ToastRoot>
<ToastViewport />
</ToastProvider>
</template>
API 参考
Provider
包裹 Toast 和 Toast 视口的提供者。它通常包裹整个应用程序。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
duration | 5000 | number | 每个 Toast 保持可见的时间(毫秒)。 |
label | 'Notification' | string | 每个 Toast 的作者本地化标签。用于帮助屏幕阅读器用户将中断与 Toast 相关联。 |
swipeDirection | 'right' | 'right' | 'left' | 'up' | 'down' | 关闭 Toast 的指针滑动方向。 |
swipeThreshold | 50 | number | 滑动必须经过的像素距离才能触发关闭。 |
Viewport
Toast 出现的固定区域。用户可以通过按下热键跳转到视口。您需要确保键盘用户可以发现热键。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
as | 'ol' | AsTag | Component | 此组件应渲染为的元素或组件。可以通过 asChild 覆盖。 |
asChild | false | boolean | 将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南。 |
hotkey | ['F8'] | string[] | 用作键盘快捷键的按键,将焦点移动到 Toast 视口。 |
label | 'Notifications ({hotkey})' | string | ((hotkey: string) => string) | Toast 视口的作者本地化标签,用于在导航页面地标时为屏幕阅读器用户提供上下文。可用的 {hotkey} 占位符将替换为您。或者,您可以传入一个自定义函数来生成标签。 |
Root
自动关闭的 Toast。不应保持打开以 获取用户响应。
Presence 组件构建 - 支持任何动画技术,同时保持对存在(presence)触发事件的访问。| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
as | 'li' | AsTag | Component | 此组件应渲染为的元素或组件。可以通过 asChild 覆盖。 |
asChild | false | boolean | 将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南。 |
defaultOpen | true | boolean | 对话框首次渲染时的打开状态。当您不需要控制其打开状态时使用。 |
duration | number | Toast 保持可见的时间(毫秒)。覆盖传递给 ToastProvider 的值。 | |
forceMount | false | boolean | 当需要更多控制时,用于强制挂载。在与 Vue 动画库控制动画时很有用。 |
open | true | boolean | 对话框的受控打开状态。可以绑定为 v-model:open。 |
type | 'foreground' | 'foreground' | 'background' | 控制 Toast 的辅助功能敏感度。对于用户操作产生的 Toast,选择 foreground。后台任务产生的 Toast 应使用 background。 |
触发事件 (Emit)
| 事件名称 | Payload | 描述 |
|---|---|---|
escapeKeyDown | [event: KeyboardEvent] | Escape 键按下时调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。 |
pause | [] | 销毁计时器暂停时调用的事件处理程序。当指针移到视口上、视口聚焦或窗口失焦时发生。 |
resume | [] | 销毁计时器恢复时调用的事件处理程序。当指针移出视口、视口失焦或窗口聚焦时发生。 |
swipeCancel | [event: SwipeEvent] | 滑动交互取消时调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。 |
swipeEnd | [event: SwipeEvent] | 滑动交互结束时调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。 |
swipeMove | [event: SwipeEvent] | 滑动交互期间调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。 |
swipeStart | [event: SwipeEvent] | 滑动交互开始时调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。 |
update:open | [value: boolean] | 打开状态更改时调用的事件处理程序。 |
默认插槽
| 插槽参数 | 插槽参数类型 | 描述 |
|---|---|---|
open | boolean | 当前打开状态 |
remaining | number | 剩余时间(毫秒) |
duration | number | Toast 将保持可见的总时间(毫秒) |
数据属性
| 属性 | 值 |
|---|---|
[data-state] | "open" | "closed" |
[data-swipe] | "start" | "move" | "cancel" | "end" |
[data-swipe-direction] | "up" | "down" | "left" | "right" |
CSS 变量
| 变量 | 描述 |
|---|---|
--reka-toast-swipe-move-x | 水平滑动时 Toast 的偏移位置 |
--reka-toast-swipe-move-y | 垂直滑动时 Toast 的偏移位置 |
--reka-toast-swipe-end-x | 水平滑动后 Toast 的结束偏移位置 |
--reka-toast-swipe-end-y | 垂直滑动后 Toast 的结束偏移位置 |
Portal
使用时,将内容部分传送到 body 中。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
defer | false | boolean | 延迟解决 Teleport 目标,直到应用程序的其他部分挂载(需要 Vue 3.5.0+)。 |
disabled | false | boolean | 禁用 teleport 并在行内渲染组件。 |
forceMount | false | boolean | 当需要更多控制时,用于强制挂载。在与 Vue 动画库控制动画时很有用。 |
to | string | HTMLElement | Vue 原生 teleport 组件 prop :to。 |
Title
Toast 的可选标题。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
as | 'div' | AsTag | Component | 此组件应渲染为的元素或组件。可以通过 asChild 覆盖。 |
asChild | false | boolean | 将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南。 |
Description
Toast 消息。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
as | 'div' | AsTag | Component | 此组件应渲染为的元素或组件。可以通过 asChild 覆盖。 |
asChild | false | boolean | 将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南。 |
Action
一个可以安全忽略的操作,以确保用户不会因为 时间限制 而完成具有意外副作用的任务。当需要用户响应时,将一个 AlertDialog 样式化的 Toast 传送到视口中。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
altText* | string | 描述执行操作的替代方法的简短说明。适用于无法轻松/快速导航到按钮的屏幕阅读器用户。 | |
as | 'div' | AsTag | Component | 此组件应渲染为的元素或组件。可以通过 asChild 覆盖。 |
asChild | false | boolean | 将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南。 |
Close
允许用户在 Toast 持续时间结束前关闭 Toast 的按钮。
| 属性 | 默认值 | 类型 | 描述 |
|---|---|---|---|
as | 'button' | AsTag | Component | 此组件应渲染为的元素或组件。可以通过 asChild 覆盖。 |
asChild | false | boolean | 将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南。 |
示例
自定义热键
使用每个按键的 keycode.info 中的 event.code 值覆盖默认热键。
<template>
<ToastProvider>
...
<ToastViewport :hotkey="['altKey', 'KeyT']" />
</ToastProvider>
</template>
自定义持续时间
自定义 Toast 的持续时间以覆盖提供者值。
<template>
<ToastRoot :duration="3000">
<ToastDescription>Saved!</ToastDescription>
</ToastRoot>
</template>
重复 Toast
当 Toast 必须在用户每次点击按钮时出现时,使用状态渲染同一 Toast 的多个实例(参见下文)。或者,您可以抽象这些部分以创建自己的命令式 API。
<template>
<div>
<form @submit="count++">
...
<button>save</button>
</form>
<ToastRoot v-for="(_, index) in count" :key="index">
<ToastDescription>Saved!</ToastDescription>
</ToastRoot>
</div>
</template>
动画滑动手势
将 --reka-toast-swipe-move-[x|y] 和 --reka-toast-swipe-end-[x|y] CSS 变量与 data-swipe="[start|move|cancel|end]" 属性结合使用,以动画化滑动关闭手势。这是一个示例:
<template>
<ToastProvider swipe-direction="right">
<ToastRoot class="ToastRoot">
...
</ToastRoot>
<ToastViewport />
</ToastProvider>
</template>
.ToastRoot[data-swipe='move'] {
transform: translateX(var(--reka-toast-swipe-move-x));
}
.ToastRoot[data-swipe='cancel'] {
transform: translateX(0);
transition: transform 200ms ease-out;
}
.ToastRoot[data-swipe='end'] {
animation: slideRight 100ms ease-out;
}
@keyframes slideRight {
from {
transform: translateX(var(--reka-toast-swipe-end-x));
}
to {
transform: translateX(100%);
}
}
可访问性
遵循 aria-live 要求。
敏感度
使用 type prop 控制 Toast 对屏幕阅读器的敏感度。
对于用户操作产生的 Toast,选择 foreground。后台任务产生的 Toast 应使用 background。
前景 (Foreground)
前景 Toast 会立即宣布。当前景 Toast 出现时,辅助技术可能会选择清除之前排队的消息。尽量避免同时堆叠不同的前景 Toast。
后台 (Background)
后台 Toast 会在下一个适当的时机宣布,例如,当屏幕阅读器完成阅读当前句子时。它们不会清除排队的消息,因此过度使用它们在响应用户交互时可能会被屏幕阅读器用户视为滞后的用户体验。
<template>
<ToastRoot type="foreground">
<ToastDescription>File removed successfully.</ToastDescription>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
<ToastRoot type="background">
<ToastDescription>We've just released Reka UI 2.0.</ToastDescription>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
</template>
替代操作
在 Action 上使用 altText prop 来指示屏幕阅读器用户执行 Toast 操作的替代方法。
您可以将用户引导到应用程序中一个永久的位置,他们可以在那里执行操作,或者实现您自己的自定义热键逻辑。如果实现后者,请使用 foreground 类型立即宣布并增加持续时间,以给用户充足的时间。
<template>
<ToastRoot type="background">
<ToastTitle>Upgrade Available!</ToastTitle>
<ToastDescription>We've just released Reka UI 2.0.</ToastDescription>
<ToastAction alt-text="Goto account settings to upgrade">
Upgrade
</ToastAction>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
<ToastRoot type="foreground" :duration="10000">
<ToastDescription>File removed successfully.</ToastDescription>
<ToastAction alt-text="Undo (Alt+U)">
Undo <kbd>Alt</kbd>+<kbd>U</kbd>
</ToastAction>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
</template>
关闭图标按钮
当提供图标(或字体图标)时,请记住为屏幕阅读器用户正确标记它。
<template>
<ToastRoot type="foreground">
<ToastDescription>Saved!</ToastDescription>
<ToastClose aria-label="Close">
<span aria-hidden="true">×</span>
</ToastClose>
</ToastRoot>
</template>
键盘交互
| 按键 | 描述 |
|---|---|
F8 | 聚焦 Toast 视口。 |
Tab | 将焦点移动到下一个可聚焦元素。 |
Shift + Tab | 将焦点移动到上一个可聚焦元素。 |
Space | 当焦点位于 ToastAction 或 ToastClose 上时,关闭 Toast。 |
Enter | 当焦点位于 ToastAction 或 ToastClose 上时,关闭 Toast。 |
Esc | 当焦点位于 Toast 上时,关闭 Toast。 |
自定义 API
抽象部分
通过将原始部分抽象到您自己的组件中来创建您自己的 API。
用法
<script setup lang="ts">
import Toast from './your-toast.vue'
</script>
<template>
<Toast
title="Upgrade available"
content="We've just released Radix 3.0!"
>
<button @click="handleUpgrade">
Upgrade
</button>
</Toast>
</template>
实现
// your-toast.vue
<script setup lang="ts">
import { ToastAction, ToastClose, ToastDescription, ToastRoot, ToastTitle } from 'reka-ui'
defineProps<{
title: string
content: string
}>()
</script>
<template>
<ToastRoot>
<ToastTitle v-if="title">
{{ title }}
</ToastTitle>
<ToastDescription v-if="content">
{{ content }}
</ToastDescription>
<ToastAction
as-child
alt-text="toast"
>
<slot />
</ToastAction>
<ToastClose aria-label="Close">
<span aria-hidden="true">×</span>
</ToastClose>
</ToastRoot>
</template>
命令式 API
如果需要,创建自己的命令式 API 以允许Toast 重复。
用法
<script setup lang="ts">
import Toast from './your-toast.vue'
const savedRef = ref<InstanceType<typeof Toast>>()
</script>
<template>
<div>
<form @submit="savedRef.publish()">
...
</form>
<Toast ref="savedRef">
Saved successfully!
</Toast>
</div>
</template>
实现
<script setup lang="ts">
import { ToastClose, ToastDescription, ToastRoot, ToastTitle } from 'reka-ui'
import { ref } from 'vue'
const count = ref(0)
function publish() {
count.value++
}
defineExpose({
publish
})
</script>
<template>
<ToastRoot
v-for="index in count"
:key="index"
>
<ToastDescription>
<slot />
</ToastDescription>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
</template>