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

弹出框

在一小块表面上显示可供阅读和操作的内容;它在用户按下的控件旁边打开并跟随这个控件,既不让页面的其余部分变暗,也不阻挡它;在 compact 窗口中,则显示在一个具有底部面板外观的模态表面上。

content_copy以 Markdown 格式查看
类别
容器
令牌
comp.popover
层级
基础

概览

Web
tanrei-popover
schedule
SwiftUI
tanreiPopover
radio_button_unchecked
Jetpack Compose
TanreiPopover
radio_button_unchecked
Flutter
TanreiPopover
radio_button_unchecked
React Native
Popover
radio_button_unchecked

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

按下按钮:一条带链接的说明,以及一个带操作的简短表单

存储空间

回收站里的文件在删除之前仍会占用你的存储空间。存储空间如何计算

重命名取消重命名

弹出框用于用户要在打开它的控件旁边处理的内容:几项设置、一个简短的表单、一个分享面板、一条需要链接或按钮的说明。它周围的页面其余部分仍然可以使用。在 compact 窗口中,它改为以底部面板的外观出现在窗口底部;在 compact 窗口中,Tanrei 的每个弹出框和对话框都是这样。

用法

控件是你自己的。在 Web 上,你用 anchor 指明它:弹出框贴着哪个元素放置,就写哪个元素的 id。这个元素同时也是打开弹出框的控件(opener):弹出框会告诉辅助技术,这个控件会打开一个对话框,以及这个对话框当前是否打开;关闭时也把焦点交还给它。如果表面以一个输入框为锚点,而打开它的是输入框里的一个按钮,就用 opener 指明这个按钮。

弹出框与菜单一样,是受控组件。打开它的控件被按下时,由你设置 open;弹出框触发 close 时,由你把它设回 false。它从不自行打开:悬停、聚焦或经过一段时间都不会打开它。close 带有原因:escape、outside、focus-out、close(它的关闭按钮)或 anchor-hidden。操作从不关闭弹出框,关闭它的是你的处理函数。在 compact 窗口中以面板形式显示时,按下遮罩会带来 outside,而从不带来 focus-out 或 anchor-hidden。

请给它一个名称:画出标题时用 headline,不画标题时用 label。

SwiftUI、Compose、Flutter 和 React Native 尚在计划中:本页开头各平台旁边的名称是规格为它们定下的,这些平台目前还没有提供任何实现。

<tanrei-button id="share" variant="outlined">分享</tanrei-button>

<!-- anchor 是弹出框贴着放置的那个控件的 id。 -->
<tanrei-popover id="share-panel" anchor="share" headline="分享此页面" closable close-label="关闭分享">
  知道链接的任何人都可以查看。
  <tanrei-button slot="actions" dismissive>停止分享</tanrei-button>
  <tanrei-button slot="actions" confirming>复制链接</tanrei-button>
</tanrei-popover>

<!-- 输入框下方的表面:与输入框等宽,焦点留在输入框里。 -->
<tanrei-text-field id="city" label="城市"></tanrei-text-field>
<tanrei-popover anchor="city" label="建议" width="anchor" initial-focus="anchor" no-inset>…</tanrei-popover>

<script type="module">
  import '@tanrei/web/components/popover';

  const opener = document.querySelector('#share');
  const panel = document.querySelector('#share-panel');
  // 由打开它的控件来切换。在这个控件上按下从不算“在外部”,所以按一次不会先关闭再重新打开。
  opener.addEventListener('click', () => (panel.open = !panel.open));
  // Escape、在外部按下、焦点离开、关闭按钮、锚点离开视野。
  panel.addEventListener('close', (e) => {
    console.log(e.detail.reason);
    panel.open = false;
  });
  // 操作不会关闭它:由处理函数在工作完成后关闭。
  panel.querySelector('[confirming]').addEventListener('click', async () => {
    await navigator.clipboard.writeText(location.href);
    panel.open = false;
  });
</script>
import { useState } from 'react';
import { Button, Popover } from '@tanrei/react';

const [open, setOpen] = useState(false);

<Button id="share" variant="outlined" onClick={() => setOpen(!open)}>分享</Button>
<Popover anchor="share" open={open} headline="分享此页面" closable closeLabel="关闭分享" onClose={() => setOpen(false)}>
  知道链接的任何人都可以查看。
  <Button slot="actions" dismissive onClick={stopSharing}>停止分享</Button>
  <Button slot="actions" confirming onClick={copyLink}>复制链接</Button>
</Popover>

规格

位置、箭头,以及与锚点等宽的弹出框

上方,居中回收站里的文件仍会占用存储空间。侧面,末尾侧回收站里的文件仍会占用存储空间。与这个按钮等宽回收站里的文件仍会占用存储空间。

位置

placement 指明弹出框位于锚点的哪一侧,以及沿这一侧怎样对齐。它的十三个值就是工具提示的那十三个,含义也相同。

值 位置
bottom-start(默认)、bottom、bottom-end 锚点下方:起始边对齐、居中,或末尾边对齐
top-start、top、top-end 锚点上方,同样的三种对齐方式
start、start-top、start-bottom 锚点的起始侧:居中、顶边对齐,或底边对齐
end、end-top、end-bottom 锚点的末尾侧,同样的三种对齐方式
auto 按下方、上方、末尾侧、起始侧的顺序,取第一个有空间的

起始和末尾随布局方向而变。offset 是到锚点的距离,以平台的布局单位计;不设置时采用 comp.popover.offset。crossOffset 让弹出框沿它所在的一侧移动,但绝不会移出锚点的这一侧。

弹出框与窗口边缘保持 comp.popover.window-margin 的距离,为此依次采取以下步骤:

  1. 首选的一侧没有空间时,它翻转到相对的一侧,并保持原来的对齐方式。
  2. 两侧都没有空间时,它取空间较大的一侧,并且尺寸以这块空间为限。这时它的内容可以滚动。
  3. 它沿所在的一侧平移,直到满足边距的要求。
  4. 窗口比它的最小尺寸还小时,它宁可盖住锚点,也不超出窗口。

锚点移动时,它跟随锚点;只要所在的一侧还有空间,它就留在这一侧。锚点完全看不见时,它触发 close,原因是 anchor-hidden。

尺寸

弹出框与它的内容一样宽,介于 comp.popover.container.min-width(160)和 container.max-width(400)之间。设置 width="anchor" 后,它与锚点一样宽,适合从输入框下方展开的表面。它绝不会比窗口减去边距后的宽度更宽。

它与各个部件加起来一样高,上限是 container.max-height(480)和所在一侧的可用空间。超过这个高度时,内容在页眉和操作之间滚动,页眉和操作留在原位。只要内容在滚动,页眉下方和操作上方就各有一道分隔线。把 dividerVisibility 设为 scroll(在 Web 上是 divider-visibility),页眉下方的分隔线就只在内容被滚动之后出现,操作上方的只在下面还有更多内容时出现;cupertino 和 m3-like 为所有弹出框选择了这种方式。SwiftUI、Compose、Flutter 和 React Native 在跟上之前都这样绘制。

对于自带内边距或一直延伸到边缘的内容,例如列表,请把 inset 设为 false(在 Web 上是 no-inset)。

部件

部件 它是什么
页眉 标题,以及 closable 为 true 时的关闭按钮。内容滚动时它留在原位
内容 默认插槽。可以是任何东西,包括链接、输入框和其他控件
操作 actions 插槽里的 Tanrei 按钮;属于确认或取消性质的,各自标为 confirming 或 dismissive。它们按按钮组的方式摆放,标签放不下时叠成一列
箭头 一个可选的三角形,指向锚点。arrow 为单个弹出框打开它;comp.popover.arrow.visible 为整个设计系统打开它

没有 variant 的操作采用 comp.popover.actions.variant(在默认系统中是 tonal)。关闭按钮是一个纯图标的 small 按钮,变体取自 comp.popover.close-button.variant:在默认系统中是 tonal,与 Tanrei 的每个关闭按钮一样,在任何内容上都能被看作一个控件。m3-like 和 cupertino 预设保留各自不带底色的字形(text)。

