生态产品多个设计器现已发布候选版本

查看生态总览
跳转到内容

TagGroup 标签组 ​

把一排标签作为一个整体操作:方向键在标签之间移动,整组只占一个 Tab 停靠点,标签可以选中、也可以移除,移除后焦点有去处。

单个标签不接收焦点,它的关闭按钮是页面上一个独立的 Tab 停靠点:十个标签就是十个停靠点,键盘用户需要按十次才能越过。标签组把这十个收成一个。

用法 ​

一排可移除标签,每个都是库内的 tag:整组只占一个 Tab 位,方向键移动标签,Delete 或 Backspace 移除,关闭按钮就是 tag 的 close-trigger

技术栈
VueReactSvelteAngular

还剩:Vue、React、Svelte、Angular

组件结构 ​

加粗的是必需部件。

data-scope="tag-group":root · label · list · cell · item-indicator

示例 ​

可选中 ​

selectionMode 决定点击一个标签是替换还是加选;Ctrl/Cmd + A 全选

话题
设计无障碍动效已归档

已选:design

尺寸 ​

size 写在组上逐个落到每个标签上,使用 tag 的三档,标签自身不写档位

小档
VueReactSvelte
缺省档
VueReactSvelte
大档
VueReactSvelte

手写部件 ​

逐部件自行编写,标签中即可放置头像、计数等自带内容,移除按钮照常归 cell 管理;条目渲染为 tag 的 root、文字是 tag 的 label,产出的结构与只提供数据的一份完全一致,Tab 位与键盘也一样

协作成员
张三李四王五

已选:li

设计指引 ​

何时使用 ​

  • 一排可移除的标签:已生效的筛选条件、一条记录关联的若干分类。
  • 一排可选的标记:点击一个即筛选一次,或按住多选。
  • 键盘与读屏用户需要逐个遍历、逐个移除。

何时不用 ​

  • 只有一个标签且不接受交互时,直接使用标签。
  • 用户需要自行输入并累积多个值时,使用标签输入,它自带输入框与增删逻辑。
  • 选项很多、需要搜索时,使用选择器的多选或穿梭框。
  • 一组互斥选项需要用户选一个时,使用单选组,一行短选项可用它的 variant="segmented"。
  • 只是把一排标签排开、不接键盘时,用弹性布局包一层即可。

特性 ​

  • roving tabindex:整组一个 Tab 停靠点,组内使用方向键移动;Home / End 到端点。
  • selectionMode 三档:none 只是标记、single 单选、multiple 可多选(Ctrl / Cmd + A 全选)。
  • 每一个标签就是库内的标签:标签本体是它的 root,文字是它的 label,移除按钮是它的 close-trigger;组只在其上叠加行角色、Tab 停靠点、选中与锚点。
  • 选中的标签不换面、不换字色,只在文字后展示 item-indicator 选中标记(默认绘制对号,也可放入图标);未选中时该部件收起,不接选中时不出现。
  • deletable 显示移除按钮,键盘路径使用 Delete / Backspace。
  • 选择与移除是两个互斥动作:点击标签本体才选择,点击移除按钮只从选中集合移除并发出 item-delete,不会让同一次冒泡 click 把待删值重新选中。
  • 移除一个之后焦点交给前一个;前面没有则交给后一个,没有剩余时交给列表容器。
  • collection 是文本、禁用与可移除的事实源;也可以逐个编写。
  • 连续输入按首字母跳转,只移动焦点、不改变选中值。
  • invalid 时标签列表报 aria-invalid,根与列表带 data-invalid;required 在根与列表上带 data-required。grid 角色不收 aria-required,放进表单字段时必填由字段标题的星号与错误文案告知。
  • 不写结构时按标准结构铺出整套部件(Vue 不写默认插槽、React 不给 children、Web Components 不写部件或只写 root),三端出自同一个结构函数:label 给标题文字,接着是标签列表,collection 的每条数据一枚标签,格子里依次是文字、选中标记与移除按钮;没给 collection 时列表是空的,移除标签由宿主改 collection。Web Components 里标题与标签文字可以写在宿主的直接子节点 slot="label"、slot="item:<值>" 上接管。

组合 ​

  • 每一个标签就是标签本身:形态、语气、尺寸三轴写在组上,逐个落到每个标签的 root,外观全部由 tag.css 决定,--xh-tag-* 覆盖槽在组内同样生效。
  • 标签内的图元使用图标。
  • 直接放进表单字段即可:字段的标题并进标签列表的名字、说明与错误文案进描述链,读屏进列表时一起念出;点字段标题,焦点落到当前标签。

