跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件工具提示

工具提示

当用户用指针指向某个元素、用键盘聚焦它或长按它时,用几个词描述这个元素;它不取得焦点,也不阻挡任何东西。

content_copy以 Markdown 格式查看
类别
信息传达
令牌
comp.tooltip
层级
基础

概览

Web
tanrei-tooltip
schedule
SwiftUI
tanreiTooltip
schedule
Jetpack Compose
TanreiTooltip
schedule
Flutter
TanreiTooltip
schedule
React Native
Tooltip
schedule

生成自 docs/status/status.json, specs/tooltip.spec.json

用指针指向按钮,或聚焦按钮:plain 和 rich 工具提示

deleteinfo

用法

锚点是你自己的元素:图标按钮、被截断的标签、图表中的标记。工具提示给它加上一段描述,除此之外不改变它的任何方面。在 Web、Compose、Flutter 和 React Native 上,工具提示包裹锚点;在 SwiftUI 上,它是锚点上的一个修饰符。

工具提示会在悬停、键盘聚焦和长按时自行打开和关闭,并通过 toggle 报告。只有在你要自己显示它时才设置 open,例如首次使用时的提示。设置它不会触发任何事件,之后工具提示仍会继续自行打开和关闭。

describes(默认为 true)让这段文本成为锚点的无障碍描述。如果文本重复了锚点自己的名称,就把它设为 false,这样屏幕阅读器不会把同样的词读两遍;工具提示显示的就是图标按钮的标签时,正是这种情况。

Web 和 React 的示例展示的是当前的规格。SwiftUI、Compose、Flutter 和 React Native 待跟进:它们提供的仍是工具提示的上一版,只有四个居中的位置,不能为单个工具提示设置延迟、偏移、箭头、第二种外观和快捷键,并且只淡入淡出,不会变大。在跟上之前,这些平台的示例展示的是那一版。

<!-- 工具提示包裹它的锚点。no-describe 相当于把 describes 设为 false:文本与标签重复。 -->
<tanrei-tooltip text="移到回收站" no-describe>
  <tanrei-button variant="text" label="移到回收站"><span slot="icon">delete</span></tanrei-button>
</tanrei-tooltip>

<!-- 紧凑的工具栏希望名称更快出现。延迟以毫秒为单位。 -->
<tanrei-tooltip text="粗体" show-delay="150" no-describe>
  <tanrei-button variant="text" label="粗体"><span slot="icon">format_bold</span></tanrei-button>
</tanrei-tooltip>

<!-- 命令的按键显示在名称之后。锚点负责把它们提供给辅助技术。 -->
<tanrei-tooltip text="粗体" shortcut="mod+b" no-describe>
  <tanrei-button variant="text" label="粗体"><span slot="icon">format_bold</span></tanrei-button>
</tanrei-tooltip>

<!-- 在控件下方,与它的起始边对齐,带箭头,画在表面上而不是反色。 -->
<tanrei-tooltip text="按日期或所有者筛选" placement="bottom-start" arrow inverse="false">
  <tanrei-button variant="text" label="筛选"><span slot="icon">filter_list</span></tanrei-button>
</tanrei-tooltip>

<tanrei-tooltip id="storage" variant="rich" headline="存储空间" placement="bottom"
                text="回收站中的文件在删除前仍会占用你的存储空间。">
  <tanrei-button variant="text" label="关于存储空间"><span slot="icon">info</span></tanrei-button>
</tanrei-tooltip>

<script type="module">
  import '@tanrei/web/components/tooltip';
  const storage = document.querySelector('#storage');
  // 首次使用时的提示。设置 open 不会触发任何事件。
  storage.open = true;
  // toggle 报告工具提示自行做了什么:悬停、聚焦、长按、Escape、超时、另一个工具提示。
  storage.addEventListener('toggle', (e) => console.log(e.detail.open));
</script>
import { Button, Tooltip } from '@tanrei/react';

<Tooltip text="移到回收站" describes={false}>
  <Button variant="text" label="移到回收站" onClick={trash}><span slot="icon">delete</span></Button>
</Tooltip>
{/* 命令的按键显示在名称之后:每个字符串是一个组合键。 */}
<Tooltip text="粗体" shortcut={['mod+b']} describes={false}>
  <Button variant="text" label="粗体" onClick={bold}><span slot="icon">format_bold</span></Button>
</Tooltip>
{/* 在控件下方,与它的起始边对齐,带箭头,画在表面上而不是反色。 */}
<Tooltip text="按日期或所有者筛选" placement="bottom-start" arrow inverse={false}>
  <Button variant="text" label="筛选" onClick={filter}><span slot="icon">filter_list</span></Button>
</Tooltip>
<Tooltip
  variant="rich"
  headline="存储空间"
  text="回收站中的文件在删除前仍会占用你的存储空间。"
  placement="bottom"
  touchTimeout={4000} // 长按之后,几句话需要更长的时间来阅读
  onToggle={(e) => log(e.detail.open)}
>
  <Button variant="text" label="关于存储空间"><span slot="icon">info</span></Button>
</Tooltip>
import Tanrei

// 锚点上的一个修饰符。它上层的 .tanreiTheme(_:) 提供绘制工具提示的那一层。
// 描述会成为锚点的 accessibilityHint。
TanreiIconButton("移到回收站", action: trash) { Image(systemName: "trash") }
    .tanreiTooltip("移到回收站", describes: false)

// isPresented 就是 open;工具提示自行打开或关闭时会写入它。
TanreiIconButton("关于存储空间", action: {}) { Image(systemName: "info.circle") }
    .tanreiTooltip("回收站中的文件在删除前仍会占用你的存储空间。",
                   title: "存储空间", variant: .rich, placement: .bottom, isPresented: $showHint)
import io.github.chnotchy.tanrei.components.TanreiIconButton
import io.github.chnotchy.tanrei.components.TanreiTooltip
import io.github.chnotchy.tanrei.components.TanreiTooltipPlacement
import io.github.chnotchy.tanrei.components.TanreiTooltipVariant
import io.github.chnotchy.tanrei.components.rememberTanreiTooltipState
import io.github.chnotchy.tanrei.primitives.TanreiText

// 与 Material 的 TooltipBox 一样:工具提示包裹它的锚点。
TanreiTooltip(text = "移到回收站", describes = false) {
    TanreiIconButton(onClick = ::trash, contentDescription = "移到回收站") { TanreiText("delete") }
}

// state 就是 open。onToggle 得知工具提示自行做了什么;show() 和 dismiss() 不触发任何事件。
val hint = rememberTanreiTooltipState(onToggle = { open -> log(open) })
TanreiTooltip(
    text = "回收站中的文件在删除前仍会占用你的存储空间。",
    variant = TanreiTooltipVariant.Rich,
    title = "存储空间",
    placement = TanreiTooltipPlacement.Bottom,
    state = hint,
) {
    TanreiIconButton(onClick = {}, contentDescription = "关于存储空间") { TanreiText("info") }
}
LaunchedEffect(Unit) { hint.show() } // 首次使用时的提示
import 'package:tanrei/tanrei.dart';

// show() 和 hide() 设置 open,不通知任何人。监听器得知的是工具提示自行做了什么。
final TanreiTooltipController hint = TanreiTooltipController();
hint.addListener(() => log(hint.isOpen));

// 与 Flutter 的 Tooltip 一样:message 是文本,excludeFromSemantics 是取反的 describes。
TanreiTooltip(
  message: '移到回收站',
  excludeFromSemantics: true, // 文本与按钮自己的名称重复
  child: TanreiIconButton(onPressed: trash, icon: const Icon(AppSymbols.delete), semanticLabel: '移到回收站'),
);
TanreiTooltip(
  variant: TanreiTooltipVariant.rich,
  title: '存储空间',
  message: '回收站中的文件在删除前仍会占用你的存储空间。',
  placement: TanreiTooltipPlacement.bottom,
  controller: hint, // 用 hint.show() 显示首次使用时的提示
  child: TanreiIconButton(onPressed: info, icon: const Icon(AppSymbols.info), semanticLabel: '关于存储空间'),
);
import { IconButton, Tooltip } from '@tanrei/react-native';