动效

弹出框的出现方式与 Tanrei 中所有出现的界面相同:从朝向锚点的那条边的中点淡入,同时略微变大(默认系统中从 96 % 开始);关闭时沿原路返回,稍快一些。被打断的动效会从当前位置掉头。在减弱动态效果时,它只淡入淡出。在 m3-like 预设下,它改为像 Material 的菜单那样从那条边展开。在页面标记中写有 open 的弹出框从第一帧起就在那里,不带动效。

在 compact 窗口中

在窄于 comp.popover.compact.window-width(默认系统中为 600)的窗口中,弹出框采用底部面板的外观。这是 Tanrei 对 compact 窗口中每个弹出框和每个对话框的规则。你打开的仍是同一个弹出框,props 和事件也相同;改变的只是它的绘制方式。

  • 它贴在窗口的底边,宽度与窗口相同,最宽到 comp.bottom-sheet.container.max-width,使用底部面板的颜色和高度(elevation)。上方的角取 comp.bottom-sheet.container.shape,下方的角是直角。屏幕键盘出现时,它位于键盘之上。
  • 它的高度随内容而定,但不会高于窗口减去顶部的 comp.bottom-sheet.container.margin-block-start。超出部分的内容在标题区和操作之间滚动。
  • 它没有箭头,也没有拖动手柄,不跟随拖动。placement、offset、crossOffset 和 width 都不适用。
  • 它是模态的:底部面板的遮罩让页面变暗,它后面的一切都变为惰性,焦点留在它里面。Escape、返回、按下遮罩和关闭按钮都会关闭它。
  • 它以底部面板的动效从底边升起,关闭时再滑下去。在减弱动态效果时,它在原地淡入淡出。
  • 弹出框打开时如果窗口宽度越过这个值,它会立即改变形态,没有动效,焦点保持不变。

把 compactPresentation 设为 own(Web 上为 compact-presentation="own"),可以让某个弹出框在任何窗口中都留在锚点旁边,用于必须紧挨着所属内容的表面。initialFocus="anchor" 的弹出框总是留在锚点旁边,因为用户正在输入的输入框必须保持可用。从不想要面板的设计系统把 comp.popover.compact.window-width 设为 0。

这里说的弹出框不包括菜单:菜单、选择器或组合框的列表以及按钮的菜单,在任何窗口中都留在它们的控件旁边。

构成

部件 说明
1 anchor The element the popover stands beside: the product's own control (a Button, an icon-only Button, a chip, a field). The popover reads its place, never draws or restyles it. It is also the opener — the element a person presses to open the popover, which carries the open state and takes focus back — unless the product names another element as the opener (a field that anchors the surface, with a button inside it that opens it).
2 scrim · 可选 In a compact window only, while the popover is presented as a sheet: the bottom sheet's scrim over everything behind it (POP-40). It blocks presses and asks the popover to close when pressed.
3 container The raised surface that holds everything else, placed on one side of the anchor and aligned along it; in a compact window, with the bottom sheet's appearance at the window's bottom edge (POP-39). It answers no pointer and no press itself and has no states: what is operated is inside it.
4 arrow · 可选 A triangle on the container's edge that faces the anchor, pointing at it; one shape with the container.
5 header · 可选 The row at the top that holds the headline and the close button. It stays in place while the content scrolls.
6 headline · 可选 What the popover is about ("Filters", "Share this page"). It is the popover's accessible name where it is given.
7 close-button · 可选 An icon-only Button of the small size showing the close glyph at the end of the header, in the variant comp.popover.close-button.variant names (tonal in the default system, as every host's close button is). The popover places it and chooses its variant; everything else it reads from Button's own tokens.
8 content Whatever the product puts in the popover: text, links, fields, a list, a few settings. The popover insets it, hands it a content color and scrolls it between the header and the actions; it does not lay it out.
9 divider · 可选 Hairlines across the container below the header and above the actions, drawn only while the content scrolls: for as long as it does in the default system, or only while content lies beyond each line under dividerVisibility scroll.
10 actions · 可选 Tanrei Buttons at the foot of the popover, each marked confirming or dismissive where it is one, laid out as a Button group. A Button whose variant the product did not set is tonal in the default system. Optional: a popover whose content acts at once (a setting, a link) needs none.

属性

属性 类型 默认值 说明
open boolean false Whether the popover is shown. The product opens it when its opener is pressed, and never on hover or on focus. Where open is a plain value (web, Compose, React Native) the product closes the popover by setting it to false when the popover emits close; where it is a binding or a controller (SwiftUI, Flutter) the popover closes itself through it (POP-21).
placement top | top-start | top-end | bottom | bottom-start | bottom-end | start | start-top | start-bottom | end | end-top | end-bottom | auto "bottom-start" Where the popover prefers to stand: a side of the anchor, and how it lines up along that side (POP-2). The thirteen values are the tooltip's, read the same way; the menu's four are among them. 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 (POP-3). Start and end follow the layout direction. The popover flips to the opposite side when the preferred one has no room, keeping the alignment (POP-5). The default is the menu's: below the anchor, start edges aligned.
offset number null The distance between the anchor's edge and the popover, in the platform's layout units; 0 lets them touch. Null takes comp.popover.offset, the system's answer for every popover. Set it only where one anchor's box is not what the eye takes for its edge.
crossOffset number 0 How far the popover moves along the side it stands on, in the platform's layout units, from where placement puts it: positive towards the end for a popover above or below, towards the bottom for one beside. It never moves the popover off the anchor's side (POP-4).
arrow boolean null Whether the popover draws an arrow that points at the anchor. Null, the default, takes comp.popover.arrow.visible, the system's choice, which is off in the default system (POP-10).
width content | anchor "content" What decides the container's width. content: as wide as its content, between comp.popover.container.min-width and comp.popover.container.max-width. anchor: as wide as the anchor, for a surface that drops from a field and should read as its continuation (POP-7). A structural choice drawn by code, not a token axis.
compactPresentation sheet | own "sheet" How the popover is presented in a compact window, one narrower than comp.popover.compact.window-width. sheet: with the bottom sheet's appearance at the window's bottom edge, as a modal dialog (POP-39 to POP-41), as every popover and every dialog of Tanrei is there. own: as a popover in every window, beside its anchor and not modal, narrowing to the window (POP-43), for a surface that must stay beside what it belongs to (a note beside the word it explains). initialFocus anchor keeps the popover's own presentation whatever this says. A structural choice drawn by code, not a token axis.
inset boolean true Insets the content by the container's paddings. Set it to false for content that pads itself or reaches the edges (a list whose rows carry their own padding, an image); the header and the actions keep their insets.
initialFocus popover | anchor "popover" Where focus is when the popover has opened. popover: focus moves into it (POP-19), which is right whenever the opener is a button. anchor: focus stays where it is, for a popover that opens under a field the person is typing in, whose keys the field keeps; Tab then moves into the popover (POP-20). It also keeps the popover beside its anchor in a compact window (POP-43): a modal sheet would make the field inert.
headline string "" What the popover is about, drawn in its header and used as its accessible name. Empty draws no headline; the popover is then named by label.
label string "" Names the popover for assistive technology where it draws no headline ("Text formatting"). Never drawn. Required while headline is empty; ignored while headline is given.
closable boolean false Draws the close button at the end of the header. Escape and a press outside already close a popover, so the button is for a popover large enough to cover what a person would press outside it, and for touch, where there is no Escape.
closeLabel string "" The close button's accessible name ("Close filters"). Required while closable is true.
dividerVisibility auto | always | scroll "auto" When the dividers below the header and above the actions are drawn while the content scrolls between them. always: both, for as long as the content scrolls, wherever it is scrolled to. scroll: each only while content lies beyond it — below the header while the content is not at its top, above the actions while more of it lies below. auto takes comp.popover.divider.visibility, the design system's answer (always in the default system). Under every choice a popover whose parts all fit draws none.

插槽

插槽 说明
default The content, below the header. A fixture's content string is plain text, which takes sys.typescale.body in the content color (POP-14).
actions The action items: Tanrei Buttons, each marked confirming or dismissive where it is one. The popover places them (POP-15) and gives those without a variant its own (POP-16). A fixture fills it through its items.

事件

事件 说明
close The popover asks to be closed, every overlay opened from inside it with it: Escape or the platform's back, a press outside, focus leaving it, its close button, or its anchor going out of sight. Carries which of these it was, named escape, back, outside, focus-out, close or anchor-hidden (in the platform's casing). An action never causes it (POP-17). A popover presented as a sheet sends escape, back, outside (a press on its scrim) or close, never focus-out or anchor-hidden (POP-40).

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

