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

标签组(Tag group)

把一组 Tag 或纸片排成会换行的若干行,按其中的内容留出间距,并把这一组作为一个有名称的列表暴露出来。成员保持原样;纸片被移除之后,焦点会移到下一个纸片上。

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

概览

Web
tanrei-tag-group
schedule
SwiftUI
TanreiTagGroup
radio_button_unchecked
Jetpack Compose
TanreiTagGroup
radio_button_unchecked
Flutter
TanreiTagGroup
radio_button_unchecked
React Native
TagGroup
radio_button_unchecked

生成自 docs/status/status.json, specs/tag-group.spec.json

一组 Tag,和一行筛选条件

一个标签(Tag)、一个纸片(Chip),各自都只是一样东西。标签组用来容纳好几个这样的东西:一篇文章的标签、列表上方的筛选条件、字段下方的几条建议。它把它们排成会换行的若干行,让它们之间保持合适的距离,并告诉辅助技术它们是一个列表。

标签组自己什么也不绘制。它没有容器,没有边线,没有内边距,也没有可见的标签。每个成员的绘制、聚焦和按下,都与它在别处时完全一样。

在 Web 上它是 <tanrei-tag-group>。其他平台尚未实现。

用法

<!-- 一组静态 Tag 要说明自己是 Tag:它的各行之间和 Tag 之间一样近。 -->
<tanrei-tag-group spacing="tags" label="标签">
  <tanrei-tag label="设计令牌"></tanrei-tag>
  <tanrei-tag label="无障碍"></tanrei-tag>
</tanrei-tag-group>

<!-- 一组纸片使用默认间距:它的各行之间离得足够远,让每个纸片都有自己的触摸目标。 -->
<tanrei-tag-group id="recipients" label="收件人">
  <tanrei-chip kind="input" label="李华" remove-label="移除李华"></tanrei-chip>
  <tanrei-chip kind="input" label="王芳" remove-label="移除王芳"></tanrei-chip>
</tanrei-tag-group>

<script type="module">
  import '@tanrei/web/components/tag-group';
  import '@tanrei/web/components/tag';
  import '@tanrei/web/components/chip';

  const recipients = document.querySelector('#recipients');
  // 纸片绝不会移除自己。由你把它取出;标签组会把焦点移到下一个纸片上。
  recipients.addEventListener('remove', (event) => {
    event.target.remove();
    // 最后一个也移除之后,标签组就没有地方可以放焦点了。请把焦点放到这一组所属的控件上。
    if (!recipients.children.length) document.querySelector('#add-recipient').focus();
  });
</script>
import { Chip, TagGroup } from '@tanrei/react';

<TagGroup label="收件人">
  {recipients.map((person) => (
    <Chip key={person.id} kind="input" label={person.name} removeLabel={`移除${person.name}`} onRemove={() => remove(person.id)} />
  ))}
</TagGroup>

规格

会换行的 Tag、表示状态的 Tag、通往每个标签对应页面的链接,以及已输入的项

构成

部件 说明
1 group The set: lines of members that start at the start edge and wrap. It draws nothing of its own — no container, no fill, no edge, no padding and no label — and is exposed as one list.
2 member A tag or a chip of the set: placed and spaced by the group, exposed as an item of its list, and otherwise drawn and behaving as it does anywhere.

属性

属性 类型 默认值 说明
spacing tags | chips "chips" What the set holds, which decides the distances between its members. tags: static tags, as close as words of one group. chips: chips, which take input, with lines far enough apart that each chip keeps its whole touch target.
label string "" The accessible name of the set ("Tags", "Filters", "Suggested fonts"). It is not drawn: a set that needs a visible name stands under the product's own heading or text, and still takes this. Optional where the set is plain from what surrounds it, recommended for a set of chips.
selection none | single | multiple "none" Whether the group holds the selection of its filter chips. none: it holds none, and each filter chip holds its own selected, as alone. single: at most one filter chip is on at a time, named by value. multiple: any number are on, named by values. While it is single or multiple the set is one Tab stop and the arrow keys move between its chips.
value string null The value of the filter chip that is on while selection is single; null puts none on. The group sets it when a person switches a chip and emits change; the product may also set it.
values strings The values of the filter chips that are on while selection is multiple: a list of strings (the platforms take a set). An empty list puts none on.

事件

事件 说明
change While selection is single or multiple, a person switched a filter chip of the set: carries value (single) or values (multiple) after the change, which the group now holds. In place of the chip's own change, which stops at the group (TGR-19).

条目

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

tag

A Tag of the set: the tag component, placed and spaced by the group and exposed as an item of its list, otherwise drawn and read as it is anywhere.