// 锚点是接受 Pressable 属性的“一个”子元素。工具提示会加上自己的处理函数,并先调用锚点自己的处理函数。
<Tooltip text="移到回收站" describes={false}>
  <IconButton accessibilityLabel="移到回收站" onPress={trash}>delete</IconButton>
</Tooltip>

// open 从你的代码里显示它,不触发任何事件;onOpenChange 得知它自行做了什么。
<Tooltip
  variant="rich"
  headline="存储空间"
  text="回收站中的文件在删除前仍会占用你的存储空间。"
  placement="bottom"
  open={showHint}
  onOpenChange={setShowHint}
>
  <IconButton accessibilityLabel="关于存储空间">info</IconButton>
</Tooltip>

规格

位于锚点下方的 工具提示,以及没有标题的 rich 工具提示

deleteinfo

构成

部件 说明
1 anchor The element the tooltip describes: the product's own (an icon-only Button, a truncated label, a chart mark). The tooltip adds a description to it and changes nothing else about it.
2 container The surface that holds the text, placed on one side of the anchor and aligned along it.
3 arrow · 可选 A triangle on the container's edge that faces the anchor, pointing at it; one shape with the container.
4 title · 可选 A short heading above the supporting text. Rich tooltips only.
5 supporting-text The description. A plain tooltip holds nothing else.
6 shortcut · 可选 The keys that perform the anchor's command, after the text. Plain tooltips only.

属性

属性 类型 默认值 说明
variant plain | rich "plain" Plain is one short phrase, by default on the inverse surface, for naming what an icon does. Rich is an optional title and a few sentences, by default on a raised surface, for explaining a term or a setting. A structural choice drawn by code, not a token axis: a design system cannot add a form by values.
text string "" The description. Required.
title string "" A heading for a rich tooltip. Ignored by a plain one.
shortcut strings The keyboard shortcut of the anchor's command, in the keyboard shortcut component's key names: one chord per string, pressed in order (["mod+b"]). A plain tooltip draws it after the text for the keyboard the person uses and exposes it as the anchor's keyboard shortcut (TIP-27); a rich one ignores it. It does not make the keys work; the product binds them.
placement top | top-start | top-end | bottom | bottom-start | bottom-end | start | start-top | start-bottom | end | end-top | end-bottom | auto "top" Where the tooltip prefers to stand: a side of the anchor, and how it lines up along that side (TIP-22). top and bottom centre it above or below the anchor; with -start or -end its start or end edge meets the anchor's. start and end centre it beside the anchor; with -top or -bottom its top or bottom edge meets the anchor's. auto takes the first side with room (TIP-23). Start and end follow the layout direction. The tooltip flips to the opposite side when the preferred one has no room, keeping the alignment (TIP-9).
offset number null The distance between the anchor's edge and the tooltip, in the platform's layout units; 0 lets them touch. Null takes comp.tooltip.offset, the system's answer for every tooltip. Set it only where one anchor's box is not what the eye takes for its edge (a control with a large invisible hit area, a mark of a chart).
crossOffset number 0 How far the tooltip moves along the side it stands on, in the platform's layout units, from where placement puts it: positive towards the end for a tooltip above or below, towards the bottom for one beside. For lining an aligned tooltip up with what is drawn inside the anchor rather than with the anchor's box. It never moves the tooltip off the anchor's side (TIP-24).
arrow boolean null Whether the tooltip draws an arrow that points at the anchor. Null, the default, takes comp.tooltip.arrow.visible, the system's choice, which is off in the default system (TIP-25).
inverse boolean null Whether this tooltip is drawn inverse: in the opposite scheme's colors, so that it stands apart from whatever it lands on. False draws it on a surface of the scheme. Null, the default, takes its form's choice: comp.tooltip.plain.container.inverse (on in the default system) or comp.tooltip.rich.container.inverse (off). It changes colors and elevation only (TIP-26).
describes boolean true Whether assistive technology reads the text as the anchor's description. Set it to false when the text repeats the anchor's own accessible name (an icon-only Button whose tooltip shows its label), so that it is not read twice.
open boolean false Whether the tooltip is showing. The tooltip opens and closes itself on hover, focus and long-press and reports it (toggle). A product sets it to show a tooltip on its own (a first-run hint); a snapshot harness sets it to draw a fixture.
showDelay number null How long, in milliseconds, a fine pointer rests on the anchor before this tooltip opens; 0 opens it at once. Null takes comp.tooltip.motion.show-delay, the system's answer for every tooltip. Set it only where one tooltip needs another pace than the rest (a dense toolbar that wants its names sooner, a chart whose marks the pointer sweeps across and should not flash). It applies to hover alone: keyboard focus still opens at once.
hideDelay number null How long, in milliseconds, this tooltip waits after the pointer has left the anchor, the gap and the tooltip before it closes; 0 closes it at once. Null takes comp.tooltip.motion.hide-delay. The tooltip stays hoverable at any value, 0 included, because the gap between the anchor and the tooltip counts as hover (TIP-1).
touchTimeout number null How long, in milliseconds, a tooltip opened by a long-press stays after the finger lifts; 0 closes it as the finger lifts, so it shows for as long as the press lasts. Null takes comp.tooltip.timeout.touch. Set it only where one tooltip needs more or less reading time on touch than the rest (a rich tooltip of a few sentences, a one-word name).

插槽

插槽 说明
default The anchor.

事件

