提示块(Callout)
属于内容的一部分、并一直留在那里的通知——一条说明、一条注意事项、某件事的原因,或者对出错之处的汇总。外观是柔和填充或轮廓,采用语义状态的颜色和字形,可带标题、操作和关闭按钮。
- 类别
- 信息传达
- 令牌
comp.callout- 层级
- 基础
- Web
tanrei-calloutschedule - SwiftUI
TanreiCalloutradio_button_unchecked - Jetpack Compose
TanreiCalloutradio_button_unchecked - Flutter
TanreiCalloutradio_button_unchecked - React Native
Calloutradio_button_unchecked
生成自 docs/status/status.json, specs/callout.spec.json
一条说明、一条带标题的注意事项,以及一条带操作和关闭按钮的通知
提示块是页面流中的一条通知:你把它放在哪里,它就待在哪里,宽度与所在位置相同,内容在多久它就在多久。它默认是一条说明,像它前面的段落一样在原地朗读,从不获得焦点。
在 Web 上它是 <tanrei-callout>。其他平台尚未实现。
正文就是元素自己的内容。标题用 headline 属性给出;如果标题是你自己的内容或者是一个标题元素,就放进 title 插槽。图标集里的字形放进 icon 插槽,你自己的按钮或链接放进 actions 插槽。
<!-- 一条说明。它的语义状态已经由标题的文字说出。 -->
<tanrei-callout status="info">
<p slot="title">说明</p>
文本样式按级别命名,从不按字号命名。
</tanrei-callout>
<!-- 标题没有说出语义状态:status-label 会先朗读(“警告,……”),但不绘制。 -->
<tanrei-callout status="warning" status-label="警告" headline="无障碍">
预设对照的是它的原型,而不是这些对比度阈值。
</tanrei-callout>
<!-- 一条小贴士:没有语义状态,用你自己的字形,把这个词写在标题里。 -->
<tanrei-callout headline="小贴士">
<span slot="icon">lightbulb</span>
让操作保持可用,在用户使用它时再说明还缺什么。
</tanrei-callout>
<!-- 长篇内容用长文排版,尺寸与提示块一致,不限制行长。 -->
<tanrei-callout size="large" status="warning">
<h3 slot="title">迁移前请先备份</h3>
<div class="tanrei-prose" data-size="large" data-measure="false">
<p>迁移会就地改写数据库。<a href="#restore">查看如何从副本恢复</a>。</p>
<ul><li>…</li></ul>
</div>
</tanrei-callout>
<script type="module">
import '@tanrei/web/components/callout';
import '@tanrei/web/callout.css'; // 服务端渲染的提示块在元素定义之前保持的框
</script>import { Button, Callout } from '@tanrei/react';
<Callout status="warning" headline="磁盘上的文件已更改" live open={conflict} closable closeLabel="关闭" onDismiss={() => setConflict(false)}>
你在这里的修改不在那个文件里。在你做出选择之前,不会覆盖任何内容。
<Button slot="actions" variant="outlined" onClick={reload}>从磁盘重新加载</Button>
<Button slot="actions" variant="text" onClick={keep}>保留我的修改</Button>
</Callout>两种外观在每种语义状态下的样子
标题、你自己的字形、操作、关闭按钮,以及 large 尺寸
构成
构成
tonal 之上。| 部件 | 说明 |
|---|---|
1 container | The block that holds the notice, as wide as its place in the layout and as tall as its content, flat on the surface it sits on. Filled in the tonal look (a quiet neutral, or a status's quiet container) and edged in the outlined look. It carries no state: nothing about it answers a pointer, a press or focus. |
2 outline · 可选 | The outlined look's edge: one hairline inside the container, the same in every status. |
3 icon · 可选 | The glyph at the start, on the first line of the text: the status's glyph from the system's icon set (info, warning, error or check_circle, the ones a snackbar draws), or the glyph the product names. In the status's own color, a little softer than the text. Decorative: the status is given in words. |
4 title · 可选 | A few words that name the notice, above the body. Plain text unless the product gives a heading; it wraps and is never cut. |
5 body | The notice itself: the product's content, from one sentence to several paragraphs with lists, links, code and tables. Long-form blocks in it are prose. |
6 actions · 可选 | One or two of the product's own Buttons or links that follow up on the notice, in a row below the text. |
7 close-button · 可选 | An icon-only Button of the small size showing the close glyph at the end of the first row, in the variant comp.callout.close-button.variant names (tonal in the default system) and, on a filled look, in the tone of that fill: its content color, so that its container is that color made translucent and its glyph is that color. The callout places it and chooses its variant and its tone; everything else it reads from Button's own tokens. The only part of a callout that answers a pointer, a press or focus, and it answers as a Button. |
尺寸
尺寸
| 长度 | 令牌 | 值(px) |
|---|---|---|
container padding-inline | tanrei.comp.callout.container.padding-inline | 16 |
tonal 之上。属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | string | "" | A few words above the body that name the notice ("Not published yet"). Plain text, never a heading. Empty draws the body alone. The title slot takes its place where the product needs a heading of its own level or inline content. |
status | default | info | warning | error | success | "default" | What the notice is, in the names the snackbar and the tag use. default: a neutral aside, with no glyph unless the product gives one. info, warning, error and success draw the look in that status's color roles with that status's glyph; a live callout is announced assertively for warning and error. Use them for what they mean everywhere in the system: advice is not a success. |
statusLabel | string | "" | The status in words, in the product's language ("Warning"), read by assistive technology before the title and the body and not drawn, so that the status never rests on color and glyph alone. For a callout whose title does not itself say the status; needed when it has a status and no title. Ignored while status is default. |
variant | tonal | outlined | "tonal" | How much the callout weighs. tonal: a quiet fill, the default. outlined: no fill and one hairline, for a callout on a tinted surface or inside a filled card, where a fill would vanish, and for many callouts close together. A design system may add values. |
size | medium | large | "medium" | The size of the text, in prose's two sizes. medium sets the body in the interface's own text style, for a callout among controls: a form, a panel, a dialog. large sets it in the style for long reading, for a callout in a page whose purpose is the text. Paddings, the glyph and the close button are the same at both. A design system may add values. |
live | boolean | false | Whether the callout is announced. False, the default, is for a callout that is part of what was written: it is a note, read where it stands. True is for one that appears or changes in answer to something (a summary of a form's errors after a submit, a notice that the file changed underneath the person): it is a live region, announced once when it is shown and when its content changes, politely or, for warning and error, assertively. It changes nothing that is drawn. |
closable | boolean | false | Draws a close button, for a notice the person may put away (an announcement, a hint they have read). A caution that qualifies the content beside it should not be closable. |
closeLabel | string | "" | The close button's accessible name, in the product's language ("Close"). Required while closable is true. |
open | boolean | true | Whether the callout is shown. The close button sets it to false; the product sets it to show a callout again or to show one in answer to something. A callout that is not open draws nothing and takes no space. |
插槽
| 插槽 | 说明 |
|---|---|
default | The body: text, or the product's own blocks (paragraphs, lists, a table, a code block, any component). Long-form blocks are laid out as prose of the callout's size. |
icon | A glyph of the system's icon set, by name, drawn in place of the status glyph, or as the glyph of a callout without a status (a tip's lightbulb). |
title | The product's own title content in place of the title prop: text with inline content, or a heading of the level the page's outline needs. The callout draws it in the title's style and changes neither its role nor its level. |
actions | The actions: the product's own Buttons or links, one or two. The callout lays them out below the text and does nothing else to them. |
事件
| 事件 | 说明 |
|---|---|
dismiss | The person closed the callout with its close button. Emitted exactly once, after open became false; never emitted when the product sets open. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
CLO-1 | THE callout SHALL draw a container as wide as the layout it is placed in gives it (CLO-22) and as tall as its content, with corners comp.callout.container.shape eased by sys.shape.corner-smoothing and no elevation, padded by comp.callout.container.padding-block above its first line and below its last block, by comp.callout.container.padding-inline at the start, and at the end by comp.callout.container.padding-inline where the text meets it or by comp.callout.container.padding-inline-end-with-close after the close button. Inside it SHALL lay out, from the start: the glyph, where it has one (CLO-5, CLO-6); comp.callout.icon.gap after it a column that takes the remaining width and holds the title (CLO-7), the body (CLO-8) and the actions (CLO-9), in that order; and comp.callout.container.gap after that column the close button, where it has one (CLO-10). THE callout SHALL centre the glyph and the close button on the first line of the text (the title's, or the body's without a title) and keep them there however tall the text grows, and any height the container has beyond its content SHALL lie below the content. |
CLO-2 | WHILE status is default THE callout SHALL fill its container with the container color of its variant and draw its title, its body and a glyph the product gave in that variant's title, body and icon colors: comp.callout.container.color.tonal, comp.callout.title.color.tonal, comp.callout.body.color.tonal and comp.callout.icon.color.tonal for tonal, and outlined's tokens of the same names (comp.callout.container.color.outlined, comp.callout.body.color.outlined, and so on). |
CLO-3 | WHILE status is info, warning, error or success THE callout SHALL draw its container, its title, its body and its glyph from the parts of its variant prefixed with the status: for a tonal info callout comp.callout.info.container.color.tonal, comp.callout.info.title.color.tonal, comp.callout.info.body.color.tonal and comp.callout.info.icon.color.tonal; for outlined, and for warning, error and success, their own tokens of the same names (comp.callout.error.icon.color.outlined, comp.callout.success.container.color.tonal, and so on). A status SHALL change these colors, the glyph (CLO-5), the tone of the close button (CLO-10) and how a live callout is announced (CLO-21), and nothing else: the callout's size, shape, layout and edge stay those of its variant and size. |
CLO-4 | WHILE variant is outlined THE callout SHALL draw an edge comp.callout.outline.width wide in comp.callout.outline.color inside its container's size, following its corners, in every status, without changing the callout's size or the place of its content; WHILE variant is tonal it SHALL draw no edge. A status SHALL NOT color the edge: it is the system's one hairline. |
CLO-5 | WHILE status is info, warning, error or success and the icon slot is empty THE callout SHALL draw that status's glyph from the system's icon set (info for info, warning for warning, error for error, check_circle for success), the glyphs a snackbar draws, in a box of comp.callout.icon.size, in its form's icon color (CLO-3) drawn at comp.callout.icon.opacity (an opacity of the glyph alone, which fades nothing else), with sys.icon.family.unselected and sys.icon.fill.unselected, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max. The glyph so drawn SHALL keep 3:1 against its container in every scheme of the default system, and against every surface role in the outlined look. THE callout SHALL keep the glyph from assistive technology, which is given the status in words (CLO-20). WHILE status is default and the icon slot is empty THE callout SHALL draw no glyph and no room for one: the text starts at the start padding. |
CLO-6 | WHERE the icon slot holds a glyph THE callout SHALL draw it in the place, the box, the color and the opacity CLO-5 gives the status glyph (the icon color of its form, CLO-2 and CLO-3), in every status and without one, SHALL draw no status glyph beside it, and SHALL keep it from assistive technology: the product's glyph says what kind of notice this is (advice, an experiment, something new), and the words say it to everyone. |
CLO-7 | WHILE title is not empty THE callout SHALL draw it above the body in the text style of its size (comp.callout.title.typescale.medium for medium, comp.callout.title.typescale.large for large) and the title color of its form (CLO-2, CLO-3), with comp.callout.title.gap between it and the body; the title SHALL wrap and SHALL never be cut. WHERE the title slot holds content THE callout SHALL draw that content in the title's place, style and color instead of the title prop, and SHALL change neither its role nor its level: a title given as text is plain text and never a heading, and a title the product gives as a heading stays a heading of the level the product gave it, drawn as every other title is. WHILE both are empty THE callout SHALL draw the body alone, on the first line. THE callout 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. |
CLO-8 | THE callout SHALL draw the content of its default slot as its body, below the title, and SHALL give it the text style of its size (comp.callout.body.typescale.medium for medium, comp.callout.body.typescale.large for large) and the body color of its form (CLO-2, CLO-3) as the style and color its text takes where nothing inside sets another. The body SHALL wrap and SHALL never be cut or limited to a number of lines. WHERE the body holds long-form blocks (paragraphs, lists, a quote, a table, a code block) they SHALL be laid out as prose of the callout's size (PRS-1, PRS-5), without a measure of their own, so that the first block has no space before it and the last none after it and the body is exactly as tall as its content; the callout SHALL add no style to anything inside a component placed in its body, and a link in the body SHALL be underlined at rest (PRS-17). |
CLO-9 | WHERE the actions slot holds content THE callout SHALL lay it out as a row below the text (the body, or the title where there is no body), comp.callout.actions.gap below it, starting at the text's start edge, with comp.callout.actions.item-gap between two actions along the row and between rows where they wrap; an action's label SHALL NOT be cut to make the row fit. The actions are the product's own Buttons and links: THE callout SHALL NOT restyle them, SHALL NOT answer their presses and SHALL NOT close when one is activated. They SHALL follow the body in reading order and in the focus order, before the close button. |
CLO-10 | WHILE closable is true THE callout SHALL draw at the end of its first row 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.callout.close-button.variant names and, in every look but outlined, in the tone of its form (BTN-45): the body color of that form (comp.callout.body.color.tonal without a status, comp.callout.info.body.color.tonal for info, and so on), the content color of the fill it stands on, so that its container is that color made translucent and its glyph is that color; both 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). The outlined look has no fill, so its close button takes no tone and is drawn as the variant draws it on the surface the callout sits on. THE callout's container, its text and its glyph SHALL NOT change under a pointer, a press or focus: the callout itself has no states. WHILE closable is false THE callout SHALL draw no close button and keep no room for one. |
CLO-11 | WHEN the user activates the close button by pointer, touch, Enter or Space THE callout SHALL set open to false, emit dismiss exactly once and close (CLO-13). THE callout SHALL NOT close by itself after a time, and SHALL NOT answer Escape: it is part of the content, and Escape belongs to whatever overlay or field the person is in. |
CLO-12 | WHEN a callout closes while focus is inside it THE callout SHALL move focus to the element that had it before focus entered the callout or, where that element no longer exists or cannot take focus, leave focus to the platform's default, so that the next Tab reaches what followed the callout; WHEN it closes while focus is elsewhere it SHALL NOT move focus. |
CLO-13 | WHILE open is false THE callout SHALL draw nothing, take no space and expose nothing to assistive technology, and its content SHALL NOT be reachable by focus; WHEN open changes THE callout SHALL appear or disappear at once, without a motion of its own, and SHALL emit dismiss only for a close the person made (CLO-11), never when the product sets open. |
CLO-19 | THE callout 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); a platform that cannot draw it SHALL fall back to a quarter circle and state that in its README. Its close button's corners and its focus indicator are the Button's (BTN-10). |
CLO-20 | WHILE live is false THE callout SHALL be exposed to assistive technology as a note: a part of the content that qualifies what is around it, read where it stands in reading order, named by its title where it has one and otherwise by statusLabel where status is not default and the product gave one. WHILE status is not default and statusLabel is not empty THE callout SHALL expose statusLabel as text before the title and the body, set apart so that it is read as a phrase of its own ("Warning, …"), and SHALL NOT draw it; WHILE status is default it SHALL ignore statusLabel. A callout that is not live SHALL NOT be announced when it is shown or when its content changes, SHALL NOT move focus, and SHALL NOT be a stop in the focus order: static content is not an alert. |
CLO-21 | WHILE live is true THE callout SHALL be a live region in place of a note. WHEN it is shown after its page or view was first presented (open becomes true, or the product adds it), and WHEN its title, its body or its status changes while it is shown, THE callout SHALL have assistive technology announce it once: statusLabel first when status is not default, then the title when it has one, then the whole body. It SHALL announce politely (after what is being read) when status is default, info or success, and assertively (interrupting what is being read) when status is warning or error, as a snackbar does, and SHALL NOT move keyboard or screen-reader focus. A live callout that is drawn with the first presentation of its page or view SHALL NOT announce itself then. live SHALL change nothing that is drawn. |
CLO-22 | THE callout SHALL lay itself out as a block in the flow of its container: it SHALL take the whole inline size the container gives it, in a page, in a list item, in a tab's panel, in a card or in another callout's body alike, with its start edge on the start edge of the text around it; it SHALL bring no outer space of its own (PRS-15: the space between it and its neighbours is its container's), SHALL never be wider than its container, SHALL have no minimum width, no maximum width and no measure of its own, and SHALL NOT scroll: content that cannot shrink (a table, a line of code) scrolls inside its own box. THE callout SHALL NOT float over the content, stick to an edge while the content scrolls or raise itself above it: where it stands is the product's layout. |
CLO-23 | WHILE the text size is enlarged THE callout SHALL grow its title and its body with it, keep its paddings and gaps, wrap, and grow in height; it SHALL keep the glyph's box at comp.callout.icon.size and the close button at its own size (a Button's icon-only circle, BTN-12), both still centred on the first line (CLO-1), SHALL cut nothing and set no fixed height, SHALL lose no content when the reader raises line height, paragraph spacing, letter spacing or word spacing, and SHALL need no horizontal scrolling at a width of 320 points with text at 200 %. |
CLO-24 | 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 callout SHALL draw an edge comp.callout.outline.width wide in the platform's text color inside its container in every variant and status, its title, body and glyph in that text color with the glyph whole (no opacity), links in the platform's link color and still underlined, and its close button as a Button draws itself there, so that a look whose fill the mode removes is still a bounded block; the status SHALL then rest on the glyph's shape, the title's words and statusLabel. |
CLO-25 | WHERE the layout direction is right-to-left THE callout SHALL mirror the order of its content and apply its paddings by logical start and end: the glyph at the right, the text after it, the actions starting at the text's right edge and the close button at the left. |
CLO-26 | THE callout 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 callout is shown THE callout SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus. |
CLO-27 | WHERE the layout direction is right-to-left THE callout SHALL draw the glyph of its icon slot mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and opacity nor the order CLO-25 gives. THE callout SHALL NOT mirror a glyph itself, SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name, and its own glyphs (the four status glyphs) are drawn through the same helper, which mirrors none of them under the default icon set; the close button's glyph is the Button's (BTN-19). |
CLO-28 | THE callout SHALL lay out a title given as text 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 name, a number with its unit and a version keep their own order in a right-to-left layout and reorder nothing beside them; THE callout SHALL still place the title and align its lines by the layout direction, not by the text's own. The body's blocks are laid out as prose lays out a block (PRS-25): each by its own first strong character unless the content states a direction, with its lines aligned to the start edge of the layout. |
CLO-29 | WHILE the system's icon set cannot yet draw a glyph the callout draws itself (a status's glyph, or the icon slot's; its font is still loading) THE callout SHALL draw nothing in that glyph's place, never the glyph's name as text, SHALL keep the glyph's box, and SHALL draw the glyph once the set can; statusLabel and closeLabel SHALL still reach assistive technology. The close button's glyph is the Button's to draw. |
CLO-30 | WHEN variant or size names a value that the active design system does not have, or status names a value this spec does not have, THE callout SHALL render with the default value (the default variant's or size's tokens, status default), and in debug builds report the unknown value. |
CLO-31 | IF the title, the title slot and the body are all empty THE callout SHALL draw nothing, take no space and expose nothing, and in debug builds report that a callout has no content: a glyph, actions or a close button alone are not a notice. WHILE status is not default and both the title and statusLabel are empty THE callout SHALL still render and in debug builds report that the status is not given in words; WHILE closable is true and closeLabel is empty it SHALL still render the close button and in debug builds report that the button has no accessible name; IF statusLabel is not empty while status is default THE callout SHALL ignore it and in debug builds report it. |
CLO-32 | WHERE the platform has ARIA attributes (the web), the host carries its note or live region role (CLO-20, CLO-21) itself, so what the product writes on it is not moved, but for a name: WHILE its name (its title, else statusLabel) is not empty THE callout SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that its name (its title, else statusLabel) is its whole name although the platform gives aria-labelledby the precedence, and WHEN its name (its title, else statusLabel) is emptied SHALL give them back as they were written; WHILE its name (its title, else statusLabel) is empty a name the product gives so — aria-label, or aria-labelledby naming an element — SHALL be its name. WHILE it is closed or empty (CLO-13) it exposes nothing, and SHALL hold them as well, giving them back when it is shown. WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaLabelledByElements) to null, THE callout SHALL take it away. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-callout | TanreiCallout | TanreiCallout | TanreiCallout | Callout |
title · 属性 | headline | title | title | title | title |
status · 属性 | status | status | status | status | status |
statusLabel · 属性 | statusLabel | statusLabel | statusDescription | statusSemanticLabel | statusAccessibilityLabel |
variant · 属性 | variant | variant | variant | variant | variant |
size · 属性 | size | size | size | size | size |
live · 属性 | live | isLive | live | live | live |
closable · 属性 | closable | isClosable | closable | closable | closable |
closeLabel · 属性 | closeLabel | closeLabel | closeDescription | closeSemanticLabel | closeAccessibilityLabel |
open · 属性 | open | isPresented | visible | visible | open |
dismiss · 事件 | dismiss | onDismiss | onDismiss | onDismiss | onDismiss |
| 备注 | <tanrei-callout> with the body as light-DOM children, the glyph in the icon slot, the title in the title slot (<p slot="title">, or <h3 slot="title"> where it is a heading) or the headline attribute (HTML's global title attribute would show a tooltip over the whole block, as the snackbar's and the app bar's), and Buttons or links in the actions slot. statusLabel and closeLabel are the status-label and close-label attributes. open is a boolean property that defaults to true; the open attribute spells it, and open="false" is how markup starts a callout closed. The host is display: block with no margin, role="note" and aria-labelledby naming the title, set through ElementInternals where axe can see it and as attributes otherwise; with live the host's role is status or alert by status, and the announcement is made through a visually hidden live region in the shadow root that exists from the element's first render, so that a callout opened later is announced (CLO-21). statusLabel is drawn as visually hidden text before the title in the shadow root, followed by a comma and a space; the glyph is aria-hidden. The body takes the form's content color and the size's text style through inherited color and font, so text placed directly in the element is styled, and a body that is rendered Markdown is wrapped by the product in <div class="tanrei-prose" data-measure="false"> with the callout's data-size (prose re-enters inside a Tanrei element). The glyph's opacity is applied to its color (a mix with transparent), never as an element's opacity. The close button is a <tanrei-button size="small"> with the close glyph in its icon slot and close-label as its label, in the shadow root (part close-button); the callout gives it the variant --tanrei-comp-callout-close-button-variant names and, in a look with a fill, the tone of its form (the form's body-color custom property) through the Button's internal channel, after each update (CLO-10). Under forced-colors: active the container takes a CanvasText border of the outline width (CLO-24). The stylesheet of @tanrei/web gives tanrei-callout:not(:defined) display: block, so the content keeps its place before the element is defined. dismiss is a CustomEvent. | TanreiCallout(_ title: String? = nil, status:, statusLabel:, variant:, size:, isLive:, isClosable:, closeLabel:, isPresented:, icon:, onDismiss:) { body } actions: { … }, the body and the actions as @ViewBuilders, the glyph by name through icon:, and a second initializer whose title is a @ViewBuilder (the title slot; .accessibilityAddTraits(.isHeader) is the product's). isPresented is a Binding<Bool> that defaults to a constant true. The container is an accessibility container labelled by the title or statusLabel; a live callout posts AccessibilityNotification.Announcement at default priority, or high for warning and error. | TanreiCallout(title, status, statusDescription, variant, size, live, closable, closeDescription, visible, icon, onDismiss, titleContent, actions) { body }, the body, titleContent and actions as composable lambdas, the glyph by name through icon. visible is hoisted state: the close button calls onDismiss and the product sets it. The container merges no descendants and carries paneTitle-free semantics: a content description of the status description on a leading invisible node, and for a live callout liveRegion polite or assertive by status. | TanreiCallout(title: …, status:, statusSemanticLabel:, variant:, size:, live:, closable:, closeSemanticLabel:, visible:, icon:, titleWidget:, actions:, onDismiss:, child:), the body as child, the title slot as titleWidget, the actions as a list of widgets, the glyph by name through icon. visible is the product's state; the close button calls onDismiss. A Semantics(container: true, label:) over the block, with liveRegion: true for a live callout and SemanticsService.announce with its assertiveness for warning and error. | <Callout title="…" status="warning">…</Callout>; the body is children, the title slot is the titleContent prop, the actions are the actions prop (elements), and the glyph is the icon prop (a glyph name). open is controlled when given, with onDismiss. A live callout sets accessibilityLiveRegion (polite or assertive) on Android and calls AccessibilityInfo.announceForAccessibility on iOS. Follows the app's direction where an ancestor's is not known. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-callout-actions-gap | sys.space.space150 | 12px |
--tanrei-comp-callout-actions-item-gap | sys.space.space100 | 8px |
--tanrei-comp-callout-body-color-outlined | sys.color.on-surface | |
--tanrei-comp-callout-body-color-tonal | sys.color.on-secondary-container | |
--tanrei-comp-callout-close-button-variant | — | 1 |
--tanrei-comp-callout-container-color-outlined | — | |
--tanrei-comp-callout-container-color-tonal | sys.color.secondary-container | |
--tanrei-comp-callout-container-gap | sys.space.space100 | 8px |
--tanrei-comp-callout-container-padding-block | sys.space.space150 | 12px |
--tanrei-comp-callout-container-padding-inline | sys.space.space200 | 16px |
--tanrei-comp-callout-container-padding-inline-end-with-close | sys.space.space100 | 8px |
--tanrei-comp-callout-container-shape | sys.shape.corner.medium | 8px |
--tanrei-comp-callout-error-body-color-outlined | comp.callout.body.color.outlined | |
--tanrei-comp-callout-error-body-color-tonal | sys.color.on-error-container | |
--tanrei-comp-callout-error-container-color-outlined | comp.callout.container.color.outlined | |
--tanrei-comp-callout-error-container-color-tonal | sys.color.error-container | |
--tanrei-comp-callout-error-icon-color-outlined | sys.color.error | |
--tanrei-comp-callout-error-icon-color-tonal | sys.color.error | |
--tanrei-comp-callout-error-title-color-outlined | comp.callout.error.body.color.outlined | |
--tanrei-comp-callout-error-title-color-tonal | comp.callout.error.body.color.tonal | |
--tanrei-comp-callout-icon-color-outlined | sys.color.on-surface-variant | |
--tanrei-comp-callout-icon-color-tonal | comp.callout.body.color.tonal | |
--tanrei-comp-callout-icon-gap | sys.space.space150 | 12px |
--tanrei-comp-callout-icon-opacity | — | 0.85 |
--tanrei-comp-callout-icon-size | sys.icon.small | 20px |
--tanrei-comp-callout-info-body-color-outlined | comp.callout.body.color.outlined | |
--tanrei-comp-callout-info-body-color-tonal | sys.color.on-info-container | |
--tanrei-comp-callout-info-container-color-outlined | comp.callout.container.color.outlined | |
--tanrei-comp-callout-info-container-color-tonal | sys.color.info-container | |
--tanrei-comp-callout-info-icon-color-outlined | sys.color.info | |
--tanrei-comp-callout-info-icon-color-tonal | sys.color.info | |
--tanrei-comp-callout-info-title-color-outlined | comp.callout.info.body.color.outlined | |
--tanrei-comp-callout-info-title-color-tonal | comp.callout.info.body.color.tonal | |
--tanrei-comp-callout-outline-color | sys.color.outline-variant | |
--tanrei-comp-callout-outline-width | sys.border.hairline | 1px |
--tanrei-comp-callout-success-body-color-outlined | comp.callout.body.color.outlined | |
--tanrei-comp-callout-success-body-color-tonal | sys.color.on-success-container | |
--tanrei-comp-callout-success-container-color-outlined | comp.callout.container.color.outlined | |
--tanrei-comp-callout-success-container-color-tonal | sys.color.success-container | |
--tanrei-comp-callout-success-icon-color-outlined | sys.color.success | |
--tanrei-comp-callout-success-icon-color-tonal | sys.color.success | |
--tanrei-comp-callout-success-title-color-outlined | comp.callout.success.body.color.outlined | |
--tanrei-comp-callout-success-title-color-tonal | comp.callout.success.body.color.tonal | |
--tanrei-comp-callout-title-color-outlined | comp.callout.body.color.outlined | |
--tanrei-comp-callout-title-color-tonal | comp.callout.body.color.tonal | |
--tanrei-comp-callout-title-gap | sys.space.space50 | 4px |
--tanrei-comp-callout-warning-body-color-outlined | comp.callout.body.color.outlined | |
--tanrei-comp-callout-warning-body-color-tonal | sys.color.on-warning-container | |
--tanrei-comp-callout-warning-container-color-outlined | comp.callout.container.color.outlined | |
--tanrei-comp-callout-warning-container-color-tonal | sys.color.warning-container | |
--tanrei-comp-callout-warning-icon-color-outlined | sys.color.warning | |
--tanrei-comp-callout-warning-icon-color-tonal | sys.color.warning | |
--tanrei-comp-callout-warning-title-color-outlined | comp.callout.warning.body.color.outlined | |
--tanrei-comp-callout-warning-title-color-tonal | comp.callout.warning.body.color.tonal | |
--tanrei-comp-callout-body-typescale-large | sys.typescale.body-large | 400 16px/1.625 |
--tanrei-comp-callout-body-typescale-medium | sys.typescale.body | 400 14px/1.5714 |
--tanrei-comp-callout-title-typescale-large | sys.typescale.heading-5 | 500 16px/1.5 |
--tanrei-comp-callout-title-typescale-medium | sys.typescale.heading-6 | 500 14px/1.4286 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
variant: tonal (27)
tonal
{} tonal-info
status: info statusLabel: Information variant: tonal tonal-warning
status: warning statusLabel: Warning variant: tonal tonal-error
status: error statusLabel: Error variant: tonal tonal-success
status: success statusLabel: Success variant: tonal title
title: Sizes are minimums title-info
title: A revised spec, not yet on every platfor… status: info title-only
title: All checks passed status: success large
size: large large-title-info
title: A revised spec, not yet on every platfor… status: info size: large icon
title: Tip slots: icon icon-info
title: Experimental status: info slots: icon title-slot
status: info slots: title actions
title: The file changed on disk status: warning slots: actions closable
closable: true closeLabel: Close closable-title-actions
title: Three fields need attention status: error live: true closable: true closeLabel: Close slots: actions status-label
status: warning statusLabel: Warning live-success
status: success statusLabel: Success live: true long-body
title: A revised spec, not yet on every platfor… status: info long-title
title: The native platforms have not renamed th… status: warning closable: true closeLabel: Close tonal-info-rtl
title: ملاحظة status: info direction: rtl closable-actions-rtl
title: تغيّر الملف على القرص status: warning closable: true closeLabel: إغلاق direction: rtl slots: actions icon-rtl
title: التالي direction: rtl slots: icon docs-note
title: Note status: info size: large docs-tip
title: Tip size: large slots: icon docs-caution
title: Back up before you migrate status: warning size: large docs-danger
title: Danger status: error size: large variant: outlined (11)
outlined
variant: outlined outlined-info
status: info statusLabel: Information variant: outlined outlined-warning
status: warning statusLabel: Warning variant: outlined outlined-error
status: error statusLabel: Error variant: outlined outlined-success
status: success statusLabel: Success variant: outlined title-outlined-warning
title: Accessibility status: warning statusLabel: Warning variant: outlined large-outlined-error
status: error statusLabel: Error variant: outlined size: large icon-outlined
title: Tip variant: outlined slots: icon actions-outlined
variant: outlined slots: actions closable-outlined-info
status: info statusLabel: Information variant: outlined closable: true closeLabel: Close ltr-title-rtl
title: v2.1 beta status: success variant: outlined direction: rtl 生成自 specs/callout.spec.json, tokens/comp/callout.tokens.json, design/tokens/
这三者只需区分一次,依据是它们位于哪里、停留多久:
| 组件 | 它是什么 | 位于哪里 | 停留多久 | 是否朗读 |
|---|---|---|---|---|
| 消息条(Snackbar) | 应用刚刚做完的事或刚刚发生的事的结果。 | 在内容之上,窗口的边缘。 | 很短,或者直到被关闭。 | 总是。 |
| 提示块 | 属于内容一部分的通知。 | 在页面流中,你放置它的地方。 | 内容在多久,它就在多久。 | 只在它为回应某件事而出现时(live)。 |
| 对话框 | 用户必须先回答才能继续的问题。 | 在一切之上,带遮罩。 | 直到得到回答。 | 通过获得焦点。 |
横幅——针对整个页面或屏幕、位于其内容上方的通知——并不是第四种东西。它就是一个放在该区域内容上方的提示块,通常带 live,通常带一个操作或关闭按钮。请把它放在所涉及区域的阅读顺序的最前面。
某个字段下方的消息(这个值不是日期)也不是提示块:它是这个字段自己的辅助文本。
variant |
绘制什么 | 用于 |
|---|---|---|
tonal(默认) |
柔和的填充。 | 大多数提示块。 |
outlined |
没有填充,只有一道细线。 | 位于着色表面上或有填充的卡片里的提示块(填充在那里会看不出来),以及许多提示块挨在一起的地方。 |
在每种语义状态下,这道细线都是系统唯一的那道细线。语义状态从不给它上色。
status 为 default、info、warning、error 或 success:这些名称也是消息条和 Tag 使用的名称,在整个系统里含义相同。语义状态绘制它的颜色和字形(info、warning、error、check_circle),并决定带 live 的提示块如何朗读。除此之外它不改变任何东西。
| 你要写的是 | 使用 |
|---|---|
| 说明:值得知道的事 | status="info" |
| 注意:需要留心的事 | status="warning" |
| 危险:会出问题的事 | status="error" |
| 小贴士:读者可以采纳的建议 | status="default",在 icon 插槽里放字形 lightbulb,标题写“小贴士” |
小贴士不是成功:没有什么事成功了。icon 插槽接受图标集里的任何字形,有没有语义状态都可以,所以“实验性”和“此版本新增”也不需要新的取值。那个词写在标题里。
size |
标题 | 正文 | 用于 |
|---|---|---|---|
medium(默认) |
14,行高 20 | 14,行高 22 | 控件之间的提示块:表单、面板、对话框。 |
large |
16,行高 24 | 16,行高 26 | 以文字为主的页面里的提示块。 |
这就是长文排版的两种尺寸。变化的只有文本样式:内边距、字形和关闭按钮都相同。
headline 是纯文本,从不是标题元素。大多数提示块的标题只是一条说明的标签;如果页面大纲里列出五次“说明”,页面反而更差。如果提示块开启了页面上用户会导航前往的一个部分,请在 title 插槽里放一个级别符合大纲需要的标题元素。提示块会像绘制任何标题一样绘制它,既不改变它的角色,也不改变它的级别。
在服务端渲染的页面上,请用 title 插槽给出标题:headline 属性由元素绘制,所以在元素定义之前不会显示。
直接放在元素里的文字采用提示块的文本样式和颜色。长篇的块(段落、列表、表格、代码块)放进一个与提示块尺寸一致、不限制行长的长文排版容器:
<tanrei-callout status="info">
<div class="tanrei-prose" data-measure="false">…渲染出来的 Markdown…</div>
</tanrei-callout>长文排版不带颜色,所以在任何填充上都能阅读,其中的链接带下划线。放在正文里的组件保留自己的样式。在限制行长的长文排版中,提示块像文本块一样受行长限制;在其他任何地方,它都撑满所给的宽度。
操作是你自己的按钮或链接,一到两个。请使用 outlined 或 text 按钮:tonal 按钮的填充与不带语义状态的 tonal 提示块相同,在那里看不出是按钮。提示块把它们排成可以换行的一行,除此之外什么都不做:按下其中一个时它不会关闭。
closable 绘制一个关闭按钮:一个小尺寸、只有图标的按钮,由 close-label 命名,变体由 --tanrei-comp-callout-close-button-variant 决定(默认系统中是 tonal)。在 tonal 提示块上,它用提示块自己的文字颜色绘制:一个由这种颜色变成半透明的圆,里面是同色的字形,因此在每种状态的填充上都看得见;在没有填充的 outlined 提示块上,它就是和别处一样的按钮。它像所有按钮一样响应悬停、焦点和按下。按下它会把 open 设为 false 并触发 dismiss。提示块从不自己关闭,也不响应 Escape。用户关闭之后,请记住这一点:每次访问都重新出现的可关闭提示块只是噪音。对旁边内容加以限定的注意事项不应该可以关闭。
live 用于在用户操作过程中出现或变化的提示块:提交之后对表单错误的汇总,或者告知文件已在用户不知情时发生更改的通知。请用 open="false" 把它留在页面里,在适用时设置 open。
带 live 的提示块,为回应一次按下而显示
<tanrei-callout id="errors" live open="false" status="error" headline="有 3 个字段需要处理">
请输入姓名,选择开始日期,并填写可以收信的地址。
</tanrei-callout>
<script type="module">
form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
event.preventDefault();
document.querySelector('#errors').open = true; // 朗读一次,立即打断(assertive)
}
});
</script>带 live 的提示块在显示时,以及在标题、正文或语义状态变化时朗读一次:default、info 和 success 以礼貌(polite)的方式朗读,warning 和 error 以立即打断(assertive)的方式朗读。页面加载时就已经在页面标记里的提示块,在那一刻不会朗读。
在 Web 上,页面加载之后由你的脚本添加进页面的带 live 的提示块会被朗读。元素无法区分一个提示块是为回应某件事而添加的,还是随单页应用的新视图一起添加的,所以如果通知只是新视图的一部分,就不要加 live。
正文、标题插槽和操作都是元素自己的子元素,所以它们就在文档里:页面内搜索能找到,可以被翻译、被选中,没有脚本也能阅读。@tanrei/web/callout.css 为尚未定义的提示块提供它将来的框——内边距、文本样式、为字形和关闭按钮留出的空间,而 open="false" 时则什么都没有——所以元素定义时任何东西都不会移动。在那之前,它显示时不带填充、字形和关闭按钮。
- 用文字说出语义状态。“警告:请先备份”无需更多说明;警告上的“无障碍”则需要
status-label="警告"。 - 按语义状态在各处的含义来使用它们。建议不是成功。
- 把一组要点放进一个提示块,而不是连续放几个提示块。
- 不要给写在页面里的说明使用带
live的提示块。静态内容不是警报。 - 不要嵌套提示块,也不要在页面顶部叠放几个:分量相同的通知并排在一起会相互抵消。
- 不要既把焦点移到错误汇总上,又朗读它。长表单的第一个错误可能在屏幕之外,适合把焦点移到汇总上;短表单适合朗读汇总,并把焦点移到第一个出错的字段。
- 不要指望靠颜色来传达语义状态。在强制颜色模式下,每个提示块都是一道边线,加上文本颜色的文字。
- 角色
note- 标注
- A callout that is not live is a note (the note role on the web), read where it stands, named by its title or, without one, by statusLabel. A live callout is a live region instead: polite in the default, info and success forms (the status role on the web) and assertive in the warning and error forms (the alert role on the web), announced once when it is shown after its page was presented and when its content changes. In both, statusLabel is exposed as text before the title and the body; the glyph is hidden. A title given as text is plain text; a heading in the title slot keeps its level. The close button's name is closeLabel. Actions are the product's own controls with their own names.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Tab | Reaches, at the callout's place in the page, the links and controls of the body, then the actions, then the close button. The callout itself is not a stop, and showing one never moves focus. |
| Enter / Space | Activates the focused close button: the callout closes and focus returns to where it was before it entered the callout (CLO-12). |
备注
- Static content is not an alert. A callout written into a page (a note in documentation, a caution above a procedure) must not interrupt a screen reader when the page loads or be repeated as a status message: it is read in order, as the paragraph before it is. live is for what appears or changes in answer to something, and even then a callout that the page is first presented with is not announced (CLO-21).
- A summary of a form's errors is a live callout with the error status, shown after the submit. Whether focus also moves to it is the product's decision, by its own means (the callout takes no focus by itself): moving focus to the summary suits a long form whose first error may be off screen; announcing it and moving focus to the first field in error suits a short one. Do not do both.
- A notice for a whole page or screen (a banner: the connection is lost, the file changed on disk, a trial ends) is a callout placed by the product above that region's content, live, usually with an action or a close button. Put it where it comes first in reading order for the region it concerns.
- WCAG 1.4.1 (use of color): a status is never told by color alone. A status form draws the status's glyph, a shape of the icon set, and the status is in the title's words or in statusLabel (CLO-20). The component cannot tell whether a title says its status, so that is the product's to check: "Accessibility" on a warning needs statusLabel "Warning"; "Warning: not published yet" does not.
- WCAG 1.3.1 (info and relationships): the callout does not decide whether its title is a heading. Most titles are labels of a note and should stay text, or the page's outline fills with entries such as "Note". Where a callout does open a part of the page that people navigate to, the product gives a heading of the right level in the title slot.
- WCAG 1.4.3 and 1.4.11 (contrast) in the default system: the title and the body are a role on its paired role (on-secondary-container on secondary-container, on-info-container on info-container, on-surface on a surface), each held to 4.5:1 by the token invariants. The glyph is drawn at an opacity of its color, so its contrast is that of the color as drawn: at least 3.5:1 on its own status container and on every surface, in every scheme. A link in the body is the system's link color, underlined: at least 4.6:1 on every tonal container (7.2:1 in the high-contrast schemes). The outlined look's edge is decorative.
- Actions are the product's Buttons. A filled or a tonal Button brings its own fill and keeps its own contrast; the label of an outlined or a text Button sits on the callout's fill, where the default system's primary keeps at least 13:1 on every tonal container. A tonal Button has the fill of the tonal callout without a status and cannot be seen as a button there: use outlined or text. The focus indicator of a product's control in the body or the actions is the system's focus color, at least 4.6:1 on every tonal container of the default system.
- A closable callout that returns on every visit is noise: when the person closes one, the product should remember it. The callout only reports the close (dismiss).
- Do not nest callouts, and do not stack several at the top of a page: two notices of the same weight side by side cancel each other. One callout may hold a list of points.
- Forced colors (CLO-24): a tonal callout loses its fill, so every look gains an edge in the text color. Statuses then differ by glyph and words alone.
- Web: the body, the title slot and the actions are light-DOM children of `<tanrei-callout>`, so they stay in the document: they are found by search in the page, translated, selected and read without script, and before the element is defined they show as plain blocks in the page's own text styles, without the fill, the glyph and the close button. A title given by the `headline` attribute is not shown until the element is defined; content pages should use the title slot.
- The callout itself has no states: nothing about its container, its text or its glyph answers a pointer, a press or focus. Its close button is a Button, and answers them as every Button does.
- WCAG 1.4.11 (non-text contrast) and 2.4.13 (focus appearance) for the close button: in a filled look it is drawn in the tone of the fill (BTN-45), its glyph in the fill's content color (the body color, held to 4.5:1 on the fill by the token invariants) over a container of that color at comp.button.tone.container.opacity. In the default system the glyph keeps against that blend, in every status, at least 9.5:1 at rest in the light schemes and 7.7:1 in the dark ones, and at least 6.0:1 hovered, focused or pressed in every scheme; the focus indicator's outer stroke, in the same color on the callout's fill, keeps at least 12:1. In the outlined look it is the variant's own Button on the surface, whose glyph the Button's own tokens hold.
生成自 specs/callout.spec.json, tokens/sys/measure.tokens.json
- 它是一条说明(
role="note"),由标题命名;没有标题时由status-label命名。带live时它改为实时区域:role="status",警告或错误则为role="alert"。 status-label作为单独的短语在标题和正文之前朗读(“警告,……”),从不绘制。status为default时它会被忽略。- 字形对辅助技术隐藏。
- 以文本给出的标题是纯文本。
title插槽里的标题元素保留自己的级别。 - 它不是焦点顺序中的停靠点,也从不移动焦点。Tab 先到达正文里的链接和控件,然后是操作,最后是关闭按钮。
- 焦点在提示块内时关闭它,焦点会回到进入提示块之前持有焦点的元素,所以下一次 Tab 会到达其后的内容。
- 关闭按钮就是一个按钮:它的点击区域会扩展到最小目标;在 tonal 提示块上,它的字形和焦点环使用提示块的文字颜色,在默认系统的每种状态和配色方案中,相对于按钮都保持至少 6:1。
- 文本放大时,所有内容都会换行并变大;字形和关闭按钮留在第一行。
- 在强制颜色模式下,每种外观都会多出一道文本颜色的边线,字形会完整绘制。
- 在从右到左的布局中,字形移到右侧,关闭按钮移到左侧,图标集标记为需要镜像的字形会被镜像,从左到右书写的标题保持自己的顺序。
- 未知的变体、尺寸或语义状态,既没有标题也没有正文的提示块,没有用文字给出的语义状态,没有名称的关闭按钮,以及没有语义状态却设置了的
status-label,会在调试构建中报告。
- 检查每个提示块的文字是否说出了它的语义状态。组件无法判断标题有没有说出来。
- 用页面的语言来写
status-label和close-label。 - 只给为回应某件事而出现或变化的提示块使用
live。 - 给操作一个合适的标签。按钮的标签只有一行,所以在窄屏幕上文本放大时,长标签可能比提示块还宽。