属性 类型 默认值 说明
label string "" Its text, as the Tag's label.
variant tonal | outlined | filled "tonal" Its look, as the Tag's variant.
status default | info | warning | error | success "default" What it reports, as the Tag's status.
size small | medium "medium" Its height, as the Tag's size. Give every member of a group the same size.
icon string "" The glyph in the Tag's icon slot, by its name in the system's icon set. Empty for none. It is how a fixture fills that slot; in an application the slot is filled as the Tag's own spec says.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-tag TanreiTag TanreiTag TanreiTag Tag
label · 属性 label title label label label
variant · 属性 variant variant variant variant variant
status · 属性 status status status status status
size · 属性 size size size size size
icon · 属性 slot="icon" icon icon icon icon
备注 A tanrei-tag child, as anywhere. A TanreiTag child, as anywhere. A TanreiTag call in the group's content, as anywhere. A TanreiTag in children, as anywhere. A Tag child, as anywhere.

chip

A Chip of the set: the chip component, placed and spaced by the group and exposed as an item of its list, otherwise drawn and behaving as it does anywhere, with its own state, events and Tab stop.

属性 类型 默认值 说明
kind action | filter | input "action" What it does, as the Chip's kind.
label string "" Its text and accessible name, as the Chip's label.
size small | medium "medium" Its height, as the Chip's size. Give every member of a group the same size.
selected boolean false Whether a filter chip is on, as the Chip's selected. While selection is none the chip holds it; while selection is single or multiple the group sets it from value or values (TGR-17).
value string "" What the group's value and values name a filter chip by, as the Chip's value. Unique within the set; needed while selection is single or multiple.
disabled boolean false As the Chip's disabled: one chip of a set may be disabled by itself.
href string "" The destination of an action chip, as the Chip's href: it makes the chip a link.
removeLabel string "" The accessible name of an input chip's remove action, as the Chip's removeLabel.
icon string "" The glyph in the Chip's icon slot, by its name in the system's icon set. Empty for none. It is how a fixture fills that slot; in an application the slot is filled as the Chip's own spec says.
事件 说明
click The chip was activated, as the Chip's click.
change A filter chip was switched, as the Chip's change. While selection is single or multiple it stops at the group, whose own change carries the selection (TGR-19).
remove The person asked to remove an input chip, as the Chip's remove. The product removes it; the group then places the rest and moves focus (TGR-9).
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-chip TanreiChip TanreiChip TanreiChip Chip
kind · 属性 kind isSelected / onRemove onSelectedChange / onRemove TanreiChip / TanreiChip.filter / TanreiChip.input kind
label · 属性 label title label label label
size · 属性 size size size size size
selected · 属性 selected isSelected selected selected selected
value · 属性 value value value value value
disabled · 属性 disabled disabled enabled isEnabled disabled
href · 属性 href destination href href href
removeLabel · 属性 removeLabel removeLabel removeContentDescription removeSemanticLabel removeAccessibilityLabel
icon · 属性 slot="icon" icon icon icon icon
click · 事件 click action onClick onPressed onPress
change · 事件 change isSelected onSelectedChange onSelected onSelectedChange
remove · 事件 remove onRemove onRemove onDeleted onRemove
备注 A tanrei-chip child, as anywhere; remove-label is its attribute. The page removes the element on remove; the group sees it leave and moves focus (TGR-9). A TanreiChip child, as anywhere. A TanreiChip call in the group's content, as anywhere; enabled is disabled's inverse. A TanreiChip in children, as anywhere. Give each a Key, so that the group can tell which one left. A Chip child, as anywhere. Give each a key, so that the group can tell which one left.

状态

行为

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