事件 说明
toggle The tooltip opened or closed by itself (hover, focus, long-press, Escape, the touch timeout, another tooltip opening); carries the new open value.

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
TIP-1 WHEN a fine pointer enters the anchor and stays on it for the show delay (TIP-17) THE tooltip SHALL open; WHEN the pointer leaves the anchor THE tooltip SHALL stay open while the pointer crosses the gap to the tooltip or moves onto the tooltip itself, and SHALL close once the pointer has been off the anchor, the gap and the tooltip for the hide delay (TIP-17). The gap is the strip between the anchor's edge and the container — the offset deep (TIP-24), and comp.tooltip.arrow.height deeper while an arrow is drawn (TIP-25) — as wide as the container along that side; it counts for hover alone, is not drawn and takes no press.
TIP-2 WHEN the anchor receives keyboard focus THE tooltip SHALL open without delay, and WHEN the anchor loses focus THE tooltip SHALL close; focus gained by pointer or touch SHALL NOT open it.
TIP-3 WHEN the user long-presses the anchor by touch THE tooltip SHALL open without activating the anchor, and WHEN the finger lifts SHALL stay open for the touch timeout (TIP-17) and then close; a touch anywhere else SHALL close it sooner.
TIP-4 WHILE the tooltip is open, WHEN the user presses Escape THE tooltip SHALL close without moving focus and SHALL consume the key, so that it does not also close an enclosing dialog; it SHALL stay closed until the anchor is entered by the pointer or focused again.
TIP-5 WHILE the pointer is over the anchor or the tooltip, or the anchor has keyboard focus, THE tooltip SHALL stay open with no time limit, until Escape (TIP-4), an activation of the anchor (TIP-6) or another tooltip opening (TIP-8) closes it.
TIP-6 WHEN the anchor is activated (a click, a tap, Enter or Space) THE tooltip SHALL close, and SHALL NOT open again from the same hover until the pointer has left the anchor and entered it again.
TIP-7 WHEN the tooltip opens or closes by itself (TIP-1 to TIP-6, TIP-8) THE tooltip SHALL emit toggle with the new value; WHEN the product sets open THE tooltip SHALL show or hide accordingly without emitting toggle, and SHALL then keep following TIP-1 to TIP-6.
TIP-8 WHEN a tooltip opens while another tooltip is open THE other tooltip SHALL close at once, and the new one SHALL open without waiting for its show delay (TIP-17), because the person is already reading tooltips; at most one tooltip SHALL be open in a window.
TIP-9 THE tooltip SHALL place its container on the side of the anchor and with the alignment that placement names (TIP-22, TIP-23), the offset away from the anchor's edge and moved along the side by crossOffset (TIP-24), and SHALL keep it at least comp.tooltip.window-margin inside the window's safe area by these steps, in this order: WHEN the preferred side has no room for the container and the opposite side has, it SHALL flip to the opposite side, keeping the alignment; WHEN neither has room it SHALL use the one of the two with more room; it SHALL then shift along the side as far as the margin asks and no farther; and WHERE the container is still larger than the room on its side (a window smaller than the tooltip) it SHALL move the container back into the margin over the anchor, its top and start edges first, rather than leave the window. It SHALL follow the anchor when the anchor moves (scrolling, resizing, layout), taking these steps again from the side it is on, which it SHALL keep for as long as that side has room, so that a page that scrolls does not throw the tooltip from side to side; and it SHALL close WHEN the anchor is removed, hidden, scrolled out of the window, or scrolled out of sight inside a scrolling ancestor so that no part of it can be seen.
TIP-10 WHILE variant is plain THE tooltip SHALL draw text in comp.tooltip.plain.supporting-text.typescale and comp.tooltip.plain.supporting-text.color on a container of comp.tooltip.plain.container.color with comp.tooltip.plain.container.shape corners and comp.tooltip.plain.container.elevation — or, WHILE it is drawn on a surface, in the colors and the elevation TIP-26 names in their place — padded by comp.tooltip.plain.container.padding-inline and comp.tooltip.plain.container.padding-block, at least comp.tooltip.plain.container.min-height tall and at most comp.tooltip.plain.container.max-width wide, wrapping longer text onto further lines; it SHALL draw its shortcut as TIP-27 says and SHALL ignore title.
TIP-11 WHILE variant is rich THE tooltip SHALL draw title in comp.tooltip.rich.title.typescale and comp.tooltip.rich.title.color above text in comp.tooltip.rich.supporting-text.typescale and comp.tooltip.rich.supporting-text.color, comp.tooltip.rich.container.gap apart, on a container of comp.tooltip.rich.container.color with comp.tooltip.rich.container.shape corners and comp.tooltip.rich.container.elevation — or, WHILE it is drawn inverse, in the colors and the elevation TIP-26 names in their place — padded by comp.tooltip.rich.container.padding-inline and comp.tooltip.rich.container.padding-block and at most comp.tooltip.rich.container.max-width wide; WHILE title is empty it SHALL draw the supporting text alone. THE tooltip SHALL set the title's letter-spacing by the script of its text, as every title is set (PRS-20): no negative letter-spacing WHERE its language is Chinese, Japanese or Korean, and none WHERE its script joins its letters or stacks marks on them.
TIP-12 THE tooltip SHALL ease its container corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0), SHALL grow with enlarged text without clipping it, and SHALL start-align its text by the layout direction.
TIP-13 THE tooltip SHALL NOT take focus, SHALL hold nothing focusable or pressable, SHALL NOT make anything inert or dimmed, and SHALL be drawn above every other layer of its window, including an open dialog and its scrim; a press on the tooltip SHALL close it and SHALL NOT reach what lies under it.
TIP-14 WHILE describes is true THE tooltip SHALL expose text — preceded by title for a rich tooltip — as the anchor's accessible description, whether or not the tooltip is open, and SHALL keep the drawn tooltip from being read a second time; WHILE describes is false it SHALL add nothing to the anchor's semantics. In both cases it SHALL NOT change the anchor's accessible name or role.
TIP-15 WHEN the tooltip opens THE tooltip SHALL appear as every surface appears: from sys.motion.appear.opacity, sys.motion.appear.scale, sys.motion.appear.distance toward its anchor and sys.motion.appear.blur to whole, grown from the middle of its edge that faces the anchor — from its own centre WHERE sys.motion.appear.origin is center, and WHILE it covers its anchor (TIP-9) — over comp.tooltip.motion.enter-duration with comp.tooltip.motion.enter-easing, so that it comes out of the anchor's side whichever side it is on; WHEN it closes THE tooltip SHALL take it back the same way to where it started, over comp.tooltip.motion.exit-duration with comp.tooltip.motion.exit-easing. WHERE sys.behavior.appearance is unfold the scale SHALL apply along the axis that leaves the anchor only. The motion SHALL be drawn only: the place TIP-9 gives, the gap of TIP-1 and the room the tooltip is measured for are those of the tooltip at rest. WHERE motion is reduced THE tooltip SHALL only fade in and out, over sys.motion.role.reduced, with no scale, distance or blur, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). WHEN it is mounted with open true THE tooltip SHALL appear without its motion while the document has not yet been first painted, and with it when it is mounted after that. A tooltip that takes over from another SHALL NOT move at all (TIP-28).
TIP-16 WHILE text is empty THE tooltip SHALL NOT open and SHALL add nothing to the anchor's semantics, and in debug builds SHALL report the empty text; WHILE variant is plain and title is set it SHALL report the ignored title in debug builds.
TIP-17 THE tooltip SHALL take its show delay from showDelay, its hide delay from hideDelay and its touch timeout from touchTimeout, each a number of milliseconds used as given (the motion dial does not scale it), so that 0 opens or closes at once: a show delay of 0 opens as the pointer enters, a hide delay of 0 closes as the pointer leaves the anchor, the gap and the tooltip, and a touch timeout of 0 closes as the finger lifts, after the tooltip has shown for the whole long-press. WHILE one of them is null THE tooltip SHALL use comp.tooltip.motion.show-delay, comp.tooltip.motion.hide-delay or comp.tooltip.timeout.touch in its place, and IF one is negative or not a finite number THE tooltip SHALL use that token likewise and in debug builds report the value it ignored.
TIP-18 THE tooltip SHALL treat its show delay, hide delay and touch timeout as waiting times, not motion: WHERE motion is reduced it SHALL keep them, on every platform, including one whose reduced-motion setting removes every animation. The show and hide delays SHALL govern hover alone (TIP-1), and the touch timeout the time after a long-press alone (TIP-3): keyboard focus (TIP-2), another tooltip being open (TIP-8) and the product setting open (TIP-7) SHALL NOT wait for any of them, and the long-press itself SHALL take the platform's own duration. WHEN showDelay, hideDelay or touchTimeout changes while that wait is running THE tooltip SHALL finish the running wait and use the new value from the next time that wait starts.
TIP-19 THE tooltip SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the tooltip is shown THE tooltip SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state, the container placed again by the new direction (TIP-20).
TIP-20 WHEN the layout direction is right-to-left THE tooltip SHALL take the side start as the anchor's right side and the side end as its left, SHALL take the alignments -start and -end of a tooltip above or below as the right and the left edges, and a positive crossOffset of such a tooltip as a move towards the left, flipping and shifting from there as TIP-9 says; it SHALL leave the sides top and bottom, the alignments -top and -bottom, a crossOffset beside the anchor and the centring on the anchor as they are, SHALL keep auto's order of sides by their names (TIP-23), SHALL start-align a rich tooltip's title and text at the right (TIP-12) and SHALL draw a plain tooltip's shortcut at the left of its text (TIP-27).
TIP-21 THE tooltip SHALL lay out its title and its text as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE tooltip SHALL still place each and align its lines by the layout direction, not by the text's own.
TIP-22 THE tooltip SHALL read placement as a side and an alignment along that side: top and bottom SHALL put the container above or below the anchor with its centre on the anchor's, top-start and bottom-start with its start edge on the anchor's start edge, and top-end and bottom-end with its end edge on the anchor's end edge; start and end SHALL put it beside the anchor, on the side the layout direction gives that name, with its centre on the anchor's, start-top and end-top with its top edge on the anchor's top edge, and start-bottom and end-bottom with its bottom edge on the anchor's bottom edge. WHEN placement names a value the tooltip does not define THE tooltip SHALL place itself as the default value says and in debug builds report the unknown value; WHEN placement changes while the tooltip is shown THE tooltip SHALL take its new place at once, without a motion.
TIP-23 WHILE placement is auto THE tooltip SHALL try the sides in the order top, bottom, end, start and SHALL take the first that has room for the container within comp.tooltip.window-margin, or the one with the most room WHEN none has, with its centre on the anchor's, and SHALL then shift, hold and follow as TIP-9 says, keeping the side it took for as long as that side has room.
TIP-24 THE tooltip SHALL take the distance between the anchor's edge and the nearest part of itself — the container's edge, or the arrow's tip while an arrow is drawn — from offset, a number of the platform's layout units used as given, so that 0 lets the two touch, and from comp.tooltip.offset WHILE offset is null; it SHALL then move the container along its side by crossOffset, before the shift of TIP-9, and by no more than leaves comp.tooltip.arrow.width of the container beside the anchor, so that the tooltip still stands on the anchor's side and an arrow can still reach the anchor. IF offset is negative or not a finite number THE tooltip SHALL use comp.tooltip.offset in its place, IF crossOffset is not a finite number THE tooltip SHALL use 0, and in debug builds it SHALL report the value it ignored.
TIP-25 THE tooltip SHALL draw an arrow WHILE arrow is true, and WHILE arrow is null and comp.tooltip.arrow.visible is on, and none otherwise. WHILE it draws one THE tooltip SHALL draw a triangle comp.tooltip.arrow.width wide at its base and comp.tooltip.arrow.height tall on the container's edge that faces the anchor, as one shape with the container — the container's color and elevation, with no seam and no shadow between the two — its tip pointing at the middle of the part of the anchor's edge that the container stands beside, and SHALL keep its base at least comp.tooltip.arrow.inset, and at least the corner's radius, from each end of that edge, standing at the edge's middle where the edge is too short for both; the container SHALL stand comp.tooltip.arrow.height farther from the anchor, so that the tip keeps the offset (TIP-24). WHEN the container flips, shifts or follows the anchor (TIP-9) the arrow SHALL move with it to the edge that now faces the anchor and point at the anchor again, and WHILE the container covers the anchor (TIP-9) THE tooltip SHALL draw no arrow.
TIP-26 THE tooltip SHALL draw itself inverse — in the opposite scheme's colors — WHILE inverse is true, on a surface of the scheme WHILE inverse is false, and WHILE inverse is null as its form's choice says: comp.tooltip.plain.container.inverse for a plain tooltip and comp.tooltip.rich.container.inverse for a rich one. The tokens TIP-10 and TIP-27 name are a plain tooltip's inverse look: WHILE drawn on a surface a plain tooltip SHALL take comp.tooltip.plain.surface.container.color, comp.tooltip.plain.surface.container.elevation, comp.tooltip.plain.surface.supporting-text.color and comp.tooltip.plain.surface.shortcut.color in their place. The tokens TIP-11 names are a rich tooltip's surface look: WHILE drawn inverse a rich one SHALL take comp.tooltip.rich.inverse.container.color, comp.tooltip.rich.inverse.container.elevation, comp.tooltip.rich.inverse.title.color and comp.tooltip.rich.inverse.supporting-text.color in place of those TIP-11 names; its shape, padding, sizes, typescales, arrow, placement, motion and timing SHALL be the same either way, and WHEN inverse changes while the tooltip is shown THE tooltip SHALL draw the other look at once, keeping its open state.
TIP-27 WHILE variant is plain and shortcut is not empty THE tooltip SHALL draw the shortcut after the text, comp.tooltip.plain.container.gap from it and on the text's first line, with the keyboard shortcut component hosted as KBS-13 says: in its plain form, at size small, with convention auto, its legends and separators in comp.tooltip.plain.supporting-text.typescale and comp.tooltip.plain.shortcut.color; the shortcut SHALL stay whole on one line while the text wraps beside it, and the two with the gap SHALL fit within comp.tooltip.plain.container.max-width. THE tooltip SHALL expose the shortcut as the anchor's keyboard shortcut in the form KBS-12 gives (aria-keyshortcuts on the web) where the platform has that property and the anchor does not state one itself, and otherwise, WHILE describes is true, SHALL add the text KBS-10 gives to the end of the anchor's description; it SHALL NOT make the keys work. WHILE variant is rich THE tooltip SHALL ignore shortcut and in debug builds report it.
TIP-28 WHEN a tooltip opens while another tooltip of its window is open or still fading out, or within comp.tooltip.motion.warm-duration after one closed because the pointer or the keyboard focus left its anchor THE tooltip SHALL appear whole in its place at once, without the motion of TIP-15, and the other SHALL disappear at once, without its own; WHEN it is a pointer that opens it within that time THE tooltip SHALL NOT wait for its show delay either, as TIP-8 says of a tooltip that opens while another is open. A tooltip closed by Escape (TIP-4), by an activation of its anchor (TIP-6) or by a press on it (TIP-13) SHALL start no such time, so that the next tooltip waits and moves as a first one does. The time is a waiting time: WHERE motion is reduced it SHALL still apply (TIP-18).
TIP-29 WHEN the tooltip is told to close while it is still entering, or to open while it is still leaving THE tooltip SHALL turn round from the opacity, the size and the position it has reached, without first jumping to the start or the end of either motion, and SHALL take no longer over the rest than the whole duration of the motion it turns to. WHILE the tooltip is closed or leaving THE tooltip SHALL take no pointer input — a press reaches what lies under it, and the pointer on it is not hover (TIP-1) — and WHILE it is closed it SHALL take no room in the layout and draw nothing, so that a tooltip nobody can see never stands in the way of what is under it.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-tooltip tanreiTooltip TanreiTooltip TanreiTooltip Tooltip
variant · 属性 variant variant variant variant variant
text · 属性 text text text message text
title · 属性 headline title title title title
shortcut · 属性 shortcut shortcut shortcut shortcut shortcut
placement · 属性 placement placement placement placement placement
offset · 属性 offset offset offset offset offset
crossOffset · 属性 crossOffset crossOffset crossOffset crossOffset crossOffset
arrow · 属性 arrow arrow arrow arrow arrow
inverse · 属性 inverse inverse inverse inverse inverse
describes · 属性 describes describes describes excludeFromSemantics describes
open · 属性 open isPresented state controller open
showDelay · 属性 showDelay showDelay showDelayMillis waitDuration showDelay
hideDelay · 属性 hideDelay hideDelay hideDelayMillis exitDuration hideDelay
touchTimeout · 属性 touchTimeout touchTimeout touchTimeoutMillis showDuration touchTimeout
toggle · 事件 toggle isPresented state controller onOpenChange
备注 Wraps its anchor (the default slot). showDelay, hideDelay and touchTimeout are number properties in milliseconds, null by default, set by the show-delay, hide-delay and touch-timeout attributes; an attribute that is empty or does not parse as a finite number is not a number (TIP-17), so show-delay="" takes the token and does not open at once. The gap of TIP-1 is measured, not drawn: the document's pointer-move listener counts a point inside it as over the tooltip, so nothing in the top layer covers what lies under the gap. The drawn tooltip is a popover="manual" element in the shadow root, so it is in the top layer (above an open modal <dialog>); it is placed with CSS anchor positioning and position-try flips where supported, and by measuring otherwise. describes is a boolean property that defaults to true; its attribute is the inverse no-describe, because an HTML boolean attribute cannot default to true. toggle is a CustomEvent whose detail.open is the new value. Snapshot harness: each fixture is a stage of 320 × 200 with a medium icon-only text tanrei-button labelled after the fixture's glyph at its centre, and the tooltip open beside it. placement takes the thirteen values as written (top-start, end-bottom, auto); offset and crossOffset are number properties in CSS pixels (attributes offset, cross-offset), parsed as the delays are, so an empty attribute is not a number. arrow and inverse are properties that are true, false or null: the attribute is absent for null, present for true and ="false" for false, as the snackbar's inverse is; null reads the choice from the cascade. shortcut is an array property; its attribute holds the chords separated by spaces, as the keyboard shortcut's keys does, and the shortcut reaches the anchor's focusable element as aria-keyshortcuts unless that element already has one. The container and the arrow are one painted shape, so the elevation's ring and shadows run round both. The enter and exit motion runs on a wrapper inside the popover whose transform origin is set from the placement that was taken; the popover element itself keeps its placed box, so the measured gap and the following do not see the motion. The time tooltips stay warm is kept per window, beside the one showing tooltip. Snapshot harness: the stage sets data-tanrei-keyboard-convention="standard", so plain-shortcut reads the same on every machine. Lagging: has no delay or timeout parameters and no hover gap; owes TIP-1 (the gap), TIP-3 and TIP-8's wording, TIP-17 and TIP-18. The target: a view modifier .tanreiTooltip(_ text:, title:, variant:, placement:, describes:, showDelay: Duration? = nil, hideDelay: Duration? = nil, touchTimeout: Duration? = nil, isPresented: Binding<Bool>?); a Duration carries its own unit (.milliseconds(300)), nil takes the token, and a negative one takes the token and is reported in debug builds. The description is the anchor's accessibilityHint. The tooltip is drawn in an overlay above the window's content (anchored with anchor preferences), not with .popover, which is modal at compact widths and moves VoiceOver focus: a non-modal overlay must do neither. Hover is onContinuousHover (iPadOS and macOS pointers), keyboard focus is focused with full keyboard access, long-press is a LongPressGesture that does not fire the anchor's action. Snapshot harness: renders the anchor and the open tooltip in a fixed 320 × 200 frame. Also owed: the thirteen placement values, offset, crossOffset, arrow, inverse and shortcut; TIP-9 and TIP-15 as they read now and TIP-22 to TIP-29. offset and crossOffset are CGFloat? and CGFloat in points; arrow and inverse are Bool?, nil taking the system's choice; shortcut is [String], exposed with the anchor's keyboardShortcut semantics where it has one and in the hint otherwise. The container and the arrow are one Shape. Snapshot harness: the stage sets the standard keyboard convention for plain-shortcut. Not yet: the title's letter-spacing by the script of its text (TIP-11, PRS-20). Lagging: has no delay or timeout parameters and no hover gap; owes TIP-1 (the gap), TIP-3 and TIP-8's wording, TIP-17 and TIP-18. The target: TanreiTooltip(text, modifier, variant, title, placement, describes, showDelayMillis: Long? = null, hideDelayMillis: Long? = null, touchTimeoutMillis: Long? = null, state = rememberTanreiTooltipState()) { anchor }, shaped like Material's TooltipBox, the times named in milliseconds as ViewConfiguration.longPressTimeoutMillis is; null takes the token, a negative value takes the token and is reported in debug builds. The waits are coroutine delays, not animations, so the animator duration scale of 0 (Android's reduced motion) does not remove them (TIP-18); state.isVisible is open, and state.show() / state.dismiss() set it. The tooltip is a Popup with PopupProperties(focusable = false). Compose has no separate description semantics: the text is appended to the anchor's merged semantics after its own name, as Android reads a View's tooltip text, and a test asserts the order. Snapshot harness: renders the anchor and the open tooltip in a fixed 320 × 200 frame. Also owed: the thirteen placement values, offset, crossOffset, arrow, inverse and shortcut; TIP-9 and TIP-15 as they read now and TIP-22 to TIP-29. offset and crossOffset are Dp? and Dp; arrow and inverse are Boolean?, null taking the system's choice; shortcut is List<String>. The container and the arrow are one Shape, so the shadow follows both. The warm time is a coroutine delay, kept at an animator duration scale of 0 (TIP-28). Snapshot harness: the stage sets the standard keyboard convention for plain-shortcut. Not yet: the title's letter-spacing by the script of its text (TIP-11, PRS-20). Lagging: has no delay or timeout parameters and no hover gap; owes TIP-1 (the gap), TIP-3 and TIP-8's wording, TIP-17 and TIP-18. Named as Flutter's Tooltip: message is the text, excludeFromSemantics is the inverse of describes, the delays are waitDuration and exitDuration, and the touch timeout is showDuration, which Flutter's Tooltip uses for the time after a long-press is released (each a Duration?: null takes the token, a negative one takes the token and is reported in debug builds). The description is the anchor's Semantics(tooltip:). The tooltip is drawn through an OverlayPortal (the overlay of the nearest Overlay, so above a dialog route opened earlier). controller is a TanreiTooltipController (a ChangeNotifier with show(), hide() and isOpen). Snapshot harness: a golden of the anchor and the open tooltip in a 320 × 200 surface. Also owed: the thirteen placement values, offset, crossOffset, arrow, inverse and shortcut; TIP-9 and TIP-15 as they read now and TIP-22 to TIP-29. offset and crossOffset are double? and double in logical pixels (Flutter's own Tooltip calls the first verticalOffset; this one is not vertical only); arrow and inverse are bool?, null taking the system's choice; shortcut is List<String>. The container and the arrow are one ShapeBorder. Snapshot harness: the stage sets the standard keyboard convention for plain-shortcut. Not yet: the title's letter-spacing by the script of its text (TIP-11, PRS-20). Lagging: has no delay or timeout props and no hover gap; owes TIP-1 (the gap), TIP-3 and TIP-8's wording, TIP-17 and TIP-18. The target: showDelay, hideDelay and touchTimeout are numbers of milliseconds, as Pressable's delayLongPress is; undefined or null takes the token, and a negative or non-finite number takes the token and is reported in development builds. Wraps its anchor (children). The description is the anchor's accessibilityHint. The tooltip is drawn in the overlay layer that TanreiProvider renders at the root of the app, not in a Modal: a Modal is modal (it takes every touch and moves accessibility focus), which a non-modal overlay must not be. Hover comes from onHoverIn/onHoverOut on platforms with a pointer. Snapshot harness: renders the anchor and the open tooltip in a fixed 320 × 200 view. Also owed: the thirteen placement values, offset, crossOffset, arrow, inverse and shortcut; TIP-9 and TIP-15 as they read now and TIP-22 to TIP-29. offset and crossOffset are numbers of density-independent pixels; arrow and inverse are booleans, undefined or null taking the system's choice; shortcut is a string array. The container and the arrow are one drawn shape. Snapshot harness: the stage sets the standard keyboard convention for plain-shortcut. Not yet: the title's letter-spacing by the script of its text (TIP-11, PRS-20).

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-tooltip-arrow-height sys.space.space75 6px
--tanrei-comp-tooltip-arrow-inset sys.space.space100 8px
--tanrei-comp-tooltip-arrow-visible — 0
--tanrei-comp-tooltip-arrow-width sys.space.space150 12px
--tanrei-comp-tooltip-motion-enter-duration sys.motion.role.appear.hint.enter 250ms
--tanrei-comp-tooltip-motion-enter-easing sys.motion.easing.enter cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-tooltip-motion-exit-duration sys.motion.role.appear.hint.exit 200ms
--tanrei-comp-tooltip-motion-exit-easing sys.motion.easing.exit cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-tooltip-motion-hide-delay sys.motion.duration.short2 100ms
--tanrei-comp-tooltip-motion-show-delay sys.motion.duration.long2 500ms
--tanrei-comp-tooltip-motion-warm-duration sys.motion.duration.medium2 300ms
--tanrei-comp-tooltip-offset sys.space.space50 4px
--tanrei-comp-tooltip-plain-container-color sys.color.inverse-surface #1f1f1f
--tanrei-comp-tooltip-plain-container-elevation sys.elevation.level0
--tanrei-comp-tooltip-plain-container-gap sys.space.space100 8px
--tanrei-comp-tooltip-plain-container-inverse — 1
--tanrei-comp-tooltip-plain-container-max-width — 200px
--tanrei-comp-tooltip-plain-container-min-height — 24px
--tanrei-comp-tooltip-plain-container-padding-block sys.space.space50 4px
--tanrei-comp-tooltip-plain-container-padding-inline sys.space.space100 8px
--tanrei-comp-tooltip-plain-container-shape sys.shape.corner.small 6px
--tanrei-comp-tooltip-plain-shortcut-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-tooltip-plain-supporting-text-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-tooltip-plain-surface-container-color sys.color.surface-bright #ffffff
--tanrei-comp-tooltip-plain-surface-container-elevation sys.elevation.level2 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04)
--tanrei-comp-tooltip-plain-surface-shortcut-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-tooltip-plain-surface-supporting-text-color sys.color.on-surface #1b1b1b
--tanrei-comp-tooltip-rich-container-color sys.color.surface-bright #ffffff
--tanrei-comp-tooltip-rich-container-elevation sys.elevation.level2 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04)
--tanrei-comp-tooltip-rich-container-gap sys.space.space50 4px
--tanrei-comp-tooltip-rich-container-inverse — 0
--tanrei-comp-tooltip-rich-container-max-width — 320px
--tanrei-comp-tooltip-rich-container-padding-block sys.space.space150 12px
--tanrei-comp-tooltip-rich-container-padding-inline sys.space.space200 16px
--tanrei-comp-tooltip-rich-container-shape sys.shape.corner.medium 8px
--tanrei-comp-tooltip-rich-inverse-container-color sys.color.inverse-surface #1f1f1f
--tanrei-comp-tooltip-rich-inverse-container-elevation sys.elevation.level0
--tanrei-comp-tooltip-rich-inverse-supporting-text-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-tooltip-rich-inverse-title-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-tooltip-rich-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-tooltip-rich-title-color sys.color.on-surface #1b1b1b
--tanrei-comp-tooltip-timeout-touch — 1500ms
--tanrei-comp-tooltip-window-margin sys.space.space100 8px
--tanrei-comp-tooltip-plain-supporting-text-typescale sys.typescale.body-small 400 13px/1.5385
--tanrei-comp-tooltip-rich-supporting-text-typescale sys.typescale.body 400 14px/1.5714
--tanrei-comp-tooltip-rich-title-typescale sys.typescale.heading-6 500 14px/1.4286

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/tooltip.spec.json, tokens/comp/tooltip.tokens.json, design/tokens/