action

One action at the foot of the popover: a Tanrei Button, placed by the popover as a Button group places its items and given the popover's variant unless the product set one.

属性 类型 默认值 说明
variant string "" The Button's variant (filled, tonal, elevated, outlined, text, or one a system added). Empty, the default: the popover gives it the variant comp.popover.actions.variant names, tonal in the default system (POP-16). Set it to give one action another look.
disabled boolean false As the Button's disabled. A disabled action keeps its place and its share of the width.
confirming boolean false The action completes what the popover is for (Apply, Save, Share). Placed at the end of a row and at the top of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19).
dismissive boolean false The action backs out (Cancel, Reset). Placed at the start of a row and at the bottom of a column (BGR-13, BGR-14). It decides the place only, never the look (BGR-19).
插槽 说明
label The Button's label, as its default slot. A fixture item's content is this label.
事件 说明
click The action was activated, as the Button's click. The popover does not close by itself (POP-17).
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-button TanreiButton TanreiButton TanreiButton Button
variant · 属性 variant variant variant variant variant
disabled · 属性 disabled disabled enabled onPressed disabled
confirming · 属性 confirming tanreiGroupPlacement(.confirming) Modifier.confirming() TanreiButtonGroupItem.placement confirming
dismissive · 属性 dismissive tanreiGroupPlacement(.dismissive) Modifier.dismissive() TanreiButtonGroupItem.placement dismissive
click · 事件 click action onClick onPressed onPress
备注 A tanrei-button child with slot="actions": its label is its content. confirming and dismissive are boolean attributes that the popover reads, as the button group does. A Button without a variant attribute takes the popover's variant through the internal controller the group already gives its items (POP-16); the attribute, once written, wins. A TanreiButton in the actions: builder. The flags are the button group's .tanreiGroupPlacement(_:) modifier (one enum: both cannot be set). A Button created with variant: nil reads the popover's variant from an environment value the popover sets (internal to the package). A TanreiButton in the actions lambda, whose receiver is the button group's scope, so the flags are its Modifier.confirming() and Modifier.dismissive(). enabled is disabled's inverse. variant: TanreiButtonVariant? = null takes the popover's from a CompositionLocal (internal to the library). A TanreiButton in actions:; a null onPressed disables it. The flags are the button group's TanreiButtonGroupItem(placement:, child:) (one enum: both cannot be set). A Button with variant: null takes the popover's from an InheritedWidget (internal to the package). A Button in the actions prop. confirming and dismissive are props the popover reads from its actions, as the button group reads them. A Button with no variant takes the popover's from a React context (internal to the package).

状态

行为

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