ID 需求
TGR-1 THE tag group SHALL lay out its members in lines: from its start edge, in the order they were given, comp.tag-group.gap.tags apart along a line WHILE spacing is tags and comp.tag-group.gap.chips apart WHILE spacing is chips, each member at its own size and centred on the middle of its line. It SHALL be as wide as its layout gives it and as tall as its lines, SHALL draw no container, fill, edge or elevation of its own, SHALL add no padding around its members, and SHALL NOT draw its label.
TGR-2 WHEN a member does not fit in what is left of a line THE tag group SHALL put it, whole, at the start of a new line below, comp.tag-group.row-gap.tags below the line above WHILE spacing is tags and comp.tag-group.row-gap.chips WHILE spacing is chips, measured between the visible bounds of the tallest members of the two lines; it SHALL NOT cut, shrink or split a member to keep it on a line, SHALL NOT scroll its lines, and SHALL NOT hide a member or stand a count in its place: every member it is given is shown. WHERE one member alone is wider than the group THE tag group SHALL give it the group's width on a line of its own, and the member SHALL then cut its own label as its own spec says (TAG-5, CHP-17).
TGR-3 THE tag group SHALL keep its members in the order they were given: it SHALL NOT sort them, move a selected chip forward or group them by kind, and the order in which focus moves through its chips and in which assistive technology reads its members SHALL be that order — along each line from its start, and line after line from the top.
TGR-4 THE tag group SHALL take from spacing only the distances of TGR-1 and TGR-2: spacing SHALL NOT change what a member is, how it is drawn or what the group exposes. WHERE the platform can tell what its members are, WHILE spacing is tags and a member takes input (a chip) THE tag group SHALL still render and in debug builds report that a set of chips wants spacing chips, because the lines of tags stand closer than a chip's touch target is high.
TGR-5 WHILE the hit areas that two neighbouring chips extend beyond their visible bounds (CHP-5, CHP-21) overlap THE tag group SHALL give every press on a chip's visible bounds to that chip, and a press in the gap between two chips — along a line or between two lines — to the nearer one, so that neighbouring hit areas meet at the middle of the gap. comp.tag-group.row-gap.chips is what keeps each chip's target whole between two lines: at sys.target.touch minus a chip's height or more, no hit areas overlap, as under the default system's value for a medium chip; a small chip on wrapped lines, and a layer that narrows the gap, have targets that meet between the lines.
TGR-6 THE tag group SHALL NOT restyle its members: each SHALL render from its own tokens at its own size, answer hover, focus and press itself, hold its own state (a filter chip its selected, which the group sets WHILE selection is single or multiple, TGR-17), emit its own events (but the change of a filter chip the group holds, TGR-19) and keep its own role and name inside the item the group exposes it as (TGR-7). WHILE selection is none each chip SHALL be its own Tab stop — an input chip one, the chip itself, whose remove action is not a stop and which Backspace or Delete removes (CHP-18, CHP-19, CHP-23) — in the order of TGR-3, and the tag group SHALL NOT handle arrow keys, Home or End, SHALL keep no selection of its own, SHALL NOT make its filter chips exclusive, and SHALL emit no event; what a selection changes is TGR-17 to TGR-21. The tag group itself SHALL NOT take focus, whatever its selection. The tag group has no size of its own: members of different sizes on one line are centred on it (TGR-1).
TGR-7 WHILE the tag group holds at least one member THE tag group SHALL expose itself to assistive technology as a list, named by label WHILE label is not empty, and each member as one item of that list, in the order of TGR-3, holding the member as the member exposes itself: a tag as its text (TAG-11), a chip as its button or its link with its state, an input chip with its remove action beside it. WHEN label changes THE tag group SHALL expose the new name at once. WHILE label is empty THE tag group SHALL still expose the list, unnamed. A platform that has no list role SHALL expose a container named by label that holds the members in order, and state that in its README.
TGR-8 WHILE the tag group holds no member THE tag group SHALL draw nothing, take no space and expose nothing — no empty list and no name — and SHALL report nothing: a set that has no tags yet, or whose last filter was removed, is not a mistake. WHILE it holds one member THE tag group SHALL lay it out and expose it as a list of one.
TGR-9 WHEN a member leaves the tag group while focus is on it — on a chip or on its remove action — THE tag group SHALL move focus to the chip that follows it in the order of TGR-3, or to the chip before it WHERE none follows, on the chip itself and never on its remove action, so that a person who removes several entered items in a row goes on from where they were; it SHALL skip a member that takes no focus (a tag, a disabled chip). WHERE no member that takes focus is left THE tag group SHALL NOT move focus, having nowhere of its own to put it: the product moves it to the control that the set belongs to (CHP-22). THE tag group SHALL NOT remove a member itself and SHALL announce nothing.
TGR-10 WHEN a member is added, is removed or changes its size THE tag group SHALL place its members again by TGR-1 and TGR-2 and SHALL move every member whose place that changes from where it was drawn to its new place over comp.tag-group.motion.move-duration with comp.tag-group.motion.move-easing, along its line and from one line to another, without fading or scaling it; a change that comes while members move SHALL take each of them on from where it is drawn. A member that is added SHALL appear in its place at once and one that is removed SHALL leave at once. WHILE a line holds the same members in the same order as before the change, its members SHALL follow the change at once: a member that changes its size, at once or over a motion of its own, carries the members after it on its line, and a line whose height changes carries the lines below. THE tag group's own height SHALL follow its lines at once, and WHEN the width the layout gives the tag group changes THE tag group SHALL place its members again at once. WHERE motion is reduced THE tag group SHALL place its members again at once. A member that has focus while it moves SHALL keep it, its focus indicator moving with it, and THE tag group SHALL NOT move focus because a member was added.
TGR-11 WHILE the tag group holds content that is not a Tanrei tag or chip THE tag group SHALL still lay it out and expose it as a member, and in debug builds report it; a platform that cannot inspect its content SHALL state the rule in its README and docs page.
TGR-12 WHILE the text size is enlarged THE tag group SHALL keep its gaps and let its members grow as their own specs say (TAG-13, CHP-17), wrapping onto as many lines as they then need; at 200% no member SHALL be lost, lie over another or be cut by the group, and the group SHALL need scrolling in no direction of its own.
TGR-13 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 tag group SHALL draw nothing of its own there either: its members draw themselves as their own specs say (TAG-14), and the distances of TGR-1 and TGR-2 stay what tells them apart.
TGR-14 WHERE the layout direction is right-to-left THE tag group SHALL start every line at the right edge and place its members from right to left, in the order they were given, wrapping to a new line below as TGR-2 says; the order of focus and of reading SHALL stay the order of TGR-3. Each member's own content SHALL follow the layout direction as the member's spec says (TAG-15, CHP-25).
TGR-15 THE tag group 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 tag group is shown THE tag group SHALL draw itself for the new direction at once, without a motion of its own, keeping its members, their state and their focus.
TGR-16 WHEN spacing names a value that the active design system does not have THE tag group SHALL render with the default value's tokens, and in debug builds report the unknown value.
TGR-17 WHILE selection is single or multiple THE tag group SHALL hold the selection of its filter chips by their value: it SHALL set each filter chip's selected to whether value equals the chip's value (single) or values holds it (multiple) WHEN selection, value or values changes, WHEN a filter chip arrives and WHEN a chip's value or kind changes, so that what each chip shows and exposes as its pressed state (CHP-11) is always the group's selection. A chip that is not a filter chip, a tag and other content take no part in it, and are laid out, reached and exposed as WHILE selection is none. WHEN value or values names no member THE tag group SHALL put nothing on for that name and report nothing, because a filter whose chip was taken away is not a mistake; WHEN a member leaves THE tag group SHALL leave value and values as they are and emit nothing. WHEN selection becomes none THE tag group SHALL leave each chip's selected as it then is and hold it no longer. WHEN selection names a value it does not have THE tag group SHALL behave as WHILE it is none, and in debug builds report the value.
TGR-18 WHILE selection is single or multiple, WHERE a filter chip's value is empty THE tag group SHALL leave that chip out of its selection — it keeps its own selected and its own change, as WHILE selection is none — and WHERE two filter chips share a value THE tag group SHALL put them on and off together, because they name one thing; in debug builds it SHALL report each, and it SHALL render either way.
TGR-19 WHEN the person switches a filter chip that has a value — by pointer, touch, Enter, Space or assistive technology — WHILE selection is single THE tag group SHALL set value to the chip's value WHERE the chip is now on, and switch every other filter chip off, or to null WHERE it is now off, because each chip is a toggle that says it is pressed and a pressed toggle that cannot be released would be untrue; WHILE selection is multiple THE tag group SHALL add the chip's value to values WHERE the chip is now on, or take it out WHERE it is now off. THE tag group SHALL then emit change exactly once, carrying value (single) or values (multiple), which it already holds, and SHALL stop the chip's own change at the group, so that a listener on the group or above it hears one change that carries the selection. THE tag group SHALL emit nothing WHEN the product sets selection, value or values.
TGR-20 WHILE selection is single or multiple and the group holds a chip that takes focus THE tag group SHALL be one stop in the focus order, in place of the stops TGR-6 gives each chip: the chip that last had focus WHILE it is still in the group and takes focus, otherwise the first filter chip that is on, otherwise the first chip that takes focus; every other chip, and with it its remove action, SHALL be out of the Tab order and stay focusable by the keys of this rule, by pointer and touch, and by assistive technology. WHILE a chip of the group has focus THE tag group SHALL move focus, without switching or removing anything: on Right Arrow to the chip drawn after it and on Left Arrow to the one drawn before it — in a right-to-left layout the same two keys the other way, so that an arrow always moves the way it points — on Home to the first chip and on End to the last, passing every member that takes no focus (a tag, a disabled chip); focus SHALL NOT wrap, and Up Arrow and Down Arrow SHALL do nothing: the members are one row in their order, however many lines draw it. Enter and Space SHALL stay the focused chip's own — Space switches a filter chip (CHP-11) — and so SHALL Backspace and Delete on an input chip (CHP-19).
TGR-21 WHILE selection is single or multiple, WHEN a member leaves while focus is on it THE tag group SHALL move focus as TGR-9 says, and the chip that then has focus SHALL be the set's stop; WHEN the chip that was the set's stop leaves, is disabled or no longer takes focus while focus is elsewhere THE tag group SHALL make a stop as TGR-20 says of a group whose last-focused chip is gone, so that the set keeps exactly one stop while it holds a chip that takes focus. WHEN selection becomes none THE tag group SHALL give each chip its own stop again.
TGR-22 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 tag group 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 list — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE label is not empty it is the list's name, over a name given so; WHILE label is empty, a name given so is the list's name. 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 tag group SHALL take it off that element.

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-tag-group TanreiTagGroup TanreiTagGroup TanreiTagGroup TagGroup
spacing · 属性 spacing spacing spacing spacing spacing
label · 属性 label label contentDescription semanticLabel accessibilityLabel
selection · 属性 selection selectionMode selectionMode selectionMode selectionMode
value · 属性 value selection selectedValue selectedValue value
values · 属性 values selection selectedValues selectedValues values
change · 事件 change selection onSelectionChange onSelectionChanged onSelectionChange
备注 <tanrei-tag-group label="Filters" spacing="chips"> holds tanrei-tag and tanrei-chip children. The members stay in the product's light DOM, in the default slot: they keep their attributes, events, styles and place in the document, and the group moves none of them. The shadow root holds one element with role="list" and aria-label, a flex container that wraps (flex-wrap: wrap; align-items: center; column-gap; row-gap from the tokens of spacing), around the slot; the host has no role, and the group gives each slotted member role="listitem" where it has no role of its own and takes it back when the member leaves (slotchange), as the carousel exposes its items. A tanrei-chip host carries no role itself — its button or link is in its shadow root — so the item holds that control. While the slot is empty the host is display: none and the list is not rendered (TGR-8). Removal: a MutationObserver and slotchange find a member that left while :focus-within held on it, and the group calls focus() on the next chip (TGR-9). The debug report of TGR-4 looks for a tanrei-chip among the slotted elements. Before the element is defined its children are already shown, as the inline elements they are, in source order and separated by the white space between them: a set of tags is readable and a set of chips is operable once the chips themselves are defined; a page that wants the final layout from the first paint adds tanrei-tag-group:not(:defined) { display: flex; flex-wrap: wrap; gap: … } with the token's custom properties. The host is display: block. Selection (TGR-17 to TGR-21): selection and value are attributes and properties; values is a property (an array), and the values attribute is also read, as JSON or as space-separated words, because the fixture harness writes attributes. A chip's value is the tanrei-chip's own value. While selection is single or multiple the group writes each filter chip's selected, stops the chip's change at the host and dispatches its own change (detail.value or detail.values), and gives every chip but the set's stop tabindex="-1" (a host that delegates focus with a negative tabindex is out of the Tab order, with everything in its shadow root), taking back only the tabindex it gave; it handles the arrow keys, Home and End from a keydown listener on the host. TanreiTagGroup(_ label: String = "", spacing: .chips) { TanreiChip(…) … }: a custom Layout that flows its subviews into lines by TGR-1 and TGR-2 and follows layoutDirection. The container is .accessibilityElement(children: .contain) with the label; SwiftUI has no list trait for an arbitrary layout, so VoiceOver reads the label and then each member (TGR-7's fallback). Focus after a removal uses @AccessibilityFocusState and @FocusState keyed by the members' identities, which the caller gives with .id(_:) as in a ForEach. Selection: TanreiTagGroup(selection: $value) with a Binding<String?> (single) or a Binding<Set<String>> (multiple), each chip given its value as TanreiChip(value:); the arrow keys come from focusable() and onMoveCommand. TanreiTagGroup(spacing = TanreiTagGroupSpacing.Chips, contentDescription = "Filters") { … }, built on FlowRow with the tokens' horizontal and vertical arrangements. Semantics: a traversal group with contentDescription and collectionInfo (one row, as many columns as members), each member given collectionItemInfo; TalkBack then reads the count. Focus after a removal: the group keeps a FocusRequester per member key and requests the next one when a focused member leaves the composition. TanreiTagGroup(spacing: TanreiTagGroupSpacing.chips, semanticLabel: 'Filters', children: […]), built on Wrap with spacing, runSpacing and crossAxisAlignment: WrapCrossAlignment.center; Wrap follows Directionality. Semantics(container: true, label:) around it; Flutter has no list role for a Wrap, so the members are read in order after the label. Focus after a removal: the group compares its children's keys between builds and requests focus on the next chip's FocusNode. <TagGroup spacing="chips" accessibilityLabel="Filters">{…}</TagGroup>: a View with flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', columnGap and rowGap from the tokens, accessibilityRole="list" (announced on Android; a labelled container on iOS). It reads its children's keys to find the chip that follows a removed one and focuses it through its ref. Follows the app's direction where an ancestor's is not known.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-tag-group-gap-chips sys.space.space100 8px
--tanrei-comp-tag-group-gap-tags sys.space.space100 8px
--tanrei-comp-tag-group-motion-move-duration sys.motion.role.selection.travel 250ms
--tanrei-comp-tag-group-motion-move-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-tag-group-row-gap-chips sys.space.space200 16px
--tanrei-comp-tag-group-row-gap-tags sys.space.space100 8px