指南

何时使用

用工具提示为认不出图标的用户说出图标操作的名称,或者就地解释一个术语、一项限制或一项设置。它是一段描述,供人顺带一读。

对触摸用户来说,工具提示在长按之前是看不见的;屏幕阅读器也只是把它当作一段描述来读。凡是用户完成任务所需要的内容,绝不要只放在工具提示里:把它作为文本显示在屏幕上。需要链接或按钮的说明不是工具提示。请使用对话框,或者把文字放在它所解释的对象旁边。

如何选择变体

变体 它是什么 用途
Plain 一个简短的短语,默认画在反色表面上。它忽略 headline,可以在文本之后显示键盘快捷键。 说出图标的作用:“移到回收站”“分享”。
Rich 几句话,上方可以有一个标题,默认画在抬升的表面上。它忽略 shortcut。 解释一个术语或一项设置:“存储空间”以及哪些内容计入其中。

变体是结构上的选择,而不是令牌的一个轴。设计系统可以改变两种形式的样式,但不能靠取值添加第三种。

位置

placement 指定工具提示优先出现在锚点的哪一侧,以及它沿这一侧如何对齐。默认是 top。

用指针指向按钮,或聚焦按钮:十三种位置

toptop-starttop-endbottombottom-startbottom-endstartstart-topstart-bottomendend-topend-bottomauto
所在的一侧 相对锚点居中 第一条边对齐 第二条边对齐
上方 top top-start top-end
下方 bottom bottom-start bottom-end
锚点之前(起始侧) start start-top start-bottom
锚点之后(末尾侧) end end-top end-bottom
  • 对齐指的是边贴着边。 top-start 把工具提示的起始边放在锚点的起始边上,end-bottom 把它的底边放在锚点的底边上。锚点很小、又靠近面板边缘,而工具提示较宽时,使用带对齐的位置;要让工具提示与一行文本对齐时也是如此。
  • 起始和末尾随布局方向而定。 在从右到左的布局中,start 是右侧,top-start 对齐的是右边。顶部和底部不镜像。
  • auto 依次尝试上方、下方、末尾侧和起始侧,取第一个有空间的一侧,并居中。锚点周围的环境无法预知时使用它,例如可能停靠在任意一边的面板里的控件。
  • 没有空间时。 工具提示翻转到相对的一侧并保持原来的对齐方式,然后沿这一侧平移,以留在窗口的安全区域之内。窗口比工具提示还小时,它留在窗口之内,必要时盖在锚点上。一旦翻转,只要那一侧还有空间,它就留在那一侧,所以页面滚动时它不会来回跳动。
  • offset 和 crossOffset。 offset 是到锚点边缘的距离,0 让两者相接;不设置时是系统规定的距离。crossOffset 让工具提示沿它所在的一侧移动:在锚点上方或下方时朝末尾侧移动,在锚点旁边时朝底部移动,但从不移出锚点的这一侧。两者都以平台的布局单位计。只有当锚点的盒子并不是眼睛所认定的边缘时才设置它们,例如带有很大的不可见点击区域的控件,或图表里的一个标记。
  • 显示期间的更改。 新的 placement 或偏移会让工具提示立即移动,不带动效。