ID 需求
POP-1 WHEN open becomes true THE popover SHALL present its container in the platform's overlay layer, the one the menu's surface uses (the web's top layer through the popover API, an overlay SwiftUI draws above its anchor, Compose's Popup, Flutter's Overlay, React Native's overlay layer that TanreiProvider renders), above the other content of its window at comp.popover.container.elevation; WHILE it is open beside its anchor — in every window but a compact one where it is a sheet (POP-39, POP-40) — THE popover SHALL NOT trap focus, SHALL NOT make any other part of the window inert, unscrollable or hidden from assistive technology and SHALL draw no scrim: it is an operated overlay that is not modal. WHEN open becomes false THE popover SHALL remove the container.
POP-2 THE popover SHALL read placement as a side and an alignment along that side, as the tooltip reads the same thirteen values (TIP-22): 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 changes while the popover is shown THE popover SHALL take its new place at once, without a motion, keeping its focus.
POP-3 WHILE placement is auto THE popover SHALL try the sides in the order bottom, top, end, start and SHALL take the first that has room for the container within comp.popover.window-margin, or the one with the most room WHEN none has, with its start edge on the anchor's start edge for bottom and top and its top edge on the anchor's top edge for end and start, and SHALL then shift, hold and follow as POP-5 says, keeping the side it took for as long as that side has room.
POP-4 THE popover 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.popover.offset WHILE offset is null; it SHALL then move the container along its side by crossOffset, before the shift of POP-5, and by no more than leaves comp.popover.arrow.width of the container beside the anchor, so that the popover 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 popover SHALL use comp.popover.offset in its place, IF crossOffset is not a finite number THE popover SHALL use 0, and in debug builds it SHALL report the value it ignored.
POP-5 THE popover SHALL keep its container at least comp.popover.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 and limit the container to that room (POP-7 for its width, POP-8 for its height); 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 popover's least size) 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 popover from side to side. 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 THE popover SHALL emit close once with the reason anchor-hidden and be closed as POP-21 says, because a surface with nothing beside it no longer says what it belongs to.
POP-6 THE popover SHALL fill its container with comp.popover.container.color, raise it by comp.popover.container.elevation, round it by comp.popover.container.shape eased 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), and clip its header, content and actions to that shape. THE popover SHALL NOT change anything the container draws under a pointer, a press or focus: the container has no hovered, focused or pressed state and draws no state layer.
POP-7 WHILE width is content THE popover SHALL make its container as wide as its widest part (its header, its content, or its actions in one row at their own widths) and its paddings, at least comp.popover.container.min-width and at most comp.popover.container.max-width, wrapping content that would be wider; WHILE width is anchor it SHALL make the container as wide as the anchor, and at least comp.popover.container.min-width where the anchor is narrower, comp.popover.container.max-width not applying, and SHALL follow the anchor's width as it changes. In either case the container SHALL never be wider than the window's safe area less comp.popover.window-margin on each side, narrowing below comp.popover.container.min-width where the window asks it to.
POP-8 THE popover SHALL make its container as tall as its parts and its paddings, at most comp.popover.container.max-height and at most the room its side leaves within comp.popover.window-margin (POP-5). WHEN its parts do not fit in that height THE popover SHALL keep the header and the actions in place and scroll the content between them, SHALL let keyboard focus and assistive technology scroll it, and SHALL draw a divider of comp.popover.divider.width in comp.popover.divider.color across the container below the header and another above the actions: WHILE its divider visibility (POP-38) is always, both for as long as the content scrolls, wherever it is scrolled to, at its top included; WHILE it is scroll, the one below the header only WHILE the content is scrolled away from its top and the one above the actions only WHILE more of it lies below; WHILE everything fits, or there is no header or no action on that side of the content, THE popover SHALL draw no divider there. WHEN the header and the actions alone leave no room for the content (large text in a short window) THE popover SHALL scroll all of its parts together instead, so that nothing is cut off or unreachable. The dividers SHALL be hidden from assistive technology and SHALL show and hide at once, without motion.
POP-9 THE popover SHALL lay out, top to bottom, inside comp.popover.container.padding-block at the top and bottom edges of its container: WHILE headline is not empty or closable is true, the header (POP-11); then the content, comp.popover.container.gap below the header where there is one; then, WHILE the popover has at least one action, the actions, comp.popover.actions.margin-block-start below the content, or below the header where there is no content. WHILE inset is true THE popover SHALL inset the content by comp.popover.container.padding-inline from the container's start and end edges; WHILE inset is false it SHALL let the content reach the container's start and end edges — and its top and bottom edges where no header or no action stands there, the block padding then not applying on that side — clipped by the container's shape, the header and the actions keeping their own insets.
POP-10 THE popover SHALL draw an arrow WHILE arrow is true, and WHILE arrow is null and comp.popover.arrow.visible is on, and none otherwise. WHILE it draws one THE popover SHALL draw a triangle comp.popover.arrow.width wide at its base and comp.popover.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.popover.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.popover.arrow.height farther from the anchor, so that the tip keeps the offset (POP-4). WHEN the container flips, shifts or follows the anchor (POP-5) 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 (POP-5) THE popover SHALL draw no arrow. The arrow SHALL take no press of its own and SHALL be hidden from assistive technology.
POP-11 WHILE headline is not empty or closable is true THE popover SHALL draw the header as one row: the headline from comp.popover.container.padding-inline after the container's start edge, taking the width the close button leaves, and, WHILE closable is true, the close button (POP-13) at the row's end, comp.popover.header.gap after the headline and with its edge comp.popover.container.padding-inline before the container's end edge, both aligned to the row's first line; the header SHALL be as tall as the taller of the two and SHALL stay in place while the content scrolls (POP-8).
POP-12 WHILE headline is not empty THE popover SHALL draw it in comp.popover.headline.typescale and comp.popover.headline.color, wrapping onto more lines rather than truncating and growing the header with it, and SHALL expose it as a heading. THE popover SHALL set the headline'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.
POP-13 WHILE closable is true THE popover SHALL draw at the end of its header an icon-only Button of the small size showing the glyph close of the system's icon set and named by closeLabel, in the variant comp.popover.close-button.variant names, given through the channel a Button reads from the component that places it, and otherwise unrestyled: it reads Button's own tokens, answers hover, focus and press as any Button does and keeps its own minimum target (BTN-5); WHEN the user activates it THE popover SHALL emit close exactly once with the reason close. WHILE closable is true and closeLabel is empty it SHALL still draw the button, and in debug builds report that the button has no accessible name. THE popover SHALL draw no close mark of its own (no path, shape or character of its own).
POP-14 THE popover SHALL hand its content comp.popover.content.color as the platform's inherited foreground (the web's inherited color, SwiftUI's foreground style, Compose's content color, Flutter's default text and icon style, React Native's Tanrei text context); plain text given as content, where the platform must style it, SHALL take sys.typescale.body. THE popover SHALL NOT lay out, restyle or mirror the content itself, and SHALL let it hold any element that takes input — links, Buttons, fields, a list, a menu's or another popover's opener.
POP-15 WHILE the popover has at least one action THE popover SHALL lay out its actions as a standard Button group lays out its items (BGR-12 to BGR-19), comp.popover.actions.gap apart and comp.popover.container.padding-inline from the container's start and end edges, in the order the group gives them — dismissive at the start and confirming at the end of a row, confirming on top and dismissive at the bottom of a column, unflagged actions between them in the order given — with focus and reading order following that order (BGR-15). WHILE comp.popover.actions.arrangement is fill it SHALL make them a full-width group across the container's inner width, the actions sharing a row equally (BGR-17) or each as wide as the column; WHILE it is end it SHALL keep them at their own widths, the row aligned to the end, and a column as wide as its widest action and aligned to the end. It SHALL place them in a row WHILE every action's label fits its share on one line (fill) or the row at its own widths fits the inner width (end), and otherwise in a column, re-deciding when the container's width or the text size changes.
POP-16 WHILE an action's variant is empty THE popover SHALL give that Button the variant comp.popover.actions.variant names, through the channel a Button reads from the component that places it, so that the Button renders from that variant's tokens and states as it would with the variant set; WHILE an action's variant is set THE popover SHALL leave it as set. THE popover SHALL NOT change an action's size or tokens, and SHALL NOT change its look or emphasis for its confirming or dismissive (BGR-19).
POP-17 WHEN the user activates an action THE popover SHALL let the Button emit its own click and SHALL NOT close, emit close or otherwise answer it itself, whether the action is confirming, dismissive or neither; the product closes the popover from the action's handler by setting open to false, and focus then returns as POP-22 says.
POP-18 THE popover SHALL be shown exactly while open is true: it SHALL NOT open or close itself when a pointer enters or leaves its anchor or its container, when focus arrives at its anchor, on a long-press or after a time, and SHALL have no show or hide delay; the product sets open when the opener is activated by pointer, touch, Enter or Space, and sets it to false when the opener is activated again. THE popover SHALL expose on its opener that it opens a dialog, whether that dialog is open and which element it is, where the platform has those properties (aria-haspopup with the value dialog, aria-expanded and aria-controls on the web), and SHALL NOT change the opener's accessible name, role or look. The opener is the anchor unless the product names another element as the opener, in which case the container is placed against the anchor (POP-2 to POP-5) and everything this spec says of the opener holds for that element.
POP-19 WHEN open becomes true WHILE initialFocus is popover THE popover SHALL move focus to the element in it that the product marked for initial focus (the platform's own mark, named in the platform notes) or, without one, to its container, so that assistive technology reads its name first and the next Tab reaches its first focusable element; it SHALL show a focus indicator on the container only when the popover was opened from the keyboard, as sys.behavior.focus-style names. WHEN open becomes true WHILE initialFocus is anchor THE popover SHALL leave focus where it is and SHALL take no key from the element that has it except Escape (POP-23). Focus SHALL move when the popover opens, not when its motion ends; a popover mounted with open true SHALL NOT move focus.
POP-20 WHILE the popover is open THE popover SHALL stand in the focus order and in the reading order of assistive technology directly after its opener, wherever the platform draws it: Tab from the opener SHALL move to the popover's first focusable element, Shift+Tab from that element SHALL move back to the opener, Tab and Shift+Tab inside the popover SHALL move through its close button, its content's focusable elements and its actions in the order they are placed, and Tab from its last focusable element SHALL move to the element that follows the opener in the page's order, which closes the popover as focus leaving it does (POP-21). Focus on the opener SHALL count as focus that has not left the popover, so that Shift+Tab to the opener leaves the popover open. THE popover SHALL NOT wrap focus from its last element to its first.
POP-21 WHEN the user presses Escape or the platform's back (Android's back gesture or button, the iOS accessibility escape gesture), presses outside the container, its arrow, its anchor, its opener and every overlay opened from inside it (POP-24), moves focus to an element outside all of those, or activates the close button (POP-13), or WHEN the anchor goes out of sight (POP-5) THE popover SHALL emit close once with that reason (escape, back, outside, focus-out, close or anchor-hidden) and SHALL be closed: through its binding or controller where open is one (SwiftUI, Flutter), and by the product setting open to false where open is a plain value (web, Compose, React Native). A press on the opener or the anchor is never a press outside: the product's own handler closes the popover from it (POP-18), so that one press never closes and opens it again. Focus leaving the window (another window or application taking it) SHALL NOT count as focus leaving the popover, and neither SHALL a move of an assistive technology's reading cursor that moves no input focus. Whether a press outside also reaches the element under it SHALL follow the platform and be stated in its README, as for the menu (MNU-5).
POP-22 WHEN the popover closes THE popover SHALL return focus to its opener IF focus was inside the popover or an overlay opened from inside it, or was lost with it (a press outside that landed on nothing focusable), and SHALL leave focus where the person put it IF the close came from focus moving to, or a press landing on, another focusable element; WHERE the opener no longer exists or cannot take focus it SHALL fall to the platform's default for a closed overlay. It SHALL show the opener's focus indicator only when the close came from the keyboard. Focus SHALL move when the popover closes, not when its motion ends.
POP-23 WHILE the popover is open, WHEN the user presses Escape or the platform's back and neither an overlay opened from inside it nor an element inside it has taken the key for itself (an open menu, a tooltip, a field composing text or clearing its value) THE popover SHALL take the key and close as POP-21 says, and SHALL let it go no further, so that it does not also close a dialog or a sheet the popover stands in; WHILE several overlays are open Escape SHALL close only the topmost.
POP-24 WHEN an overlay opens from an element inside the popover's container — a menu, a tooltip, a docked picker or a select's surface, another popover, a dialog or a modal sheet — THE popover SHALL treat it as its own: a press inside it SHALL NOT count as a press outside, focus moving into it SHALL NOT count as focus leaving, and the popover SHALL stay open under it, inert with the rest of the window WHILE that overlay is modal. WHEN that overlay closes THE popover SHALL receive back the focus it returns. WHEN the popover is closed, for a reason POP-21 names or because the product sets open to false, THE popover SHALL close every overlay opened from inside it that is not modal with it at once, their own close events carrying no reason of the person's, and SHALL return focus to its own opener as POP-22 says, not to an opener inside it. WHEN a popover is opened from inside a popover that was itself opened from inside a popover THE popover SHALL still work, and in debug builds SHALL report the depth.
POP-25 WHERE the popover's anchor is inside a modal overlay (a dialog, a modal sheet) THE popover SHALL be presented above that overlay and belong to it: operable while the rest of the window is inert, and in that overlay's focus loop directly after its opener (POP-20). WHEN a popover opens while another popover that it was not opened from is open THE popover SHALL be drawn above it; THE popover SHALL NOT close another popover by opening — the press or the focus that opened it closes the other as POP-21 says — so that the product that sets two open at once has two.
POP-26 WHEN the popover opens beside its anchor THE popover SHALL make its container appear as every anchored surface appears: from sys.motion.appear.opacity, sys.motion.appear.scale, sys.motion.appear.distance toward the anchor and sys.motion.appear.blur to whole, grown from the middle of the edge that faces the anchor — from its own centre WHERE sys.motion.appear.origin is center, and WHILE it covers its anchor (POP-5) — over comp.popover.motion.open-duration with comp.popover.motion.open-easing, and WHEN it closes THE popover SHALL take it back the same way to where it started, over comp.popover.motion.close-duration with comp.popover.motion.close-easing. WHERE sys.behavior.appearance is unfold the scale SHALL apply along the axis that leaves the anchor only, so that the container unfolds from that edge at its full width. WHERE motion is reduced THE popover SHALL only fade it in and out, over sys.motion.role.reduced, with no scale, distance or blur. WHEN it is mounted with open true THE popover SHALL appear without its motion while the document has not yet been first painted, and with it when it is mounted after that. The motion SHALL be drawn only: the place POP-5 gives and the room the popover is measured for are those of the popover at rest.
POP-27 WHEN the popover is told to close while it is still opening, or to open while it is still closing THE popover SHALL turn round from the opacity and the size 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 popover is closed or closing THE popover SHALL take no pointer input and no focus — a press reaches what lies under it — and WHILE it is closed it SHALL take no room in the layout, draw nothing and expose nothing to assistive technology.
POP-28 THE popover SHALL expose its container as a dialog named by headline or, WHILE headline is empty, by label: one that is not modal, hiding nothing outside it from assistive technology, WHILE it stands beside its anchor, and a modal dialog WHILE it is presented as a sheet (POP-40); the close button SHALL be named by closeLabel, each action by its own label, and the arrow and the dividers SHALL be hidden. THE popover SHALL NOT expose its text as its anchor's description: what describes an anchor is a tooltip.
POP-29 IF headline and label are both empty THE popover SHALL still open, and in debug builds SHALL report that it has no accessible name. IF the popover opens with no content, no headline and no action THE popover SHALL draw nothing, take no focus and expose nothing, and in debug builds SHALL report an empty popover. IF no anchor can be found when open becomes true THE popover SHALL stay closed, emit nothing, and in debug builds report the missing anchor.
POP-30 WHILE the text size is enlarged THE popover SHALL keep its width limits (POP-7), wrap its headline and let its content wrap, grow its container's height with them up to the limits of POP-8 and scroll beyond them, stack its actions once a label no longer fits its share (POP-15), and SHALL NOT scale the close button, the arrow, the offset or the window margin beyond what the platform's layout units do. Nothing in the header or the actions SHALL be clipped or truncated.
POP-32 WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE popover SHALL draw an edge sys.border.hairline wide in the platform's text color round its container and its arrow, as one outline, because the mode removes the shadow and may give the container the page's color; its headline and plain text SHALL take the platform's text color.
POP-33 THE popover 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 popover is shown THE popover SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus, the container placed again by the new direction (POP-34).
POP-34 WHEN the layout direction is right-to-left THE popover 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 popover above or below as the right and the left edges, and a positive crossOffset of such a popover as a move towards the left, flipping and shifting from there as POP-5 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, and SHALL keep auto's order of sides by their names (POP-3). It SHALL mirror its header — the headline at the right, the close button at the left — apply its paddings by logical start and end, and place its actions as a Button group places them right to left (BGR-8); it SHALL NOT mirror the glyph close, the arrow's shape, or its content, which follows the layout direction itself.
POP-35 THE popover SHALL lay out its headline as text of its own direction, isolated from the text around it: it 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 popover SHALL still place the headline and align its lines by the layout direction, not by the text's own.
POP-36 WHILE the on-screen keyboard is shown THE popover SHALL take the keyboard's top edge as an edge of the window's safe area (POP-5), and SHALL keep to the visible part of the window wherever that is smaller than the window: it SHALL flip, shift or shorten so that no part of it is under the keyboard, and SHALL scroll the focused element into the room left; WHEN the keyboard hides it SHALL take its place again by POP-5. It SHALL move as the platform moves content for its keyboard, and at once where motion is reduced.
POP-37 WHEN placement, width or initialFocus names a value the popover does not define THE popover SHALL render with the default value, and in debug builds report the unknown value.
POP-38 THE popover SHALL draw its dividers (POP-8) as its dividerVisibility names: WHILE dividerVisibility is auto, as comp.popover.divider.visibility names, which follows sys.behavior.divider-visibility unless a design system sets the popover apart; WHILE dividerVisibility is always or scroll, as that value names, whatever the tokens say. WHEN dividerVisibility, or the token it follows, changes THE popover SHALL draw the other form at once, moving no part and keeping the scroll position and focus; WHEN dividerVisibility names a value the popover does not define THE popover SHALL treat it as auto and in debug builds report the unknown value. A platform that cannot draw a choice SHALL fall back to the first value, always, and state that in its README.
POP-39 WHILE the window is narrower than comp.popover.compact.window-width (a compact window), compactPresentation is sheet and initialFocus is popover THE popover SHALL present its container with the bottom sheet's appearance (BSH-7) instead of beside its anchor: at the window's bottom edge, as wide as the window but no wider than comp.bottom-sheet.container.max-width and centred in it, filled with comp.bottom-sheet.container.color, raised by comp.bottom-sheet.container.elevation, its top corners rounded by comp.bottom-sheet.container.shape eased by sys.shape.corner-smoothing and its bottom corners square, its color reaching under the bottom system inset and its parts above it, and sitting on the on-screen keyboard while it is shown; as tall as its parts, but never taller than the window below its top system inset less comp.bottom-sheet.container.margin-block-start, its content scrolling beyond that as POP-8 says. It SHALL draw no arrow and no drag handle, follow no drag, close on no swipe, and ignore placement, offset, crossOffset and width there; its header, content and actions SHALL keep their own tokens and layout (POP-9 to POP-17, POP-38) across the sheet's width. WHERE comp.popover.compact.window-width is 0 no window is compact.
POP-40 WHILE the popover is presented as a sheet (POP-39) THE popover SHALL be a modal overlay, as a modal bottom sheet is (BSH-2): it SHALL draw comp.bottom-sheet.scrim.color with comp.bottom-sheet.scrim.blur over the whole window behind it (MDL-1), make everything outside it and the overlays opened from inside it inert, keep the content behind it from scrolling, keep focus inside it — Tab and Shift+Tab wrap between its last and first focusable elements — and expose its container as a modal dialog (POP-28). WHEN it opens it SHALL move focus into it as POP-19 says for initialFocus popover — mounted with open true as well, because everything behind it is then inert (as a modal sheet does, BSH-3) — and WHEN it closes it SHALL return focus to its opener as POP-22 says. A press on the scrim SHALL ask to close with the reason outside, Escape with escape (POP-23) and the platform's back with back, and the close button with close (POP-21); since focus cannot leave it and it does not stand beside its anchor, it SHALL NOT ask with focus-out or anchor-hidden and SHALL NOT follow its anchor. Its opener SHALL still say that it opens a dialog and whether it is open (POP-18).
POP-41 WHEN the popover opens as a sheet (POP-39) THE popover SHALL slide its container up from below the window's bottom edge into its place while fading the scrim in, as a modal bottom sheet opens (BSH-1), over comp.bottom-sheet.motion.enter-duration with comp.bottom-sheet.motion.enter-easing, and WHEN it closes there it SHALL slide the container down out of view while fading the scrim out, over comp.bottom-sheet.motion.exit-duration with comp.bottom-sheet.motion.exit-easing, turning round from where it has got to as POP-27 says. WHERE motion is reduced it SHALL fade in and out in place without sliding, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's). Focus SHALL move when it opens or closes, not when its motion ends.
POP-42 WHEN the window's width crosses comp.popover.compact.window-width, or compactPresentation or initialFocus changes, while the popover is open THE popover SHALL take the other presentation at once, without a motion — beside its anchor and not modal (POP-1 to POP-5), or as a sheet (POP-39, POP-40) — keeping its open state, its scroll position and the element in it that has focus, and SHALL emit nothing; IF focus is outside it (on its opener) as it becomes a sheet THE popover SHALL move focus into it as POP-19 says, because the opener is then inert.
POP-43 WHILE compactPresentation is own, or initialFocus is anchor, THE popover SHALL stay an anchored popover that is not modal in every window, compact ones included: WHERE the window is narrower or shorter than the container wants THE popover SHALL narrow and shorten the container as POP-7 and POP-8 say, cover its anchor as POP-5 says where it must, and scroll, and SHALL NOT draw a scrim or trap focus. A popover whose initialFocus is anchor stays beside its anchor because focus stays in the element the person is typing in, which a modal sheet would make inert. WHEN compactPresentation names a value the popover does not define THE popover SHALL treat it as sheet and in debug builds report the unknown value.
POP-44 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE popover SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its dialog (POP-28), beside its anchor and as a sheet alike — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE headline or label is not empty it is the dialog's name (POP-28), over a name given so; WHILE both are empty, a name given so is the dialog's name and SHALL NOT be reported as having none in debug builds. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE popover SHALL take it off that element.

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-popover tanreiPopover TanreiPopover TanreiPopover Popover
open · 属性 open isPresented visible controller visible
placement · 属性 placement placement placement placement placement
offset · 属性 offset offset offset offset offset
crossOffset · 属性 crossOffset crossOffset crossOffset crossOffset crossOffset
arrow · 属性 arrow arrow arrow arrow arrow
width · 属性 width width width width width
compactPresentation · 属性 compactPresentation compactPresentation compactPresentation compactPresentation compactPresentation
inset · 属性 inset inset inset inset inset
initialFocus · 属性 initialFocus initialFocus initialFocus initialFocus initialFocus
headline · 属性 headline headline headline headline headline
label · 属性 label accessibilityLabel contentDescription semanticLabel accessibilityLabel
closable · 属性 closable closable closable showCloseButton closable
closeLabel · 属性 closeLabel closeLabel closeLabel closeButtonSemanticLabel closeLabel
dividerVisibility · 属性 dividerVisibility dividerVisibility dividerVisibility dividerVisibility dividerVisibility
close · 事件 close onClose onDismissRequest onClose onClose
备注 <tanrei-popover anchor="…"> names its anchor as the menu does: anchor is the id of the anchor element in the popover's own tree, and anchorElement a property for an element that an id cannot reach (another shadow root); opener and openerElement name a separate opener, as the docked picker's do (POP-18). The popover sets aria-haspopup="dialog", aria-controls and aria-expanded on the opener and removes them when it is disconnected. Attributes are kebab-case (cross-offset, initial-focus, close-label, compact-presentation); offset and crossOffset are number properties in CSS pixels parsed as the tooltip's are, so an empty attribute is not a number; arrow is true, false or null — absent for null, present for true and ="false" for false; inset is a boolean property that defaults to true, whose attribute is the inverse no-inset. close is a CustomEvent whose detail.reason is escape, back, outside, focus-out, close or anchor-hidden; the product sets open to false from it. Slots: default and actions. The container is a popover="manual" element with role="dialog" and no aria-modal in the shadow root, so it is in the top layer (above an open modal <dialog> when the element is a descendant of that dialog, which it must be to be operable there, POP-25); manual and not auto, because the popover implements its own light dismiss so that close carries a reason and so that an overlay opened from inside it is not closed by the browser's own rule. It is placed with CSS anchor positioning where the browser has it and by measuring otherwise, and follows its anchor with the shared FollowController. The container and the arrow are one painted shape, so the elevation's ring and shadows run round both. Focus order (POP-20): the top layer does not change the tab order, so where the element does not directly follow its opener in the document the popover moves focus itself — Tab on the opener while open goes to the first tabbable element of the popover, Shift+Tab from that element to the opener, and Tab from the last to the element after the opener. The autofocus attribute on a slotted element marks initial focus; the container has tabindex="-1". A press outside also reaches the element under it. Under forced-colors: active the painted shape takes a CanvasText outline (POP-32). The inline attribute, for tests, draws the open container in place. Snapshot harness: a stage of 480 × 480 with a medium outlined tanrei-button labelled "Storage" at its centre and the popover open against it; width-anchor gives the button a width of 240; the screenshot is the stage. dividerVisibility is the divider-visibility attribute (auto, always, scroll); auto reads --tanrei-comp-popover-divider-visibility from the cascade with a style query, and where the browser has none the popover writes the name it read on its clip when it lays out. Compact (POP-39 to POP-42): a media query cannot read a custom property, so the popover reads the resolved comp.popover.compact.window-width through a probe and matches it with matchMedia('(width < …px)'), presenting again at once on change. As a sheet the container is a native <dialog> opened with showModal() in the shadow root, in place of the popover="manual" element: in the top layer over ::backdrop (the scrim), the rest of the page inert and held from scrolling. The slotted content is the same, and the element that had focus is focused again across the change. The keyboard's cover is read from visualViewport. The inline attribute keeps the anchored presentation in every window. Snapshot harness: the compact- fixtures are the real overlay in a 360 × 640 window, and the screenshot is the window. A modifier on the anchor: .tanreiPopover(isPresented: Binding<Bool>, headline:, accessibilityLabel:, placement:, offset: CGFloat? = nil, crossOffset:, arrow: Bool? = nil, width:, inset:, initialFocus:, closable:, closeLabel:, onClose:, actions:, content:); a separate opener is named with .tanreiPopoverOpener(_:) on that view. Dismissal writes false to the binding and then calls onClose with the reason. Drawn in an overlay above the window's content, anchored with anchor preferences, as the tooltip is, not with .popover: the system's is modal at compact widths and its arrow, placement and motion cannot be given by tokens. Initial focus is .defaultFocus or an @AccessibilityFocusState the product passes; the container is an accessibility container with the header trait on the headline, placed after the opener with accessibilitySortPriority; the iOS escape gesture is .accessibilityAction(.escape). A press outside does not reach what is under it. Snapshot harness: the anchor and the open popover in a fixed 480 × 480 frame. Not built on this platform yet: dividerVisibility (auto, always, scroll; POP-38) — the dividers still follow the scroll position, as scroll does. compactPresentation is compactPresentation: TanreiPopoverCompactPresentation = .sheet; in a horizontally compact size class the sheet form is a sheet the popover draws with the bottom sheet's tokens, as SwiftUI's own popover adapts (presentationCompactAdaptation), not .popover. Compose idiom, as the menu: TanreiPopover(visible, onDismissRequest, headline, contentDescription, placement, offset: Dp? = null, crossOffset, arrow: Boolean? = null, width, inset, initialFocus: TanreiPopoverInitialFocus, initialFocusRequester: FocusRequester? = null, closable, closeLabel, actions, content) inside the anchor's Box, shown in a Popup with a PopupPositionProvider of its own. visible rather than calling it only while open, so that the close motion can run. The caller closes it, so close is onDismissRequest and it carries the reason. The Popup is focusable while initialFocus is popover, so back reaches it; because a focusable popup is a window of its own, the popover moves focus itself at its ends (POP-20): Tab from its last element clears the popup's focus, asks the anchor's FocusManager to move next and reports focus-out. paneTitle is the headline or contentDescription. A press outside does not reach what is under it. Snapshot harness: the anchor and TanreiPopoverContent in place in a 480 × 480 dp box; the popup itself is covered by instrumented tests. Not built on this platform yet: dividerVisibility (auto, always, scroll; POP-38) — the dividers still follow the scroll position, as scroll does. compactPresentation is compactPresentation: TanreiPopoverCompactPresentation = Sheet; the sheet form is shown in a Dialog window whose own dim is off, Tanrei drawing the bottom sheet's scrim. Flutter idiom, as TanreiMenuAnchor: TanreiPopover(controller:, placement:, offset:, crossOffset:, arrow:, width:, inset:, initialFocus:, headline:, semanticLabel:, showCloseButton:, closeButtonSemanticLabel:, onClose:, actions: […], content:, builder:) wraps the anchor that builder returns; TanreiPopoverController.open() and .close() are open, and onClose carries the reason. Shown with an OverlayPortal and a CompositedTransformFollower: the portal's child stays where it is declared in the widget, focus and semantics trees, so the popover follows its anchor in traversal and reading order without more (POP-20). Outside presses are a TapRegion whose group the overlays opened from inside it join (POP-24); a press outside also reaches what is under it. autofocus: true on a descendant marks initial focus; Semantics(scopesRoute: false, namesRoute: true, label:) names the container. Snapshot harness: the anchor and the open popover in a 480 × 480 box. Not built on this platform yet: dividerVisibility (auto, always, scroll; POP-38) — the dividers still follow the scroll position, as scroll does. compactPresentation is compactPresentation: TanreiPopoverCompactPresentation.sheet; the sheet form is a modal route drawn with the bottom sheet's tokens. <Popover visible anchorRef openerRef? onClose headline …>{content}</Popover>, presented on the overlay layer that TanreiProvider renders (not a Modal, which would make the rest of the screen unreachable) and placed by measuring the anchor (measureInWindow), again on layout, scroll and keyboard events; Android's back button (BackHandler) is close with the reason back, and the iOS escape gesture is onAccessibilityEscape. Controlled: the caller sets visible to false from onClose. accessibilityViewIsModal stays false; the container is given accessibility focus when it opens and the opener when it closes. A hardware keyboard's Tab order follows the native view order, which the overlay layer puts last: the README states that the popover is reached from its opener by the focus it takes on opening, and returns to the opener on leaving. A press outside closes the popover and does not reach what is under it. Follows the app's direction where an ancestor's is not known. Not built on this platform yet: dividerVisibility (auto, always, scroll; POP-38) — the dividers still follow the scroll position, as scroll does. compactPresentation is a string prop ('sheet' | 'own'); the sheet form is presented in a Modal whose own dim and transition are off, with accessibilityViewIsModal true.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-popover-actions-arrangement — 0
--tanrei-comp-popover-actions-gap sys.space.space200 16px
--tanrei-comp-popover-actions-margin-block-start sys.space.space200 16px
--tanrei-comp-popover-actions-variant — 1
--tanrei-comp-popover-arrow-height sys.space.space75 6px
--tanrei-comp-popover-arrow-inset sys.space.space100 8px
--tanrei-comp-popover-arrow-visible — 0
--tanrei-comp-popover-arrow-width sys.space.space150 12px
--tanrei-comp-popover-close-button-variant — 1
--tanrei-comp-popover-compact-window-width sys.breakpoint.medium 600px
--tanrei-comp-popover-container-color sys.color.surface-bright #ffffff
--tanrei-comp-popover-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-popover-container-gap sys.space.space150 12px
--tanrei-comp-popover-container-max-height — 480px
--tanrei-comp-popover-container-max-width — 400px
--tanrei-comp-popover-container-min-width — 160px
--tanrei-comp-popover-container-padding-block sys.space.space200 16px
--tanrei-comp-popover-container-padding-inline sys.space.space200 16px
--tanrei-comp-popover-container-shape sys.shape.corner.medium 8px
--tanrei-comp-popover-content-color sys.color.on-surface #1b1b1b
--tanrei-comp-popover-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-popover-divider-visibility sys.behavior.divider-visibility 0
--tanrei-comp-popover-divider-width sys.border.hairline 1px
--tanrei-comp-popover-header-gap sys.space.space100 8px
--tanrei-comp-popover-headline-color sys.color.on-surface #1b1b1b
--tanrei-comp-popover-motion-close-duration sys.motion.role.appear.anchored.exit 250ms
--tanrei-comp-popover-motion-close-easing sys.motion.easing.exit cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-popover-motion-open-duration sys.motion.role.appear.anchored.enter 300ms
--tanrei-comp-popover-motion-open-easing sys.motion.easing.enter cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-popover-offset sys.space.space50 4px
--tanrei-comp-popover-window-margin sys.space.space100 8px
--tanrei-comp-popover-headline-typescale sys.typescale.heading-6 500 14px/1.4286