测试样例

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

生成自 specs/tag-group.spec.json, tokens/comp/tag-group.tokens.json, design/tokens/

指南

标签组用来做什么

这一组是什么 使用
静态的 Tag:文章的类别、一项设置可以取的值、一行状态 设置了 spacing="tags" 的标签组。
通往每个标签对应页面的标签 由带 href 的纸片组成的标签组。Tag 是文本,不能成为链接。
列表上方的筛选条件、字段下方的建议、用户输入的项 由纸片组成的标签组。
作为一个控件工作的筛选条件:一次只打开一个,或者打开好几个,只按一次 Tab 就能越过 设置了 selection、由筛选纸片组成的标签组(见下文)。
必须留在一行上的一排切换按钮 工具栏。
两到四个不同的操作 按钮组。
几个当中选一个 单选组、标签页或分段按钮。
需要可见标签、必填和选填标记和错误信息的控件 字段组。
字段里面已输入的值 由字段自己处理。组合框会自己排布它的纸片。

间距

spacing 说明这一组里放的是什么,并决定两个距离:同一行上两个成员之间的距离,以及两行之间的距离。

  • chips(默认)用于纸片。纸片在可见边缘之外也接收按下,范围最大到一个触摸目标。行与行之间离得足够远,让每个中号纸片都保有完整的触摸目标;两个纸片的区域会重叠的地方,它们在间隙的正中相接。
  • tags 用于静态的 Tag。行与行之间的距离和一行上 Tag 之间的距离一样近,所以换了行的一组读起来仍是一整块。