最佳实践 ​

  • 条目的去留由宿主决定:item-delete 只报告用户要移除哪一个,宿主从自己的数据中删除。撤销、二次确认、服务端失败回滚都只有宿主知道。
  • 移除之后提供回退路径,否则用户误点后无法恢复。
  • 标签文字尽量短,且首字母有区分度:连续输入按首字母跳转。
  • 不接选中时把 selectionMode 保持为 none:一排纯标记标签报告“未选中”是错误信息。

反模式 ​

  • 把整排标签铺成十个 Tab 停靠点:这正是本组件要解决的问题,不再逐个编写标签。
  • 移除后不处理焦点:被移除的标签带着焦点一起消失,焦点会回到页面开头。
  • 只用颜色表达含义:色觉障碍的用户无法分辨,文字本身要说明。

API 参考 ​

产物 ​

层值
自定义元素<xh-tag-group>
Vue 组件XhTagGroupCell XhTagGroupItem XhTagGroupItemDeleteTrigger XhTagGroupItemIndicator XhTagGroupItemText XhTagGroupLabel XhTagGroupList XhTagGroupRoot
组合式函数useTagGroup
状态机tagGroupMachine
皮肤@xihan-ui/styles/tag-group.css

Props ​

属性类型必填说明
labelledboolean作者渲染了 label 部件时置真,由适配器统计而不是判断标题文字是否有值。 为假时可聚焦部件不把 label 的 id 写进 aria-labelledby:指向未渲染的 id 是无效引用,作者写在控件上的 aria-label 也会被它压住。
ariaLabelstring不显示出来的名字,写成 aria-label 落在与字段标签同一个部件上。 放进字段(字段接线)或渲染了 label 部件时名字以它们为准,aria-labelledby 盖过它。
collectionTagGroupNode[]条目数据,显示文本、禁用与可移除的事实源。提供后条目部件只需声明 value。 未提供时回到文本与禁用都写在条目部件上的方式。
valuestring | string[]选中值,提供即受控;单选可写为裸串,内部归一为数组。
defaultValuestring | string[]
selectionModeTagGroupSelectionMode选择模式,默认 none。
deletableboolean是否提供移除按钮,默认 false。false 时该按钮同时被禁用与收起。
disabledboolean整组禁用:键盘与点击都不再修改选中值,也不可移除任何标签。
readOnlyboolean只读:仍可聚焦、可导航与朗读,但选中值不可修改、标签也不可移除。
invalidboolean校验失败:标签列表报告 aria-invalid,根与列表带 data-invalid。
requiredboolean必填:根与列表带 data-required。grid 角色不收 aria-required,必填由字段的标题与错误文案告知。
loopboolean方向键到达末尾是否回绕,默认 true。
dirDirection文字方向,默认 ltr。
orientationOrientation方向键轴向,默认 horizontal:标签成排出现。
typeaheadboolean连打检索,默认开启。
variantTagVariant形态:solid / subtle / outline,逐个写到每个标签(tag 的 root)上。
toneTone语气:brand / neutral / success / warning / danger / info,决定使用哪族颜色。
sizeSize尺寸:sm / md / lg,使用 tag 的三档。
onValueChange(details: TagGroupValueChangeDetails) => voidvalue 变化意图回调。
onItemDelete(details: TagGroupItemDeleteDetails) => void移除意图回调。条目由宿主的数据决定去留,组件只报告用户要移除该标签, 同时把它从选中集合中移除,并把焦点交给相邻的标签。
translationsPartial<TagGroupTranslations>

TagGroupNode ​

collection 的元素。

字段类型必填说明
valuestring是
labelstring展示文本,也是连打检索与移除按钮可访问名的取字来源;默认回退为 value。
disabledboolean条目禁用:方向键跳过它,但它仍可聚焦、仍是导航起点,也不可移除。
deletableboolean逐条覆盖可移除;未提供时跟随整组的 deletable。

事件 ​

自定义元素将载荷放在 detail;Vue 使用同名 emit。

事件载荷说明
value-changeTagGroupValueChangeDetails选中集合变化;detail 为 { value: string[] }
item-deleteTagGroupItemDeleteDetails用户要移除某个标签;detail 为 { value: string }

插槽 ​

仅列出带载荷的插槽。

Vue 组件插槽载荷说明
XhTagGroupRootdefaultTagGroupRootSlotProps
XhTagGroupRootlabel—
XhTagGroupRootitemTagGroupNodeMeta

React 适配器 props ​

只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。