测试样例

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

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

Web 上绘制方式的不同之处

  • 弹出框是顶层(top layer)中的 popover="manual" 元素。位于模态 <dialog> 之内时,它必须是这个对话框的后代,才能在那里操作。
  • 在 compact 窗口中,面板是用 showModal() 打开的原生 <dialog>,遮罩就是它的 ::backdrop。
  • 在外部按下会关闭弹出框,这次按下也会传到它下方的元素。
  • Web 没有自己的返回手势,所以 close 绝不会带有原因 back。
  • anchorElement 和 openerElement 是属性(property),用于 id 够不着的元素,例如位于另一个 shadow root 里的元素。
  • arrow 可以是未设置、true 或 false:这个属性(attribute)可以不写、写成 arrow,或写成 arrow="false"。
  • 用 autofocus 标记打开时取得焦点的元素。
  • 在强制颜色模式下,容器和它的箭头带有一道文本颜色的边线。

指南

工具提示、弹出框、菜单还是对话框

方面 工具提示 弹出框 菜单 对话框
怎样打开 悬停、键盘聚焦、长按 按下打开它的控件 按下它的锚点 由产品打开
里面是什么 对锚点的描述 供用户操作的内容 只有菜单项 一项决定
是否取得焦点 从不 是,并且交还 是,并且交还 是,并且留住
是否阻挡页面 否 否(在 compact 窗口中作为面板时为是) 否 是
  1. 如果它只是说明控件是什么,里面没有任何可以按下的东西,它就是工具提示。
  2. 如果它是一列命令或选项,它就是菜单。
  3. 如果用户不作出回应就无法继续,它就是对话框。
  4. 否则它就是弹出框。