默认值是稳妥的一侧。留在 chips 的一组 Tag 只是彼此离得稍远一些。设为 tags 的一组纸片仍然可以工作,但它的行距小于触摸目标的高度,调试构建会报告这一点。

小号纸片用于密集、并且这一组只占一行的地方。换行之后,它们的触摸目标还没有达到完整大小,就已经在两行之间相接了。在触摸屏上会换行的一组,请使用中号纸片。

设计系统可以为 spacing 添加取值。

布局

  • 成员按你给出的顺序排列。标签组从不对它们排序,也从不把选中的纸片移到前面。
  • 放不下的成员会整个移到下一行。不截断、不缩小、不隐藏,也不滚动,更没有“还有 3 个”。想少显示一些,就少给标签组几个成员,并把最后一个做成显示其余成员的操作纸片。
  • 比标签组还宽的单个成员,宽度等于标签组的宽度,并用省略号截断它自己的标签。
  • 这一组从起始边开始排列。要把它放在一行的末尾或中间,请用它周围的布局来摆放标签组。
  • 给每个成员相同的尺寸。尺寸不同的成员会在所在的行上居中对齐。
  • 没有成员的标签组什么也不绘制,也不占空间。没有标签的帖子不需要特殊处理。

组的标签

label 为辅助技术给这一组命名:“标签”“筛选条件”。它不会被绘制。这一组需要可见的名称时,请在它前面写上你自己的标题或文字,同时仍然提供 label。