React 组件属性类型必填说明
XhTagGroupItemvaluestring是
XhTagGroupItemdisabledboolean默认交给 connect 查询 collection,写死 false 会覆盖数据中的声明。
XhTagGroupItemdeletableboolean
XhTagGroupRootlabelReactNode标题内容。提供后不必再写 label 部件。
XhTagGroupRootrenderItem(node: TagGroupNodeMeta) => ReactNode每个条目的自定义内容;未提供时使用 collection 中的 label。
XhTagGroupRootchildrenSlotChildren<TagGroupRootSlotProps>

状态 ​

以下名称仅用于内部状态机。

状态:idle

事件:VALUE.SET · ITEM.SELECT · ITEM.TOGGLE · ITEM.FOCUS · ITEM.DELETE · LIST.BLUR · PRESS.START · PRESS.END · LIST.TRACKED

判据:canPress

connect API ​

getXxxProps() 返回对应部件的宿主属性。

成员类型说明
valuestring[]选中集合;单选模式下长度 ≤ 1。
collectionreadonly TagGroupNodeMeta[]由 collection 推导的条目元信息,按数据顺序排列;未提供 collection 时为空数组。
selectionModeTagGroupSelectionMode生效的选择模式。
focusedValuestring | null焦点锚点;焦点不在组内时为 null。
disabledboolean
readOnlyboolean
invalidboolean
requiredboolean
deletableboolean整组是否提供移除按钮。
isSelected(value: string) => boolean
setValue(next: string[]) => void
select(value: string) => void只保留该条目;加选使用 toggle。
toggle(value: string) => void
deleteItem(value: string) => void移除一个标签。程序化入口,不移动焦点。
getRootProps() => T['element']
getLabelProps() => T['element']
getListProps() => T['element']
getItemProps(props: TagGroupItemProps) => T['element']一个标签:库内 tag 的 root(data-scope="tag"),三轴与置灰由 tag 提供; row 角色、身份、roving tabindex、选中(data-selected)与锚点(data-highlighted)叠加在它上面。
getCellProps(props: TagGroupItemProps) => T['element']标签内的格子;移除按钮必须落在它之内。
getItemIndicatorProps(props: TagGroupItemProps) => T['element']选中标记:落在格子内、文字之后与摘除钮之前,选中时展示、未选中时以 hidden 收起; 对读屏隐藏,选中态由标签上的 aria-selected 表达。内容留空时由皮肤绘制对号,也可放入图标。
getItemTextProps(props: TagGroupItemProps) => T['element']标签文字:tag 的 label,截断规则挂在该层。
getItemDeleteTriggerProps(props: TagGroupItemProps) => T['button']移除按钮:所在标签那份 tag 的 close-trigger,不占 Tab 位;可及名、禁用与收起都由 tag 提供。

无障碍 ​

键盘 ​

规格出处:W3C APG

按键生效条件行为
Tab / Shift+Tabfocus outside the group整组只占一个 Tab 位:焦点进入锚点标签,无锚点时先落到列表容器再由它转移;每个标签的移除按钮一律不占停靠点
ArrowRightfocus in group, orientation=horizontal焦点移到下一个可停留标签(禁用项跳过、尽头按 loop 回绕);orientation=vertical 时改由 ArrowDown 承担,dir=rtl 再对调左右
ArrowLeftfocus in group, orientation=horizontal焦点移到上一个可停留标签(禁用项跳过、尽头按 loop 回绕);orientation=vertical 时改由 ArrowUp 承担,dir=rtl 再对调左右
Homefocus in group焦点移到首枚可停留标签
Endfocus in group焦点移到末枚可停留标签
Enter / Spacefocus on item, selectionMode=single 且可改只选中焦点标签,替换原有选中;标签禁用或整组只读则不认
Enter / Spacefocus on item, selectionMode=multiple 且可改切换焦点标签的选中态,其余选中不动
Ctrl+A / Cmd+Afocus in group, selectionMode=multiple 且可改选中全部可选标签;已经全选则把它们一并取消(禁用但已选中的不动)
Delete / Backspacefocus on item, 该标签可移除且可修改移除焦点标签,并把焦点交给前一个;前面没有则交给后一个,全部移除后交给列表容器
单个可打印字符focus in group, typeahead 未关连打检索把焦点移到首字母匹配的标签,不改选中值

ARIA ​

以下属性由 connect 生成。

部件属性值
listaria-disabled'true' | 'false'
listaria-invalid'true' | 'false'
listaria-labelprops.ariaLabel
listaria-labelledbylabel 部件的 id | undefined
listaria-multiselectable'true' | 'false'
listaria-readonly'true' | 'false'
listrole'grid'
cellrole'gridcell'
item-indicatoraria-hidden'true'
itemaria-disabled'true' | 'false'
itemaria-selected'true' | 'false' | undefined
itemrole'row'