箭头

用指针指向按钮,或聚焦按钮:带箭头

deletefilter_listinfo

arrow 在工具提示的边缘画一个指向锚点的小三角形。在默认系统中它是关闭的。不设置时,采用设计系统的选择;true 和 false 为单个工具提示作出决定。在 Web 上,这个属性写作 arrow 表示 true,写作 arrow="false" 表示 false。

箭头与工具提示是同一个形状,颜色相同。它指向的是锚点,而不是工具提示的中部,工具提示翻转或平移之后也是如此;它还会避开圆角。带箭头时,工具提示会多离开一个箭头的高度,所以你看到的间隙在有无箭头时都一样。

仅凭所在的一侧还说不清指的是哪个控件时,使用箭头:例如你自己在拥挤的工具栏上打开的一条提示。悬停时显示的名称很少需要箭头。

两种外观

用指针指向按钮,或聚焦按钮:两种形式各自的反色外观和表面外观

Plain,反色Plain,表面Rich,表面Rich,反色

两种形式都可以画成反色,即使用相反配色方案的颜色;也可以画在当前配色方案中抬升的表面上。

inverse Plain Rich
未设置(默认) 按设计系统为 plain 工具提示所作的选择:在默认系统中是反色。 按设计系统为 rich 工具提示所作的选择:在默认系统中是表面。
true 反色。 反色。
false 画在表面上。 画在表面上。