选择

默认情况下,标签组不持有值。每个筛选纸片持有自己的 selected,每个纸片各自是一个 Tab 停靠点。

筛选条件作为一个控件工作时,请设置 selection。给每个筛选纸片一个 value,标签组就会说出哪些纸片是打开的。

  • selection="single":最多只有一个纸片打开,由 value 指明。打开一个纸片会把其余的关掉。把打开的那个纸片关掉之后,就没有纸片打开,value 为 null,因为每个纸片都是一个开关。
  • selection="multiple":可以打开任意多个,由 values 指明。

用户切换一个纸片时,标签组设置 value 或 values,并触发一次带有 detail.value 或 detail.values 的 change。纸片自己的 change 到标签组为止。你自己设置 value 或 values 时,纸片会随之切换,但不触发任何事件。

有选择时,这一组是一个 Tab 停靠点:上次获得焦点的纸片;没有的话,是第一个打开的纸片;再没有的话,是第一个纸片。方向键按它所指的方向在纸片之间移动,在从右到左的布局中也是如此;Home 和 End 移到第一个和最后一个纸片。这些按键不切换任何纸片,焦点也不会从最后一个纸片循环回第一个。Space 切换获得焦点的纸片。

一次一种排序方式,以及多个筛选条件

<tanrei-tag-group id="filters" label="筛选条件" selection="multiple" values="open">
  <tanrei-chip kind="filter" label="未关闭" value="open"></tanrei-chip>
  <tanrei-chip kind="filter" label="分配给我" value="assigned"></tanrei-chip>
  <tanrei-chip kind="filter" label="待审阅" value="review"></tanrei-chip>
</tanrei-tag-group>

<script type="module">
  document.querySelector('#filters').addEventListener('change', (event) => {
    // detail.values:现在打开的纸片的值,标签组已经持有这些值。
    applyFilters(event.detail.values);
  });
</script>
import { Chip, TagGroup } from '@tanrei/react';

