backdrop
组件

Toast

一条临时显示的简短消息。
  • 自动关闭。
  • 悬停、聚焦和窗口失焦时暂停关闭。
  • 支持快捷键跳转到消息视口。
  • 支持通过滑动手势关闭。
  • 公开用于滑动手势动画的 CSS 变量。
  • 可控或不可控。

安装

在命令行中安装组件。

sh
$ npm add reka-ui

结构

导入组件。

vue
<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

包装消息提示和消息视口的提供者。它通常包装整个应用。

<!- @include: @/meta/ToastProvider.md ->

Viewport

消息提示出现的固定区域。用户可以通过快捷键跳转到此视口。你需要确保键盘用户能够发现此快捷键。

<!- @include: @/meta/ToastViewport.md ->

Root

自动关闭的消息提示。不应为了获取用户响应而保持打开状态。

tip
Built with Presence component - supports any animation techniques while maintaining access to presence emitted events.

<!- @include: @/meta/ToastRoot.md ->

Data AttributeValue
[data-state]"open" | "closed"
[data-swipe]"start" | "move" | "cancel" | "end"
[data-swipe-direction]"up" | "down" | "left" | "right"
CSS VariableDescription
--reka-toast-swipe-move-x
水平滑动时消息提示的偏移位置
--reka-toast-swipe-move-y
垂直滑动时消息提示的偏移位置
--reka-toast-swipe-end-x
水平滑动后消息提示的最终偏移位置
--reka-toast-swipe-end-y
垂直滑动后消息提示的最终偏移位置

Portal

使用时会将要呈现的内容传送至 body 中。

<!- @include: @/meta/ToastPortal.md ->

Title

消息提示的可选标题

<!- @include: @/meta/ToastTitle.md ->

Description

消息提示的内容。

<!- @include: @/meta/ToastDescription.md ->

Action

一个可以安全忽略的操作,确保用户不会因时间限制而执行具有意外副作用的操作。

当需要获取用户响应时,应改为向视口中传送一个样式化为消息提示的“AlertDialog”

<!- @include: @/meta/ToastAction.md ->

Close

允许用户在消息提示持续时间内提前将其关闭的按钮。

<!- @include: @/meta/ToastClose.md ->

示例

自定义快捷键

使用 keycode.info 中每个键的 event.code 值覆盖默认快捷键。

vue
<template>
  <ToastProvider>
    ...
    <ToastViewport :hotkey="['altKey', 'KeyT']" />
  </ToastProvider>
</template>

自定义持续时间

自定义消息提示的持续时间以覆盖提供者值。

vue
<template>
  <ToastRoot :duration="3000">
    <ToastDescription>已保存!</ToastDescription>
  </ToastRoot>
</template>

重复消息提示

当用户每次点击按钮都必须显示消息提示时,使用状态渲染同一消息提示的多个实例(见下文)。或者,你可以抽象各个部分以创建自己的命令式 API

vue
<template>
  <div>
    <form @submit="count++">
      ...
      <button>保存</button>
    </form>

    <ToastRoot v-for="(_, index) in count" :key="index">
      <ToastDescription>已保存!</ToastDescription>
    </ToastRoot>
  </div>
</template>

动画滑动手势

--reka-toast-swipe-move-[x|y]--reka-toast-swipe-end-[x|y] CSS 变量与 data-swipe="[start|move|cancel|end]" 属性结合使用,以动画方式实现滑动关闭手势。以下是一个示例:

vue
<template>
  <ToastProvider swipe-direction="right">
    <ToastRoot class="ToastRoot">
      ...
    </ToastRoot>
    <ToastViewport />
  </ToastProvider>
</template>
css
/* styles.css */
.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 属性控制屏幕阅读器对消息提示的敏感度。

对于用户操作产生的消息提示,选择 foreground。由后台任务生成的消息提示应使用 background

前台

前台消息提示会立即播报。当前台消息提示出现时,辅助技术可能会选择清除先前排队的消息。尽量避免同时堆叠不同的前台消息提示。

后台

后台消息提示会在下一个合适的时机播报,例如,当屏幕阅读器读完当前句子时。它们不会清除排队的消息,因此如果过度使用,对于屏幕阅读器用户来说,在响应用户交互时可能会感觉到用户体验迟缓。

vue
<template>
  <ToastRoot type="foreground">
    <ToastDescription>文件移除成功。</ToastDescription>
    <ToastClose>关闭</ToastClose>
  </ToastRoot>

  <ToastRoot type="background">
    <ToastDescription>我们刚刚发布了 Reka UI 2.0。</ToastDescription>
    <ToastClose>关闭</ToastClose>
  </ToastRoot>
</template>

替代操作

Action 上使用 altText 属性,以指导屏幕阅读器用户以替代方式操作消息提示。

你可以将用户引导至应用中可永久操作的位置,或者实现自己的自定义快捷键逻辑。如果实现后者,请使用 foreground 类型立即播报,并增加持续时间以给用户充足的时间。

vue
<template>
  <ToastRoot type="background">
    <ToastTitle>有可用更新!</ToastTitle>
    <ToastDescription>我们刚刚发布了 Reka UI 2.0。</ToastDescription>
    <ToastAction alt-text="前往账户设置进行升级">
      升级
    </ToastAction>
    <ToastClose>关闭</ToastClose>
  </ToastRoot>

  <ToastRoot type="foreground" :duration="10000">
    <ToastDescription>文件移除成功。</ToastDescription>
    <ToastAction alt-text="撤销 (Alt+U)">
      撤销 <kbd>Alt</kbd>+<kbd>U</kbd>
    </ToastAction>
    <ToastClose>关闭</ToastClose>
  </ToastRoot>
</template>

关闭图标按钮

当提供图标(或字体图标)时,请记得为屏幕阅读器用户正确标注。

vue
<template>
  <ToastRoot type="foreground">
    <ToastDescription>已保存!</ToastDescription>
    <ToastClose aria-label="关闭">
      <span aria-hidden="true">×</span>
    </ToastClose>
  </ToastRoot>
</template>

键盘交互

KeyDescription
F8
聚焦到消息提示视口。
Tab
将焦点移动到下一个可聚焦元素。
Shift + Tab
将焦点移动到上一个可聚焦元素。
Space
当焦点位于 ToastActionToastClose 上时,关闭消息提示
Enter
当焦点位于 ToastActionToastClose 上时,关闭消息提示
Esc
当焦点位于 Toast 上时,关闭消息提示

自定义 API

抽象部分

通过将基本部分抽象到你自己的组件中来创建你自己的 API。

用法

vue
<script setup lang="ts">
import Toast from './your-toast.vue'
</script>

<template>
  <Toast
    title="有可用更新"
    content="我们刚刚发布了 Radix 3.0!"
  >
    <button @click="handleUpgrade">
      升级
    </button>
  </Toast>
</template>

实现

vue
// 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="关闭">
      <span aria-hidden="true">×</span>
    </ToastClose>
  </ToastRoot>
</template>

命令式 API

创建你自己的命令式 API,以便在需要时允许消息提示重复

用法

vue
<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">
      保存成功!
    </Toast>
  </div>
</template>

实现

vue
// your-toast.vue
<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>关闭</ToastClose>
  </ToastRoot>
</template>