在 Web 上,这个属性写作 inverse 表示 true,写作 inverse="false" 表示 false,与消息条(Snackbar)的属性相同。

变化的只有颜色和层级。形状、尺寸、文本样式、箭头、位置和动效在两种外观下都相同,而且两种外观都不表示工具提示很重要。只在某个工具提示不这样设置就无法与背景区分开时才设置它:例如深色图片查看器或反色栏之上的深色标签,或者内容繁杂的凸起表面之上一段较长的说明。

键盘快捷键

用指针指向按钮,或聚焦按钮:命令的按键显示在名称之后

format_boldformat_italic

shortcut 在 plain 工具提示的文本之后显示锚点所对应命令的按键,并按用户所用的键盘书写。它使用键盘快捷键组件的按键名称,每个字符串是一个组合键;在 Web 上,属性中的组合键用空格分隔。

工具提示只显示按键。它不会让这些按键生效:按键由你的产品来绑定。较长的文本在按键旁边换行时,按键始终完整地保持在一行之内。rich 工具提示忽略 shortcut,并会在调试构建中报告。

打开、焦点和关闭

工具提示是供阅读的浮层。用户从中获取信息,从不在里面进行操作,所以它从不取得焦点。

输入 打开 关闭
指针 指针在锚点上停留满显示延迟之后。 指针离开锚点、间隙和工具提示满隐藏延迟之后。指针可以越过间隙移到工具提示上去阅读它。
键盘焦点 锚点获得键盘焦点时立即打开。来自指针或触摸的焦点不会打开它。 焦点离开锚点时。
触摸长按 立即打开,不激活锚点。 手指抬起后经过触摸超时;如果在别处触摸,则更早关闭。
  • 使用期间没有时间限制。 指针位于锚点、工具提示或两者之间的间隙之上时,或者锚点持有键盘焦点时,工具提示保持打开。间隙不会被绘制,也不响应按下:它只是算作悬停。
  • Escape。 Escape 关闭工具提示而不移动焦点,并且不再向外传递,所以包含它的对话框保持打开。在指针再次进入锚点或锚点再次获得焦点之前,工具提示保持关闭。
  • 激活。 激活锚点(点击、轻点、Enter 或 Space)会关闭工具提示,同一次悬停不会再次打开它。
  • 一次只有一个。 一个工具提示打开时,窗口里的其他工具提示都会关闭;新的工具提示打开时没有延迟,因为用户已经在阅读工具提示了。
  • 在工具提示之间移动。 如果一个工具提示接替了另一个,或者在另一个工具提示因指针或焦点离开其锚点而关闭之后不久打开,它会立即完整出现:没有延迟,也没有动效。所以指针沿工具栏扫过时,可以一个接一个地读到名称。因 Escape、激活锚点或在其上按下而关闭的工具提示,不会开始这段计时。
  • 不取得焦点,不阻挡任何东西。 它不包含任何可聚焦或可按下的内容,不让任何东西变为惰性(inert),也不绘制遮罩。在工具提示上按下会关闭它,并且这次按下不会到达它下面的内容。
  • 位于一切之上。 它画在所在窗口的所有其他层之上,包括打开的对话框及其遮罩,所以对话框里的工具提示是看得见的。
  • 位置。 它位于 placement 所指定的地方,见位置。锚点移动时它会跟随;锚点被移除、被隐藏、被滚出窗口,或者在滚动区域内被滚出视野时,它会关闭。
  • 动效。 它与设计系统中其他表面的出现方式相同,从靠近锚点的一侧出现:默认情况下,它从朝向锚点的那条边的中点略微变大并同时淡入;消失时按原路返回。被打断的动效会从当前位置掉头。关闭过程中,工具提示不接收指针,所以它从不挡住下面的内容。在减弱动态效果的设置下,它只淡入淡出。
  • 朗读。 工具提示出现时不朗读任何内容。describes 为 true 时,无论工具提示是否打开,它的文本(rich 工具提示则在标题之后)都是锚点的描述,并且画出来的工具提示不会被再读一遍。

