跳转到内容

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

设计指引

何时使用

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

何时不用

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

特性

  • 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 是文本、禁用与可移除的事实源;也可以逐个编写。
  • 连续输入按首字母跳转,只移动焦点、不改变选中值。

组合

  • 每一个标签就是标签本身:形态、语气、尺寸三轴写在组上,逐个落到每个标签的 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

属性类型必填说明
collectionTagGroupNode[]条目数据,显示文本、禁用与可移除的事实源。提供后条目部件只需声明 value。 未提供时回到文本与禁用都写在条目部件上的方式。
valuestring | string[]选中值,提供即受控;单选可写为裸串,内部归一为数组。
defaultValuestring | string[]
selectionModeTagGroupSelectionMode选择模式,默认 none。
deletableboolean是否提供移除按钮,默认 false。false 时该按钮同时被禁用与收起。
disabledboolean整组禁用:键盘与点击都不再修改选中值,也不可移除任何标签。
readOnlyboolean只读:仍可聚焦、可导航与朗读,但选中值不可修改、标签也不可移除。
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>

事件

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

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

插槽

仅列出带载荷的插槽。

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

状态

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

状态idle

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

判据canPress

connect API

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

成员类型说明
valuestring[]选中集合;单选模式下长度 ≤ 1。
collectionreadonly TagGroupNodeMeta[]由 collection 推导的条目元信息,按数据顺序排列;未提供 collection 时为空数组。
selectionModeTagGroupSelectionMode生效的选择模式。
focusedValuestring | null焦点锚点;焦点不在组内时为 null。
disabledboolean
readOnlyboolean
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-labellabel.list
listaria-labelledbylabel 部件的 id
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 允许。因此 listgrid、每个标签(标签root)承担 row、标签内的格是 gridcell;手写部件时 cell 这一层不能省略,使用 collection 时由组件铺开。

样式参考

皮肤

@xihan-ui/styles/tag-group.css 使用 [data-scope="tag-group"][data-part="root"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides

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

数据属性

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

部件属性
rootdata-disabled''(条件成立时才出现)
rootdata-orientationprops.orientation
rootdata-readonly''(条件成立时才出现)
labeldata-disabled''(条件成立时才出现)
listdata-disabled''(条件成立时才出现)
listdata-orientationprops.orientation
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-variant='solid'])
tone
variant=solid
--xh-_tone-subtle-hover
--xh-bg-subtle
tag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-bg-pressedlist
root
backgrounddisabled
is(:active, [data-pressed])
not([data-disabled])
not([data-variant='solid'])
pressed
selectable
tone
variant=solid
--xh-_tone-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-bg-selectedlist
root
backgrounddisabled
not([data-disabled])
not([data-variant='solid'])
selected
variant=solid
--xh-_tone-subtletag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-bg-selected-hoverlist
root
backgrounddisabled
highlighted
is(:hover, [data-highlighted])
not([data-disabled])
not([data-variant='solid'])
selected
variant=solid
--xh-_tone-subtle-hovertag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-bg-selected-pressedlist
root
backgrounddisabled
is(:active, [data-pressed])
not([data-disabled])
not([data-variant='solid'])
pressed
selectable
selected
variant=solid
--xh-_tone-subtle-activetag-group 的 list、root 部件 background 覆盖槽。
--xh-tag-group-item-border-selectedlist
root
border-colordisabled
not([data-disabled])
not([data-variant='solid'])
selected
variant=solid
currentColor
transparent
tag-group 的 list、root 部件 border-color 覆盖槽。
--xh-tag-group-item-fg-selectedlist
root
colordisabled
not([data-disabled])
not([data-variant='solid'])
selected
variant=solid
--xh-_tone-fgtag-group 的 list、root 部件 color 覆盖槽。
--xh-tag-group-item-indicator-fgitem-indicatorcolordefaultcurrentColortag-group 的 item-indicator 部件 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-1_5tag-group 的 list 部件 gap 覆盖槽。

动效

background · color · scaletransition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。

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

Released under The MIT License