<TagGroup label="排序方式" selection="single" value={order} onChange={(event) => setOrder(event.detail.value)}>
  <Chip kind="filter" label="最新" value="newest" />
  <Chip kind="filter" label="最近更新" value="updated" />
  <Chip kind="filter" label="评论最多" value="comments" />
</TagGroup>

选择只包含筛选纸片。这一组里的其他纸片,例如“添加”纸片或已输入的项,用同样的方向键到达,并照常工作。已输入的项仍然用 Backspace 或 Delete 移除,之后获得焦点的纸片成为这一组的 Tab 停靠点。

你拿掉一个筛选纸片时,标签组让 value 和 values 保持原样。如果这个筛选条件也应当消失,请自己修改它们。

必须始终恰好有一个答案、且无法清除的选择,应使用分段按钮或单选组。

移除纸片

纸片请求被移除,由你来移除它。如果当时焦点在这个纸片上,或在它的移除操作上,标签组会把焦点放到它后面的那个纸片上;后面没有纸片时,放到前面的那个上。连续移除好几项的人,可以从原来的位置接着做下去。

除非你在某个纸片上设置 destructiveRemove,纸片的移除操作都是普通外观,标签组也不会改变它。只有当移除会丢失一步无法恢复的东西时(例如附件),才设置它;已应用的筛选条件或搜索词一步就能恢复。

有两件事仍然由你负责:

  • 没有纸片可以接收焦点时,请把焦点移到这一组所属的控件上:用来添加项的字段,或者打开筛选条件的按钮。否则浏览器会把焦点丢掉。
  • 标签组不朗读任何内容。移除必须被听到时,例如被移除的筛选条件离它所改变的列表很远,请用实时区域(live region)或消息条(Snackbar)来报告。

推荐

  • 一组静态 Tag 请设置 spacing="tags"。
  • 给这一组一个 label,使用页面的语言。
  • 一组里只放一种成员。已输入的项后面跟一个“添加”纸片是例外。

避免

  • 不要把标签组用于对话框或表单的操作。请使用按钮组。
  • 不要在里面放 Tag 和纸片以外的东西。其他内容会照样排布,并在调试构建中报告。
  • 不要从标签组去改成员的样式。请改为在层里更改标签组的令牌。

无障碍

角色
list
标注
label names the list ("Tags", "Filters"); it is not drawn. Each member is one list item and keeps its own name inside it: a tag its text, a chip its label, an input chip's remove action its removeLabel. A filter chip's state is its own pressed state, also while the group holds the selection: the chip then shows the group's selection. An empty group exposes nothing.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab / Shift+Tab While selection is none: moves focus to each chip of the set in turn, in the order given; an input chip is one stop, its remove action is not in the Tab order. While selection is single or multiple: the set is one stop — the chip that last had focus, else the first chip that is on, else the first chip (TGR-20). A set of tags has no stop.
Left Arrow / Right Arrow, Home / End While selection is single or multiple: move focus to the chip drawn before or after (the way the arrow points, in a right-to-left layout too), or to the first or the last, without switching anything; focus does not wrap. Up and Down Arrow do nothing. While selection is none the group handles no key.
Enter, Space, Backspace / Delete The focused chip's own keys, as the Chip says: Space switches a filter chip — and, while selection is single or multiple, changes the group's selection (TGR-19); Backspace or Delete removes an input chip. After a removal, focus is on the chip that followed, or on the one before where none follows (TGR-9).

