一组相关的 Toggle,共享同一个值。组合就是全部 API:
ToggleGroup 里放若干 ToggleGroupItem,每一项的 value 就是它在组里的身份。
value 永远是数组,单选也不例外 —— multiple(默认 false)决定的只是
「同时能有几项在里面」,不是值的形状。所以单选时 defaultValue={['start']},
拿到的回调参数是 ['center'] 这样的单元素数组;再点一次已经按下的那项会把它松开,
数组变成 [](一项都不选是合法状态)。
根元素是 role="group",所以 aria-label 必给 —— 它是这组按钮的无障碍名;
组里每一项仍然各是一个 <button aria-pressed>,只有图标时照旧各自补 aria-label。
使用
import { ToggleGroup, ToggleGroupItem } from '@gedatou/cadenza-ui'<ToggleGroup aria-label="对齐方式" defaultValue={['start']}>
<ToggleGroupItem aria-label="左对齐" value="start"><IconAlignLeft /></ToggleGroupItem>
<ToggleGroupItem aria-label="居中" value="center"><IconAlignCenter /></ToggleGroupItem>
</ToggleGroup>组成
两个部件,没有第三个:
ToggleGroup
├── ToggleGroupItem
└── ToggleGroupItem组内请用 ToggleGroupItem 而不是 Toggle:后者也读得到组的 context、功能上照样切换,
但它拿不到分段、间距、圆角合并那一整组样式。
变体与尺寸
variant 和 size 写在组上,经 context 发给每一项,这样整条控件不会长短不一。
优先级是最普通的那种:项 → 组 → 库的默认值。组上设一次,整条控件就统一了;
个别项要与众不同,在那一项上写就行,会盖过组。
(转正之前的 vendored 版本是反的:它读 context.size || size,而项自己的 size 有默认值
'default',所以组一旦设了那个旋钮,项就再也盖不过去。封装层把项的默认值撤到解析那一步,
才换回正常的优先级。)
两档 variant(default / outline)和三档 size 长什么样,见
Toggle 的变体与尺寸—— 同一套 toggleVariants,
组只负责把它下发下去。
间距
spacing 是项与项之间的间距,单位是 spacing 单位(默认 2,即 gap-2)。
0 是一个开关而不只是「间距为零」:它同时把 data-spacing="0" 写到组和每一项上,
于是中间项去掉圆角、首尾各补回外侧那一边的圆角,outline 变体里相邻的两条边框合并成
一条 —— 一条真正的分段控件。焦点态自带 z-10,焦点环不会被隔壁那项压住。
间距本身是靠一个 --gap CSS 变量下发的,封装层把它合并进你传的 style(函数形态
也认),而不是整份替换 —— 否则组上写一个 style 就会把 gap 抹平,data-spacing 却还
报着旧值。
方向
orientation="vertical" 时组竖排,方向键同时换成上下 —— 这一条现在是一件事,
以前是两件:
vendored 那版自己声明了一个
orientationprop,拿它画 flex 方向、也自己写data-orientation,却从不把它传给 Base UI。于是 Base UI 的 composite 一直按 默认的横向导航:一个看着是竖的组,方向键还是走 ← / →,↑ / ↓ 什么都不做。 封装层这里是自建组合而不是转出,orientation直接交给 Base UI,data-orientation由它按自己的 state 写,布局类读的正是同一个属性 —— 视觉和键盘不可能再对不上。
完整按键表见键盘交互。
多选
multiple 打开后同时可以按下多项,数组按点击顺序增删。值的形状不变 —— 还是那个数组,
只是里面同时能有几项变了。上面这个 demo 顺带演示了受控写法。
受控
受控三件套是 value / defaultValue / onValueChange。回调第二参是真的
ChangeEventDetails(导出名 ToggleGroupChangeEventDetails):reason 恒为
'none',cancel() 认数 —— 调了它,组内部那份值就不推进。
值的类型是 Value extends string,只能是字符串(不像
RadioGroup 还收 number)。受控时 Value 直接从你的
state 推断出来:
type Mark = 'bold' | 'italic' | 'underline'
const [marks, setMarks] = useState<Mark[]>(['bold'])
// value 收 Mark[],onValueChange 交回来的也是 Mark[]只是别指望它去校验每一项:ToggleGroupItem 的 value 是各自推断的,组的泛型管不到
它们 —— 拼错一个字母 TypeScript 不会拦,回调里就多出一个业务代码不认识的值。
什么时候用 ToggleGroup
单选的 ToggleGroup 语义上依然是一组按钮(role="group" + 各自的 aria-pressed),
不是 radiogroup:它表达「当前生效的是哪个模式」,而且允许一个都不选。要表达「这道题
必须选一个、而且要跟着表单提交」,用 RadioGroup。
几个 toggle 彼此无关(加粗、显示网格线、静音)时不必组队 —— 各写各的 Toggle 就好;组的意义是共享一个值和一次 Tab 停留。
状态与 className
className 在这里函数形态是有效的 —— 和 Toggle
不同。区别在路由:Toggle 把你的 className 交给 cva(内部是 clsx,遇函数静默丢掉),
而这两个部件只用 cva 取变体类,你的 className 是经本库的 cn 合并的 —— cn 遇到函数
会返回一个函数、等 state 到了再解析。所以 (state) => string 照常可用,
下面两张表里的 data-* 也照常可用,两条路都通。
ToggleGroup
ToggleGroupItem
根元素带 data-slot="toggle-group",每一项带 data-slot="toggle-group-item"。
ToggleGroupState(disabled / multiple / orientation)与 ToggleGroupItemState
就是两处函数形态 className 各自收到的那个参数。
键盘交互
方向键走到头会绕回另一端,这是 Base UI 的 loopFocus(默认 true),不想要就传
false。禁用的项在以上所有导航里都会被跳过 —— 它渲染的是原生 disabled,本来就拿
不到焦点。
Props
顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className。
ToggleGroup
ToggleGroupItem
ToggleGroupProps / ToggleGroupItemProps / ToggleGroupState /
ToggleGroupItemState / ToggleGroupChangeEventDetails 五个类型一并导出。