推荐

  • 通过按下某个控件来打开它,再次按下这个控件时关闭它。
  • 内容要短。只做一件事,最多几个输入框。
  • 如果弹出框大到会盖住用户原本要在外部按下的地方,就加上 closable;在没有 Escape 的触摸设备上也要加。
  • 位于用户正在输入的输入框下方的表面,使用 width="anchor" 和 initialFocus="anchor"。
  • 只有在 compact 窗口中也必须留在所属内容旁边的弹出框(例如紧挨着它所解释的词的注释),才把 compactPresentation 设为 own。
  • 操作的标签要短。比弹出框还宽的标签不会换行。

避免

  • 不要在悬停或聚焦时打开弹出框。触摸无法这样打开它;而仅仅因为指针经过就取得焦点的表面,是一个陷阱。
  • 不要把在外部误按一下就会丢失的内容放进弹出框。长表单应该放在对话框或面板里。
  • 不要在弹出框里嵌套一层以上的弹出框。每多一层,就要多按一次 Escape。
  • 用户必须先作出回应才能继续的内容,不要用弹出框。那是对话框。

无障碍

角色
dialog
标注
The popover is a dialog that is not modal — a modal one while it is a sheet in a compact window (POP-40) — named by its headline, or by label where it draws none. The headline is also exposed as a heading. The opener keeps its own name and role and exposes that it opens a dialog and whether it is open. The close button is named by closeLabel and each action by its own label; confirming and dismissive are not announced, and the actions are read in the order they are placed (BGR-15). The arrow and the dividers are decorative. The popover is not its anchor's description.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Enter / Space on the opener The product opens the popover; focus moves into it (to the element the product marked, otherwise to the container). Pressed again while it is open, the product closes it.
Tab / Shift+Tab Move through the popover's close button, its content and its actions; the popover comes directly after its opener in the order. Shift+Tab from its first element returns to the opener and leaves it open; Tab from its last element moves on to what follows the opener and closes it. Focus is never kept in. As a sheet in a compact window (POP-40) it keeps focus in: Tab and Shift+Tab wrap.
Escape Closes the popover and returns focus to its opener; with overlays opened from inside it, only the topmost. It goes no further: a dialog the popover stands in stays open. As a sheet, a press on the scrim closes it too.
Enter / Space Activate the focused close button (close, reason close) or action (its click; the popover does not close by itself, POP-17). The popover adds no default action to Enter.