一排可移除的标签是“集合 + 每条自带动作”,在 ARIA 中只有表格语义能容纳:可聚焦的移除按钮不允许位于 option 等控件角色内,gridcell 允许。因此 list 是 grid、每个标签(标签的 root)承担 row、标签内的格是 gridcell;手写部件时 cell 这一层不能省略,使用 collection 时由组件铺开。

样式参考 ​

皮肤 ​

@xihan-ui/styles/tag-group.css 按 [data-scope="tag-group"][data-part="root"] 部件选择器书写,发布产物以挂载类 .xh-scope-tag-group 代替其中的 data-scope(特异性相同),位于 xihan.components 层。覆盖样式使用 xihan.overrides,部件选择器照常可用。

forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。

数据属性 ​

由 connect 生成;条件不成立时不输出无值属性。

部件属性值
rootdata-disabled''(条件成立时才出现)
rootdata-invalid''(条件成立时才出现)
rootdata-orientationprops.orientation
rootdata-readonly''(条件成立时才出现)
rootdata-required''(条件成立时才出现)
labeldata-disabled''(条件成立时才出现)
listdata-disabled''(条件成立时才出现)
listdata-instant''(条件成立时才出现)
listdata-invalid''(条件成立时才出现)
listdata-orientationprops.orientation
listdata-required''(条件成立时才出现)
celldata-disabled''(条件成立时才出现)
celldata-highlighted''(条件成立时才出现)
celldata-selected''(条件成立时才出现)
item-indicatordata-disabled''(条件成立时才出现)
item-indicatordata-highlighted''(条件成立时才出现)
item-indicatordata-selected''(条件成立时才出现)
itemdata-deletable''(条件成立时才出现)
itemdata-disabled''(条件成立时才出现)
itemdata-highlighted''(条件成立时才出现)
itemdata-selectable''(条件成立时才出现)
itemdata-selected''(条件成立时才出现)

CSS 变量 ​

本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。

变量部件CSS 属性状态默认来源说明
--xh-tag-group-gaprootgapdefault--xh-space-2tag-group 的 root 部件 gap 覆盖槽。
--xh-tag-group-item-bg-hoverlist
root
backgrounddisabled
highlighted
is(:hover, [data-highlighted])
not([data-disabled])
not([data-tone])
not([data-variant='solid'])
tone
variant=ghost
variant=solid
--xh-_tone-subtle-hover
--xh-bg-subtle
--xh-bg-subtle-hover
tag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-bg-pressedlist
root
backgrounddisabled
is(:active, [data-pressed])
not([data-disabled])
not([data-tone])
not([data-variant='solid'])
pressed
selectable
tone
variant=ghost
variant=solid
--xh-_tone-subtle-active
--xh-bg-subtle-active
--xh-bg-subtle-hover
tag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-bg-pressed-solidlist
root
backgrounddisabled
is(:active, [data-pressed])
not([data-disabled])
pressed
selectable
tone
variant=solid
--xh-_tone-active
--xh-bg-brand-active
tag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-border-invalidlist
root
--xh-tag-borderinvalid--xh-border-invalidtag-group 的 list、root 部件 --xh-tag-border 覆盖槽。
--xh-tag-group-item-indicator-fgitem-indicator
root
colordefault
variant=solid
--xh-_tone-fg
currentColor
tag-group 的 item-indicator、root 部件 color 覆盖槽。
--xh-tag-group-item-indicator-sizeitem-indicator--xh-icon-sizedefault--xh-glyph-size-texttag-group 的 item-indicator 部件 --xh-icon-size 覆盖槽。
--xh-tag-group-label-fglabelcolordefault--xh-fg-mutedtag-group 的 label 部件 color 覆盖槽。
--xh-tag-group-label-font-sizelabelfont-sizedefault--xh-text-label-sizetag-group 的 label 部件 font-size 覆盖槽。
--xh-tag-group-label-font-weightlabelfont-weightdefault--xh-text-label-weighttag-group 的 label 部件 font-weight 覆盖槽。
--xh-tag-group-list-gaplistgapdefault--xh-space-2tag-group 的 list 部件 gap 覆盖槽。

动效 ​

动效角色:按压 · 状态 · 指示与换位 · 出现 · 列表(见动效规范)。

共享关键帧 xh-fade-out · xh-pop-in 由 family/motion.css 提供,皮肤 @import 它,单独引入仍成立;background-color · color · scale · translate 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。

系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。

Released under The MIT License