单个工具提示的延迟

设计系统为所有工具提示统一设置显示延迟、隐藏延迟和触摸超时。单个工具提示可以用 showDelay、hideDelay 和 touchTimeout 设置自己的值,以毫秒为单位。不设置时,各自采用系统的值。

  • 何时设置。 紧凑的工具栏希望名称更快出现时,设置更短的显示延迟;图表上的标记会被指针扫过时,设置更长的显示延迟,这样就不会有工具提示一闪而过。有几句话的 rich 工具提示设置更长的触摸超时,只有一个词的名称设置更短的触摸超时。
  • 0 表示立即。 显示延迟为 0 时,指针一进入锚点,工具提示就打开;隐藏延迟为 0 时,指针一离开锚点、间隙和工具提示,它就关闭。即使这样,指针仍然可以悬停到工具提示上,因为间隙算作悬停。触摸超时为 0 时,工具提示在整个长按期间显示,手指一抬起就关闭。
  • 原样使用。 你设置的值不会被动效调节量缩放,减弱动态效果时也保持不变:延迟是等待,不是动画。负值或非数字的值会采用系统的值,并会在调试构建中报告。在 Web 上,空的属性同样不是数字。
  • 仅限悬停和触摸。 键盘焦点仍然立即打开工具提示;在另一个工具提示正显示时打开的工具提示仍然跳过显示延迟;设置 open 仍然立即显示或隐藏它。长按本身采用平台自己的时长。
  • 等待期间的更改。 已经开始的等待会按开始时的值走完。新的值从下一次起生效。

目前只有 Web 和 React 接受这些属性。其他平台跟上时,规格会按各平台的惯用方式为它们命名:Compose 上是 showDelayMillis、hideDelayMillis 和 touchTimeoutMillis,Flutter 上是 Tooltip 的 waitDuration、exitDuration 和 showDuration,SwiftUI 和 React Native 上是规格自己的名称。

每个平台都使用非模态的呈现方式:在 Web 上是顶层(top layer)中的 popover="manual" 元素;在 SwiftUI 上是窗口之上的浮层(不是 .popover,它在 compact 宽度下是模态的);在 Compose 上是不能取得焦点的 Popup;在 Flutter 上是 OverlayPortal;在 React Native 上是 TanreiProvider 的浮层(不是 Modal,它会接收所有触摸)。

推荐

  • plain 工具提示保持在几个词以内,rich 工具提示保持在几句话以内。
  • 在同一个屏幕上,给同一类锚点设置相同的位置,例如工具栏里每个图标的下方。
  • 对大多数工具提示,把 inverse 和 arrow 交给设计系统决定,只为确实需要的那一个设置它们。
  • 用 shortcut 显示命令的按键,而不要把它们写进文本里。
  • 给同一组锚点的工具提示设置相同的延迟,让它们保持同一个节奏。
  • 工具提示显示的是锚点自己的标签时,把 describes 设为 false。

避免

  • 不要把工具提示当作图标按钮的名称。按钮的 label 才是它的名称。
  • 不要在禁用的锚点上放工具提示来解释它为什么被禁用。禁用的控件无法获得焦点,也可能收不到指针事件。把原因写在可见的文本里。
  • 不要在工具提示里放链接、按钮或任何其他可交互的内容。
  • 不要在工具提示里重复锚点已经用可见文本说明的内容。
  • 不要用反色外观或箭头来让工具提示显得更重要。

无障碍

角色
tooltip
标注
The tooltip names nothing. The anchor keeps its own accessible name and role; while describes is true the text (after the title, for a rich tooltip) is the anchor's description, available whether or not the tooltip is showing. An icon-only Button's label is its name (BTN-16) and is never replaced by a tooltip: a tooltip that shows the same words sets describes to false. A shortcut is the anchor's keyboard shortcut where the platform has that property, and the end of its description otherwise (TIP-27); the drawn keys and the arrow are hidden.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab Moving focus to the anchor opens its tooltip at once; moving focus away closes it. The tooltip itself is never a tab stop.
Escape Closes an open tooltip without moving focus, and goes no further (an enclosing dialog stays open).