备注

  • The popover has no target of its own; its close button and its actions are Tanrei Buttons, which meet the minimum target themselves (BTN-5). comp.popover.actions.gap keeps each action's whole target in a column in the default system. The opener is the product's control and keeps its own target.
  • Tooltip, popover, menu, dialog — which one. A tooltip describes its anchor, opens on hover and focus, never takes focus and holds nothing a person can operate (TIP-13). A popover is opened by a press, takes focus and holds content a person operates. A menu is a list of commands or choices, moved through with the arrow keys; it holds only its items. A dialog stops the task until it is answered. A note that needs a link or a button is a popover, never a tooltip.
  • Never open a popover on hover or on focus (POP-18): what appears that way must be dismissible, hoverable and persistent (WCAG 1.4.13) and cannot be reached by touch, and a surface that takes focus because the pointer passed is a trap. Open it from a press.
  • Focus: the popover is an operated overlay that is not modal. It moves focus in when it opens and returns it to the opener when it closes, but does not keep it: Tab leaves it, which closes it. An open surface nobody is in would cover content the keyboard user has moved on to, and a pointer user closes it by pressing elsewhere; focus leaving is the keyboard's form of the same thing. This is the menu's rule, the docked picker's and the docked search view's.
  • Initial focus goes to the container unless the product marks an element (POP-19), as for the dialog: assistive technology reads the popover's name first, and a field that takes focus by itself would raise the on-screen keyboard over a popover the person has not read. Mark the first field of a short form.
  • initialFocus anchor is for a surface under a field the person is typing in (suggestions, a format hint with a link). Focus stays in the field, so the field's own keys keep working; the popover is still next in the Tab order, and still closes when focus leaves both.
  • Reading order: the popover follows its opener for a screen reader as it does for Tab (POP-20). On the web, place the element directly after its anchor in the document where the layout allows; where it cannot be, the opener's aria-controls and the focus that moves in carry a person there.
  • A press outside closes the popover. Do not put in a popover what a person would lose by one stray press: a long form belongs in a dialog or a sheet. Where open is a plain value the product may also answer close with the reason outside by leaving the popover open; do that only with a visible way to close it (closable, or a dismissive action).
  • Keep nesting shallow: an overlay opened from a popover works at any depth (POP-24), but each level is one more thing Escape must be pressed for. One popover inside a popover is the most a product should need; a debug build reports a third.
  • Not modal means the rest of the page can still be read and scrolled, and a screen reader's cursor can leave the popover without closing it (POP-21). Nothing a person must answer before going on belongs in a popover.
  • Text size and small windows: at 200% text the popover wraps, grows to its limits and scrolls. In a compact window it is a sheet across the window's width (POP-39); one that keeps its own presentation is, at a window 320 wide, the window's width less the margins (POP-43). It never needs scrolling in two directions.
  • A compact window (POP-39 to POP-42): there a popover takes the bottom sheet's appearance, as every popover and every dialog of Tanrei does, and becomes a modal dialog — a scrim, everything behind it inert, focus kept in, Escape, back or a press on the scrim to close it. Modal because the sheet form is what the platforms make of a popover on a phone, and they make it modal: iOS adapts a popover to a sheet in a compact size class (UIPopoverPresentationController's adaptive sheet; SwiftUI's presentationCompactAdaptation), and Apple's guidelines present a popover's content in a compact view as a modal sheet; React Spectrum presents a popover as a modal on mobile; Material's bottom sheet that stands in for a menu or a dialog on mobile is a modal one. A surface over a scrim that left the page operable would show one thing and do another (WAI-ARIA APG, dialog: a modal dialog makes what is behind it inert). The opener still says it opens a dialog. Set compactPresentation to own for a popover that must stay beside what it belongs to; initialFocus anchor always keeps it there, because the field being typed in must stay usable.
  • Forced colors (POP-32): the container gains an edge in the text color, because its shadow is removed. Contrast in the default system: on-surface on surface-bright is held to 4.5:1 by the token invariants; the container is told from the page by its elevation's ring and shadow, which are not held to a contrast of their own, as for the menu.

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

