Toast 轻提示
非阻塞反馈——3 秒后自动消失,居中或底部出现。
代码演示
下方在 Web 平台解析到 ToastHost.web.tsx:使用纯 React state、CSS transition 与 timer 驱动 fade + slide;native 才解析到 ToastHost.tsx 并使用 Reanimated 4。两端共享同一个 Toast Store 与 delivery identity 契约。每个 Button 都带真实的 onPress,点击即可触发对应提示。
视觉规范
来源:src/components/ui/Toast/styles.ts、ToastHost.tsx、ToastHost.web.tsx、toast.ts。
| 元素 | 规则 |
|---|---|
| 形态 | 圆角 8(radius.md)c.inverseSurface 底(亮色 #1C1C1E),c.inverseOnSurface 白字 |
| 内边距 | 横向 14(space[6])/ 纵向 10(space[4]) |
| 宽度 | max-width 85%,水平居中;距底部 32px(space[10]) |
| 字号 | 14px(type.sm)/ 500 |
| 出现 | 200ms(motion.base)fade + 8px slide-up |
| 停留 | 默认 3000ms(duration 可覆盖) |
| 退出 | 200ms fade + 下滑 8px |
| 层级 | host position: absolute + zIndex: 200 + pointerEvents: none |
状态变体
kind 仅决定文本左侧 6×6 圆点的颜色(来源:dotColorFor):
✓ success · c.success 绿点(亮色 #52C41A)
✕ error · c.error 红点(亮色 #F4511E)
ℹ info · 无圆点(默认)
用法
import { toast, ToastHost } from '@unif/react-native-design';
// 在应用根放一次 ToastHost
<ToastHost />;
// 任意位置触发
toast('已保存');
toast.success('订单提交成功');
toast.error('网络异常,请重试');
toast.info('已切换到日报模式');
// 自定义时长
toast({ message: '正在同步…', duration: 5000 });
API
来源:src/components/ui/Toast/toast.ts、types.ts、ToastHost.tsx。
命令式函数 toast(不是组件):
| 调用 | 签名 | 说明 |
|---|---|---|
toast(input) | (input: ToastInput) => void | 默认 kind: 'info' |
toast.info(input) | (input: ToastInput) => void | info 提示 |
toast.success(input) | (input: ToastInput) => void | success 提示(绿点) |
toast.error(input) | (input: ToastInput) => void | error 提示(红点) |
ToastInput = string(简写,走默认 kind + 3000ms)| 对象:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
message | string | — | 消息文本(必填) |
kind | 'info' | 'success' | 'error' | 'info' | 类型(决定圆点颜色) |
duration | number? | 3000 | 自动消失毫秒数 |
position | 'top' | 'bottom' | 'center' | 'bottom' | 显示位置(top/center 自动避让 safe-area) |
<ToastHost /> 组件 props(ToastHostProps,在 app 根附近挂一个):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
testID | string? | — | 容器 testID;文本节点派生 ${testID}-text |
全局挂一个
<ToastHost />即可;同一时间只显示一条,新调用替换旧的。要在Modal里显示 toast 时,可在 Modal 内再挂一个——它会接管,关闭后自动归还(见下方投递语义)。
投递语义
toast() 由一个纯状态机驱动(src/components/ui/Toast/store.ts),模型是 pending / delivery 双态:
| 场景 | 行为 |
|---|---|
未挂 <ToastHost /> 时调用 | 消息保留为 pending(不是丢弃、不告警);Host 挂上后立即补投 |
| 未挂 Host 时连续调用多次 | latest-wins —— 只保留最新一条,不排队补投历史消息 |
| 已挂 Host 时连续调用 | 立即投递,后者替换前者;旧的那条的定时器 / 动画不再影响 UI |
挂了多个 <ToastHost /> | 栈式接管:最后挂载的收投递,前任入栈挂起并立刻收起自己那条 toast;接管者卸载后自动归还(乱序卸载也安全) |
| 接管瞬间的在途 toast(有人挂到我上面) | 丢弃。它属于「已经被盖住的过去」,搬到接管者身上重放没有意义 |
| 归还瞬间的在途 toast(接管者卸载、我恢复) | 整条交回并立即重投。「Modal 内操作成功 → toast → 关窗」是最常见的路径,那条 toast 刚发出、用户一眼都还没看到 |
| Host 卸载时消息还没显示完(栈里没有前任) | 未完成的投递退回 pending,下一个 Host 会重新投递;若期间已有更新的消息,更新的优先 |
| Host 渲染 / 订阅回调抛错 | 作废该 Host,消息退回 pending 等待新 Host |
这是与旧版本的行为变更:此前未挂 Host 时 toast() 会告警并丢弃消息。现在消息会保留并在 Host 挂载后补投 —— 如果你依赖「没有 Host 就静默丢弃」,需要改为条件调用。
Modal 里挂第二个 —— 这是受支持的用法RN Modal 是独立的 native window,根 Host 渲染的 toast 会被它整块盖住(双端都看不见)。所以在 Modal 的内容树里再挂一个 <ToastHost /> 是正解:它接管 owner,toast 渲染进 Modal 自己的 window;Modal 关闭、内层 Host 卸载后 owner 自动归还给根 Host,此时还没播完的那条会整条交回根 Host 重新播一遍(不追剩余时长)——所以「Modal 里点确定 → toast → 关窗」看到的是完整的 3 秒,不是被截断的尾巴。
接管以挂载顺序为准,不看层级。 谁最后挂载谁就是 owner —— 如果根子树被 re-key(切主题 / 切语言 / ErrorBoundary 重置)导致根 <ToastHost /> 重新挂载,它会从 Modal 内那个手里夺回 owner,直到 Modal 关闭为止 toast 又看不见了。别在 Modal 开着的时候 re-key 根。
这是与 0.24.x 及更早版本的行为变更:此前重复挂载的 <ToastHost /> 永久惰性(还会 dev warn),Modal 内自挂是死码。现在多 Host 是合法用法,告警已删除。
竞态守卫
每次投递带 owner token + leaseId + entry id 三重身份。Host 的每个定时器、RAF 和动画完成回调在改 UI 或上报完成前都要通过这三项 CAS —— 否则经典竞态会发生:A 的 3 秒定时器在 B 已经显示之后才触发,把 B 提前抹掉。
同一条消息被重新投递(Host 重挂)会拿到新的 leaseId,所以仅比较 entry.id 不足以区分两次投递。
ToastDelivery / lease / subscriber 等标识类型是 Host 与 Store 之间的内部协议,不从包根导出。ToastEntry.id 同样是内部身份,不保证跨版本稳定 —— 业务不要依赖它的具体取值。
无障碍(a11y)
来源:src/components/ui/Toast/ToastHost.tsx、ToastHost.web.tsx、toast.ts、types.ts。
- 默认
accessibilityRole:无。Toast 由<ToastHost>渲染为<View>+<Text>(native 与 web 两份实现一致),源码未设置accessibilityRole。 - 朗读 / live region:
<ToastHost>在每条 toast 出现时调用AccessibilityInfo.announceForAccessibility(message)(native 与 web 两端一致,web 经 RN-Web 注入 aria-live region),screen reader 会主动播报 message 文本。host 容器仍pointerEvents="none"(不抢焦点)。需要用户停留确认的关键操作(而非一次性通知)仍应改用会获得焦点的模态对话框。 - a11y props:命令式
toast(...)入参(ToastInput:message/kind/duration)与ToastHostProps(仅testID)均不含 a11y 字段;kind(success/error/info)只决定圆点颜色,不带语义角色。 - 无受控状态(
checked/selected/disabled均不适用)。
现状如实记录:Toast 出现时主动播报 message(SR 可感知),但仍是非阻塞提示(不抢焦点、自动消失);需要用户停留确认的关键操作请用模态对话框。
- ❌ 不要在 Toast 里放按钮——需要交互的反馈用内联 Confirmation 或模态对话框
- ❌ 不要堆叠多个 Toast——同一时间最多 1 条,新的把旧的替换
- ❌ 不要超过 50 字——超过就用模态对话框承载
- ❌ 不要为「提高可用性」乱挂多个
<ToastHost />—— 只有最后挂载的那个在收投递;多挂只在Modal内有意义(见上方 tip) - ❌ 不要依赖
ToastEntry.id的具体取值 —— 它是内部竞态守卫,不是稳定公共契约