备注

  • The tooltip takes no input, so it has no target of its own: minimumTarget applies to the anchor, which keeps its own hit area.
  • WCAG 1.4.13 (content on hover or focus): the tooltip can be dismissed without moving the pointer or focus (Escape, TIP-4), can be hovered (TIP-1: the gap between the anchor and the tooltip counts as hover — at any offset, with or without an arrow — so this holds at any hideDelay, 0 included), and stays until hover or focus ends or it is dismissed (TIP-5). Placement, the arrow and the motion change none of the three: a tooltip that flips, shifts or follows its anchor is the same open tooltip, and the gap is measured to wherever it stands.
  • A tooltip is invisible to touch users until they long-press, and to screen-reader users it is only a description. Nothing a person needs to finish a task may live only in a tooltip. A note that needs a link or a button is not a tooltip; that form belongs to a separate non-modal popover component.
  • Do not put a tooltip on a disabled anchor to explain why it is disabled: a disabled control cannot be focused, and on the web it may receive no pointer events. Put the reason in visible text.
  • Web: the description reaches the anchor's focusable element as aria-description, because aria-describedby cannot point across shadow roots. The anchor is the first focusable element slotted into the tooltip; a Tanrei component forwards aria-description from its host to the native element in its shadow root. The drawn tooltip has role tooltip and aria-hidden while describes carries the text.
  • The motion is short, small and never the only sign of anything; where motion is reduced nothing grows or travels (TIP-15). A tooltip that is leaving is already closed for input (TIP-29), so nothing invisible is ever in the pointer's way.
  • The inverse look and the surface look are two looks of one thing, not two meanings: neither says that a tooltip matters more. Choose by what the tooltip lands on.
  • A shortcut in a tooltip is a reminder for people who see it. It must also work for those who do not: the product binds the keys, and the anchor carries the shortcut for assistive technology (TIP-27).

生成自 specs/tooltip.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • describes 为 true 时,文本(rich 工具提示则在标题之后)是锚点的无障碍描述,无论工具提示是否正在显示都可以读到。锚点保留自己的名称和角色。describes 为 false 时,工具提示不给锚点添加任何内容。
  • 画出来的工具提示不会被读两遍。它从不取得焦点,也不包含任何可聚焦的内容。
  • 锚点获得键盘焦点时,工具提示立即打开;焦点移走时,它关闭。工具提示本身从不是 Tab 停靠点。
  • Escape 关闭它而不移动焦点,并且不会同时关闭包含它的对话框。
  • 它满足 WCAG 1.4.13(悬停或聚焦时显示的内容):不移动指针或焦点就能关闭它;无论隐藏延迟是多少(包括 0),指针都能悬停到它上面,因为锚点与工具提示之间的间隙算作悬停;它会一直保留,直到悬停或聚焦结束,或者它被关闭。
  • 文本放大时它随之变大,不会裁掉文本;它的文本向布局方向的起始侧对齐。
  • 快捷键会作为锚点的键盘快捷键提供给辅助技术(在 Web 上是 aria-keyshortcuts,设置在取得焦点的元素上),画出来的按键对辅助技术隐藏。如果锚点已经声明了自己的快捷键,则在 describes 为 true 时,改为把工具提示的按键拼写出来,放在描述的末尾。
  • 箭头是装饰,对辅助技术隐藏。两种外观使用的都是成对的文本颜色和容器颜色,构建会在每种配色方案下检查它们的对比度。
  • 它的动效很小:默认只有淡入和略微变大。在减弱动态效果的设置下,它只淡入淡出,不变大也不移动;Android 除外,它的这项设置会移除所有动画。各种延迟、触摸超时,以及下一个工具提示会立即打开的那段时间,都是等待而不是动效,在任何地方都保持不变。
  • 文本为空的工具提示从不打开,也不添加任何内容。调试构建会报告这种情况,也会报告 plain 工具提示上被忽略的标题。

已知的平台差距:

  • SwiftUI、Compose、Flutter 和 React Native 还不接受 showDelay、hideDelay 和 touchTimeout,也不把间隙算作悬停:在这些平台上,每个工具提示都采用系统的延迟,指针要在隐藏延迟之内越过间隙。它们还只有四个居中的位置(top、bottom、start、end),没有 offset、crossOffset、arrow、inverse 和 shortcut;它们的工具提示只淡入淡出,不变大也不移动。
  • Web:只有在浏览器把触摸长按报告为上下文菜单的地方,长按才会打开工具提示。iOS Safari 不这样报告,所以在那里工具提示无法通过触摸打开。
  • Web:富工具提示的标题是 headline 属性。<tanrei-tooltip> 上的 title 属性属于浏览器自己,浏览器会为它画出原生的工具提示:不要设置它。
  • SwiftUI 没有工具提示角色。描述是锚点的无障碍提示(accessibility hint)。
  • iOS 上的 SwiftUI:在已聚焦的锚点上按 Return 或 Space 不会关闭工具提示,Escape 也只在锚点持有焦点时才关闭它。因轻点而获得焦点的锚点(例如文本字段)会打开它的工具提示。被设为透明或被遮住的锚点不会被检测为隐藏。
  • SwiftUI 把工具提示画在由 .tanreiTheme(_:) 和 Tanrei 对话框提供的一层里。由 SwiftUI 另行呈现的内容,例如你自己的 .sheet 或 .popover,需要加上 .tanreiTooltipLayer(),其中的工具提示才会显示。
  • Compose 没有描述属性。文本会成为锚点的状态描述,在名称和角色之后读出。Tanrei 的控件把它放在自己的节点上;其他锚点只有在某个会合并后代的祖先接收它时才会得到描述。在切换型的锚点上,例如切换图标按钮,它会取代“开”或“关”。
  • Compose:如果锚点位于打开的对话框后面,你从代码里在它上面显示的工具提示会画在对话框窗口的下方。
  • Compose:焦点位于窗口的其他地方、锚点只是被悬停时,只有在 Android 9(API 28)及更高版本上,Escape 才会到达工具提示。
  • Compose:真实鼠标的悬停、在外部的触摸、Escape 监听器以及 TalkBack 对锚点状态描述的朗读,目前只在 Robolectric 上测试过,尚未在设备上测试。
  • Flutter:画出来的工具提示始终被排除在语义之外,因为 Flutter 的工具提示角色目前还不能使用。锚点上的描述不受影响。
  • Flutter:只有焦点位于锚点之内时,或者由 Tanrei 对话框处理时,Escape 才会被消费。焦点在别处时,其他处理 Escape 的祖先(例如 Material 的对话框路由)仍然会收到它。
  • React Native:描述是锚点的 accessibilityHint,排在锚点自己的提示之后。
  • React Native:在别处触摸不会让长按打开的工具提示更早关闭。触摸超时仍然会关闭它。
  • React Native:工具提示不会跟随滚动的锚点,锚点滚出视野时它也不会关闭。只有当你把窗口的边衬区(insets)传给 TanreiProvider 时,它才会保持在安全区域之内。
  • React Native:悬停、把指针移到工具提示上、长按和 Escape 尚未在设备上检验过。

需要你做的事

  • 给锚点设置它自己的无障碍名称。工具提示从不替代它。
  • 文本重复这个名称时,把 describes 设为 false。
  • 你用 shortcut 显示的按键要由你自己绑定;这个命令出现在其他地方(例如菜单里)时,也要显示相同的按键。
  • 让锚点可以获得焦点,这样键盘用户才能打开工具提示。
  • 如果你缩短了触摸超时,请确认在剩下的时间里文本仍然读得完。超时为 0 时,工具提示只在手指按住期间显示。
  • 把用户需要的一切都放在工具提示之外。触摸用户和屏幕阅读器用户可能永远看不到它。
  • 在 React Native 上,把 TanreiProvider 放在应用的根部,并传入窗口的 insets:它的浮层会把工具提示画在安全区域之内。在 SwiftUI 上,在锚点的上层应用 .tanreiTheme(_:),并给你自己的面板和弹出层的内容加上 .tanreiTooltipLayer()。在 Flutter 上,工具提示的上层需要有一个 Overlay,每个带 Navigator 的 WidgetsApp 都有。