组件负责的部分

  • 容器是一个非模态的对话框,以 headline 或 label 命名。它之外的任何东西都不会被隐藏,也不会变为惰性(inert)。
  • 在 compact 窗口中以面板形式显示时,容器是一个模态对话框:它之外的一切都变为惰性,Tab 和 Shift+Tab 在它里面循环。关闭时焦点回到打开它的控件。
  • 打开它的控件会暴露两点:它会打开一个对话框,以及这个对话框当前是否打开。控件自己的名称和角色不变。
  • 打开时,焦点移到你标记的元素上,否则移到容器,这样弹出框的名称会最先被读到。只有从键盘打开弹出框时,容器上才显示焦点指示。
  • 无论弹出框在文档中的什么位置,它在 Tab 顺序中都紧跟在打开它的控件之后。在它的最后一个元素上按 Tab,焦点会移到这个控件之后的内容上,弹出框随之关闭。焦点从不被留在里面。
  • Escape 只关闭最上面的浮层,并且不再向外传递:包含弹出框的对话框保持打开。
  • 关闭时,如果焦点原本在弹出框之内,就把焦点交还给打开它的控件。如果用户已经把焦点移到了别处,焦点就留在那里。
  • 从弹出框内部打开的浮层,例如菜单或工具提示,属于这个弹出框:在其中按下或聚焦不会关闭弹出框。
  • 文本放大到 200% 时,弹出框里的文本换行,弹出框增大到上限,然后滚动。在 compact 窗口中,它是横跨窗口宽度的面板。保持自身形态的弹出框,在宽 320 的窗口里,宽度是窗口宽度减去边距。

需要你做的事

  • 给弹出框一个 headline 或 label;设置了 closable 时,再给一个 closeLabel。
  • 在打开它的控件被按下时设置 open,并在 close 时把它设为 false。
  • 在 Web 上,请尽量把弹出框元素放在文档中紧跟在打开它的控件之后的位置,让屏幕阅读器的阅读顺序与 Tab 顺序一致。
  • 简短表单的第一个输入框,请标记为初始焦点。只有文本的弹出框不要加标记。