备注

  • Not interactive by itself: the tag group never takes focus itself, and while selection is none it handles no key (TGR-6), so it has no target of its own to size and minimumTarget holds through its chips, each of which extends its own hit area (CHP-5). What the group owes them is room: gap and row-gap decide how much of each extended hit area a chip keeps beside and between its neighbours (TGR-5).
  • Why a list, for tags and for chips alike: a list tells a person how many members there are and where they are among them, which is what a set of like things needs — four tags on an article, the third of nine filters. It holds static text, links and buttons equally, so one component needs no setting for its semantics.
  • Why not a group: a group says that controls belong together and nothing more. It is what a button group is, whose two to four members are different actions; a tag group's members are of one kind and any number.
  • Why not a list box, also while the group holds a selection: a list box is one control whose options hold the selection (aria-selected), and an option cannot hold a button — an input chip's remove action, or the chip's own button — inside it. React Aria's TagGroup exposes a selectable set as a grid, rows that carry aria-selected and the remove button in a cell; Tanrei's chip is a button whose pressed state already says whether it is on, which is how M3's filter chips and Carbon's selectable tags say it, so a selection keeps the list and each chip its pressed state. One choice among several that is not a filter is a radio group, tabs or a segmented button; a choice from a list is a select.
  • Why one stop with arrow keys only with a selection: while selection is none each chip is its own Tab stop, as every Button of a button group is (CHP-23), and a set of links or of entered items is crossed by Tab. A set that holds a selection is one control to the person who uses it, as a segmented button's segments and Material's chip set are: one stop, the arrow keys inside it (TGR-20), focus not wrapping, as in the chip field (CHF-13). The list stays, so a screen reader still says how many chips there are and where one is among them.
  • While selection is none the tag group does not make filter chips exclusive and holds no value: each filter chip is on or off by itself. With selection single the group puts one on at a time and a chip that is on can be switched off, leaving none, because each chip is a toggle; a choice that must always hold one value is a segmented button or a radio group. With multiple any number are on. The group names its chips by their value, as a segmented button names its segments, and its change carries value or values, as the segmented button's does (TGR-19).
  • After the last member is removed the group is gone and cannot hold focus. The product moves focus to what the set belonged to (the field that adds items, the button that opens the filters); otherwise the platform drops it. The docs page says so with an example.
  • Removing and selecting together: an input chip has no selected state (CHP-11), so in a set that holds a selection the entered items are reached by the arrow keys and removed by Backspace or Delete as before, and the filter chips are switched; the chip that receives focus after a removal becomes the set's stop (TGR-21). A filter chip that the product takes away leaves value and values as they were (TGR-17): the product, which took it away, changes them where it wants the filter gone.
  • A removal is not announced by the group. Where it must be (a filter removed far from the list it changes), the product reports it in a live region or with a snackbar.
  • A status among a set of tags never rests on color alone: each tag says it in words (TAG-11).
  • A small chip is for dense places where a set stands on one line. On wrapped lines small chips are 24 high and 16 apart under the default system, so their touch targets meet between the lines at 40; a set of small chips that wraps on a touch screen wants medium chips.
  • Text size: at 200% members grow and the set wraps onto more lines; nothing is cut by the group (TGR-12).
  • Compose and React Native have no list role for an arbitrary layout: the group is a traversal group whose content description is label and whose collection information gives the count (Compose), or a labelled container (React Native), and each member keeps its own role.

生成自 specs/tag-group.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 这一组作为一个列表暴露出来,由 label 命名。每个成员是其中的一项:Tag 作为它的文本朗读;纸片作为它的按钮或链接朗读,并带上状态;输入纸片旁边还有它的移除操作。
  • 列表会告诉用户一共有多少个成员,以及自己正在其中的哪一个:“列表,4 项”。
  • 成员按给出的顺序朗读和获得焦点,在从右到左的布局中也是如此。
  • 没有选择时,每个纸片在焦点顺序中各是一个停靠点,输入纸片也只是一个:Tab 会跳过它的移除操作,在纸片上按 Backspace 或 Delete 即可移除它。标签组不获得焦点,也不处理任何按键。
  • 有选择时,这一组是一个停靠点。方向键、Home 和 End 在它的纸片之间移动,每个筛选纸片仍然说出自己是否处于按下状态。标签组仍然是一个列表,所以屏幕阅读器仍然会说出一共有多少个纸片。
  • 获得焦点的纸片被移除之后,焦点会移到下一个纸片上,绝不会移到移除操作上。有选择时,那个纸片成为这一组的停靠点。
  • 空的标签组不暴露任何东西:没有空列表,也没有名称。
  • 文本放大到 200% 时,成员变大,并换到更多的行上。不截断任何内容,也不出现横向滚动。
  • 在强制颜色模式下,标签组和平时一样什么也不绘制。成员绘制各自的边线。
  • 在从右到左的布局中,每一行都从右侧开始。

它在 Web 上的构建方式:列表是标签组 shadow root 里的一个元素,包在插槽外面。成员在标签组里的时候,标签组会给每个成员写上 role="listitem",成员离开时再把它拿掉。自己已有 role 的成员,或者本身就有含义的原生元素(链接、按钮),会保留原有的角色,不算作列表项。请把这样的元素包在 span 里。

Web 上的已知差距:

  • 在元素定义之前,成员按顺序出现在页面上,彼此之间只隔着它们之间的空白,没有间距,也没有列表。想让页面从首次绘制起就有这个布局,请添加:

    tanrei-tag-group:not(:defined) {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: var(--tanrei-comp-tag-group-row-gap-chips) var(--tanrei-comp-tag-group-gap-chips);
    }

需要你做的事

  • 给标签组一个 label。组件不自带任何文字。
  • 给每个输入纸片一个说出其项目名称的 removeLabel。
  • 有选择时,给每个筛选纸片一个在这一组中唯一的 value。没有 value 的筛选纸片不参与选择,调试构建会报告这一点。
  • 纸片触发 remove 时移除它;最后一个也移除之后,自己移动焦点。
  • 用每个 Tag 自己的文字说出状态。只靠颜色永远说不出状态。