Base UI CheckboxGroup 完全指南:类型 API、状态推导与受控/非受控多选实现
【免费下载链接】base-uiUnstyled UI components for building accessible web apps and design systems. From the creators of Radix, Floating UI, and Material UI.项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui
本文以 Base UI(Radix、Floating UI、Material UI 同源团队出品的 Unstyled 组件库)的
CheckboxGroup组件为对象,系统讲解其在当前仓库中的类型定义与 API 契约:包括 Props 全表、Data Attributes、State/ChangeEventDetails等派生类型、Canonical Types 映射关系,以及受控/非受控值管理、父级复选框联动(allValues+parent)、表单与校验集成等实战用法。读完本文,你将能基于 packages/react/src/checkbox-group 的源码证据,准确选用正确的 Props、正确消费事件与状态类型,并快速定位底层实现与测试用例。
从官方文档到仓库实现:CheckboxGroup 是什么
Base UI 的官方组件文档位于 docs/src/app/(docs)/react/components/checkbox-group/page.mdx/react/components/checkbox-group/page.mdx),其中对CheckboxGroup的定义是:为一组复选框(checkboxes)提供共享状态(Provides shared state to a series of checkboxes)。与同一仓库中的 Checkbox 组件配套使用——CheckboxGroup负责维护"哪些值被选中"的公共状态,而每一个Checkbox.Root负责单个复选项的渲染与交互。
本篇文章对应的关联文档是 types.md/react/components/checkbox-group/types.md)。它是通过docs/src/utils/createTypes.ts的createTypes(import.meta.url, CheckboxGroup)(见 types.ts/react/components/checkbox-group/types.ts))从源码自动生成的类型/API 参考页,文档头部也标注了"Autogenerated"并提示通过pnpm docs:validate重新生成。因此,本文的核心论据全部指向真实的 TypeScript 类型与实现:
- 组件实现:CheckboxGroup.tsx
- 上下文(共享状态如何传递给子复选框):CheckboxGroupContext.ts
- 父子联动逻辑:
useCheckboxGroupParent.ts - 数据属性常量:CheckboxGroupDataAttributes.ts
- 出口(public API):index.ts
阅读下文时,你可以随时打开上述文件核对每个类型与文档描述的一一对应关系。
CheckboxGroup Props 全表与类型契约
CheckboxGroup通过forwardRef暴露,根元素为<div>,并带有role="group"与aria-labelledby(指向标签元素的 id),源码见 CheckboxGroup.tsx 第 161-174 行。其 Props 完整定义在 CheckboxGroupProps,继承自BaseUIComponentProps<'div', CheckboxGroupState>。下表与文档 types.md/react/components/checkbox-group/types.md) 保持一致,并补充了源码注释中的说明:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
defaultValue | string[] | -(省略时视为空数组EMPTY_ARRAY) | 组内初始应处于勾选状态的复选框名称。要渲染受控组,请改用value。 |
value | string[] | - | 组内应处于勾选状态的复选框名称。要渲染非受控组,请改用defaultValue。 |
onValueChange | (value: string[], eventDetails: CheckboxGroup.ChangeEventDetails) => void | - | 组内任一复选框被勾选/取消勾选时触发,新值作为第一个参数传入。 |
allValues | string[] | - | 组内所有复选框的名称列表,创建父级复选框(parent checkbox)时必填。 |
disabled | boolean | false | 是否忽略用户交互(禁用整个组)。 |
className | string \| ((state: CheckboxGroup.State) => string \| undefined) | - | 应用到根元素的 CSS 类,或根据组件状态返回类的函数。 |
style | React.CSSProperties \| ((state: CheckboxGroup.State) => React.CSSProperties \| undefined) | - | 应用到根元素的样式,或根据状态返回样式对象的函数。 |
render | ReactElement \| ((props: HTMLProps, state: CheckboxGroup.State) => ReactElement) | - | 将根元素替换为其他标签或与其他组件组合(如Fieldset.Root render={<CheckboxGroup />})。 |
关于默认值的两个细节来自源码实现(CheckboxGroup.tsx 第 60-67 行):
defaultValue省略时,内部会退化为空数组defaultValueProp ?? EMPTY_ARRAY;value与defaultValue通过useControlled(来自@base-ui/utils/useControlled)统一管理受控/非受控状态:传入value时组件完全受控,不传时以defaultValue作为初始值进入非受控模式。
关于value数组语义的几个要点
从useCheckboxGroupParent.ts的实现与CheckboxGroup.test.tsx的测试可以确认value数组的语义:
- 元素是复选框的
value/name字符串,不是索引或 React key。官方示例使用['fuji-apple', 'gala-apple']这类业务值。 - 顺序即点击顺序:测试断言
onValueChange收到的数组依次为['red']→['red', 'green']→['red', 'green', 'blue'],取消勾选后为['green'](见 CheckboxGroup.test.tsx 的prop: onValueChange分组)。 - 支持空字符串值:测试
supports an empty string item value验证了value=""的复选项可以被正确勾选/取消。 - 受控值变为
undefined时按空数组处理:测试treats a controlled value that becomes undefined as an empty array说明即使受控值被外部清空为undefined,组件也不会崩溃。 defaultValue={null}被当作空数组(测试treats null as an empty array),这是对 JavaScript 消费方传入非预期值时的宽容处理。
Data Attributes:无样式组件的状态钩子
CheckboxGroup暴露的根元素数据属性只有一个,定义于 CheckboxGroupDataAttributes.ts:
| Attribute | 类型 | 说明 |
|---|---|---|
data-disabled | - | 当复选框组被禁用时出现。 |
由于 Base UI 是 Unstyled 组件,data-disabled是 CSS 选择状态的首选钩子,例如:
[data-disabled] { opacity: 0.6; pointer-events: none; }另外,虽然类型文档只列出了data-disabled,但组件内部经由stateAttributesMapping: fieldValidityMapping(CheckboxGroup.tsx 第 173 行)还会根据 Field 状态输出表单相关的状态属性。这一点在CheckboxGroup.test.tsx的Field分组中有明确验证:
[data-dirty]:组值与初始值不一致时出现,再次还原后消失;[data-filled]:只要组值非空即出现,即使没有渲染出对应的复选框(测试[data-filled] follows the group value even without a matching rendered checkbox);- 校验错误时子复选框会出现
aria-invalid。
派生类型:State、ChangeEventDetails 与 ChangeEventReason
CheckboxGroup.State
文档 types.md/react/components/checkbox-group/types.md) 给出了完整定义,其 TypeScript 源码在 CheckboxGroupState,继承自FieldRootState:
type CheckboxGroupState = { /** Whether the component should ignore user interaction. */ disabled: boolean; /** Whether the field has been touched. */ touched: boolean; /** Whether the field value has changed from its initial value. */ dirty: boolean; /** Whether the field is valid. */ valid: boolean | null; /** Whether the field has a value. */ filled: boolean; /** Whether the field is focused. */ focused: boolean; };disabled由fieldDisabled || disabledProp合并而来(CheckboxGroup.tsx 第 59 行),其余字段来自Field上下文。该 State 被用于三类场景:
className/style的函数式形式,例如className={(state) => state.filled ? 'is-filled' : ''};render回调的第二个参数state;- 与
CheckboxGroup.State命名空间类型对应(见下文 Canonical Types)。
CheckboxGroup.ChangeEventReason
type CheckboxGroupChangeEventReason = 'none';这是 Base UI 统一的BaseUIEventReasons['none'](来自internals/reasons),表示目前CheckboxGroup的变更事件没有细分触发原因,统一为'none'。子组件(如父级复选框的onCheckedChange)同样复用该 reason。
CheckboxGroup.ChangeEventDetails
type CheckboxGroupChangeEventDetails = { /** The reason for the event. */ reason: 'none'; /** The native event associated with the custom event. */ event: Event; /** Cancels Base UI from handling the event. */ cancel: () => void; /** Allows the event to propagate in cases where Base UI will stop the propagation. */ allowPropagation: () => void; /** Indicates whether the event has been canceled. */ isCanceled: boolean; /** Indicates whether the event is allowed to propagate. */ isPropagationAllowed: boolean; /** The element that triggered the event, if applicable. */ trigger: Element | undefined; };ChangeEventDetails是 Base UI 的BaseUIChangeEventDetails泛型实例化(CheckboxGroup.tsx 第 219-220 行)。其中cancel()是重点:在onValueChange中调用eventDetails.cancel()可以阻止组状态更新。源码中setValue包装器(CheckboxGroup.tsx 第 69-79 行)先调用onValueChange,再检查isCanceled决定是否写入新值;测试does not update the group when onValueChange cancels the event验证了:即使onValueChange收到['red'],由于调用了cancel(),所有复选框的aria-checked仍然保持false。这为"先确认再改状态"的业务场景(如配额校验、二次确认)提供了官方机制。
Canonical Types:命名空间别名与使用建议
文档 types.md/react/components/checkbox-group/types.md) 的 "Canonical Types" 一节给出了类型别名映射表,其来源是源码末尾的namespace CheckboxGroup声明(CheckboxGroup.tsx 第 222-227 行):
| Canonical | Alias | 使用建议 |
|---|---|---|
CheckboxGroup.State | CheckboxGroupState | 已导入CheckboxGroup命名空间时用左侧;否则用右侧 |
CheckboxGroup.Props | CheckboxGroupProps | 同上 |
CheckboxGroup.ChangeEventReason | CheckboxGroupChangeEventReason | 同上 |
CheckboxGroup.ChangeEventDetails | CheckboxGroupChangeEventDetails | 同上 |
// 方式一:使用命名空间类型(已 import CheckboxGroup) function handleChange(value: string[], details: CheckboxGroup.ChangeEventDetails) { // ... } // 方式二:使用独立别名 import type { CheckboxGroupChangeEventDetails } from '@base-ui/react/checkbox-group';CheckboxGroup.Props是对CheckboxGroupProps的 Re-export,两者完全等价。这是 Base UI 的整体风格:组件在运行时是值(value),同时作为命名空间承载配套类型。
上下文与共享状态机制
CheckboxGroup之所以能让一组复选框共享状态,是因为它通过CheckboxGroupContext.Provider向下传递上下文(CheckboxGroup.tsx 第 176-178 行)。上下文结构定义在 CheckboxGroupContext.ts:
interface CheckboxGroupContext { value: string[]; setValue: (value: string[], eventDetails: BaseUIChangeEventDetails<...>) => void; allValues: string[] | undefined; parent: UseCheckboxGroupParentReturnValue; disabled: boolean; validation: UseFieldValidationReturnValue; registerControlId: LabelableContext['registerControlId']; }其中:
setValue是经useStableCallback包装的稳定回调,内部先触发onValueChange、再按isCanceled决定是否提交(见上文"事件详情"部分);parent是useCheckboxGroupParent的返回值,包含getParentProps/getChildProps/registerChildId/disabledStatesRef,专门服务于"父级复选框"场景;validation来自Field根组件,使CheckboxGroup可以直接参与字段校验(见下文"表单与校验");registerControlId用于标签作用域:注释明确指出,复选框如果看到同一个registerControlId,就与组共享标签作用域,组本身才是字段的控件,而不是组内某个任意复选框。
父级复选框:allValues + parent 的联动原理
官方三步用法
文档 page.mdx/react/components/checkbox-group/page.mdx) 的 "Parent checkbox" 一节给出了创建"全选/取消全选"父级复选框的步骤:
- 让
<CheckboxGroup>成为受控组件(传入value与onValueChange); - 把组内所有子复选框的值数组传给
allValues; - 在父级
<Checkbox.Root>上加parent布尔 prop。
当部分(而非全部)子复选框被勾选时,组会控制父级复选框的indeterminate状态。完整可运行示例见 demos/parent/css-modules/index.tsx/react/components/checkbox-group/demos/parent/css-modules/index.tsx)(三选一的全选示例,用state.indeterminate渲染横线图标):
const fruits = ['fuji-apple', 'gala-apple', 'granny-smith-apple']; <CheckboxGroup value={value} onValueChange={setValue} allValues={fruits} aria-labelledby={id} > <label id={id}> <Checkbox.Root parent> <Checkbox.Indicator render={(props, state) => ( <span {...props}>{state.indeterminate ? <HorizontalRuleIcon /> : <CheckIcon />}</span> )} /> </Checkbox.Root> Apples </label> {/* 子项:value="fuji-apple" / "gala-apple" / "granny-smith-apple" */} </CheckboxGroup>底层实现:useCheckboxGroupParent
父子联动的核心逻辑在 useCheckboxGroupParent.ts:
- 状态机:
status取'on' | 'off' | 'mixed'三态。checked = value.length === allValues.length,indeterminate = value.length !== allValues.length && value.length > 0。当父级在mixed态被点击时,下一状态为'on'(全选);'on'时点击则变为'off'(全不选)——测试preserves initial state if mixed when parent is clicked与does not advance the parent toggle cycle when the group cancels a parent change详细验证了这一循环,后者还确认:如果组的onValueChange调用了cancel(),内部状态不会推进到下一个状态,再次点击会重试同一个mixed → on转换。 - 禁用项处理:
getParentProps中的onCheckedChange会通过disabledStatesRef过滤禁用项。未勾选的禁用项不会被全选;已勾选的禁用项在全不选时保持勾选。测试handles unchecked disabled checkboxes/handles checked disabled checkboxes分别覆盖了这两种情况。 - 可访问性:父级复选框通过
registerChildId收集每个子复选项真实渲染出的id,组装成空格分隔的aria-controls(测试should apply space-separated aria-controls attribute with child names)。子项卸载时对应 id 会被清理(drops an unmounted child from aria-controls)。注册表使用Map而非普通对象,规避了constructor这类值名带来的原型链读取风险(测试does not read aria-controls ids off Object.prototype)。 - 取消传播:父级或子级复选框都可以通过自己的
onCheckedChange调用eventDetails.cancel()来阻止组变更(测试lets a parent checkbox cancel a parent-enabled group change/lets a child checkbox cancel...)。
嵌套父级复选框
嵌套场景(如"权限组"包含"子权限组")的完整示例见 demos/nested/css-modules/index.tsx/react/components/checkbox-group/demos/nested/css-modules/index.tsx):外层CheckboxGroup以mainPermissions为allValues,内层以userManagementPermissions为allValues;勾选外层"Manage Users"时联动填充内层全部子项,取消时清空内层;内层任一变化又会反向更新外层manage-users的勾选状态。这是一个典型的"级联勾选"模式:
<CheckboxGroup value={mainValue} onValueChange={...} allValues={mainPermissions}> {/* 外层父项:User Permissions(parent) */} <CheckboxGroup value={managementValue} onValueChange={...} allValues={userManagementPermissions}> {/* 内层父项:Manage Users(parent),以及 create-user / edit-user / ... */} </CheckboxGroup> </CheckboxGroup>注意内层父项还可以显式传入indeterminate来增强半选态表现(源码第 35-38 行)。
表单集成与校验:与 Field / Fieldset / Form 协同
文档 page.mdx/react/components/checkbox-group/page.mdx) 的 "Form integration" 一节展示了把CheckboxGroup渲染为Fieldset的推荐写法——通过render将CheckboxGroup与Fieldset.Root组合,每个复选项包在Field.Item+Field.Label中:
<Form> <Field.Root name="allowedNetworkProtocols"> <Fieldset.Root render={<CheckboxGroup />}> <Fieldset.Legend>Allowed network protocols</Fieldset.Legend> <Field.Item> <Field.Label> <Checkbox.Root value="http" /> HTTP </Field.Label> </Field.Item> {/* https / ssh 同理 */} </Fieldset.Root> </Field.Root> </Form>从源码看,CheckboxGroup与表单深度集成(CheckboxGroup.tsx 第 103-141 行):
getFormValue遍历validation.registeredInputs,只把已勾选且可提交的输入值过滤出来,作为提交给表单的值;useRegisterFieldControl将组注册为字段控件(fieldName存在且未禁用时才注册);- 值变化时通过
useValueChanged触发:清除对应字段错误(clearErrors)、比对初始值计算dirty、调用validation.change触发校验。
CheckboxGroup.test.tsx的Field分组对校验行为有非常细的测试覆盖,包括:
- required 语义:组内多个复选框都是
required时,只勾选其中一个不能消除valueMissing错误,必须全部勾选(keeps a required error while another required checkbox in the group is unchecked); - 禁用项豁免:禁用且
required的复选项不参与约束校验(ignores a disabled required checkbox when validating the group); - 卸载后的校验:已勾选的复选项卸载后,组仍会基于注册表继续校验剩余必需项(
keeps validating the remaining required checkbox after a checked sibling unmounts); - 三种校验模式:
validationMode="onChange"/"onBlur"/"onSubmit"均有对应测试,验证自定义validate函数收到的值始终是组的完整string[]; - 外部受控变更也触发重校验(
revalidates when the controlled value changes externally)。
可访问性要点:命名与标签
文档 page.mdx/react/components/checkbox-group/page.mdx) 的 "Usage guidelines" 强调:表单控件必须有可访问名称(accessible name)。CheckboxGroup的三种命名方式:
aria-labelledby+ 兄弟标签:<div id="protocols-label">Allowed network protocols</div>配合<CheckboxGroup aria-labelledby="protocols-label">;- 包裹式
<label>:每个复选项用<label><Checkbox.Root value="http" /> HTTP</label>包裹——默认情况下Checkbox.Root渲染为<span>,因此支持包裹标签; - Field / Fieldset 组件:见上文"表单集成"一节。
文档还特别说明了一个细节("Rendering as a native button"):默认Checkbox.Root渲染<span>以支持包裹式 label;当改用兄弟标签(htmlFor/id)时,应把每个复选框渲染为原生<button>(nativeButton+render={<button />}),因为原生按钮 + 兄弟标签是无障碍语义更好的组合。若想用原生按钮又保留包裹式<label>,则用render回调把<button>放进<label>中,避免无效 HTML(隐藏 input 会放到 label 外部)。
源码层面,CheckboxGroup根元素自带role="group"与aria-labelledby,而useLabelableId({ id: null })(CheckboxGroup.tsx 第 89 行)确保Field.Label的htmlFor不会错误地指向组内某个任意复选框——组本身才是字段控件。Field.Label分组测试验证了这一点:hydration 后 label 的for属性被移除,aria-labelledby指向 label 的 id(labels the group rather than pointing Field.Label at one checkbox inside it),并保证共享 Field.Root 时各复选项 id 唯一(keeps checkbox ids unique when the group shares one Field.Root)。
结语:从类型契约到工程实践的完整闭环
CheckboxGroup是 Base UI 中"小而专"的组件:类型层面,它以CheckboxGroup.Props / .State / .ChangeEventDetails命名空间清晰地定义了 API 契约;实现层面,它用useControlled统一受控/非受控、用CheckboxGroupContext分发共享状态、用useCheckboxGroupParent支撑全选/半选/级联联动、用 Field 体系打通校验与表单提交;测试层面,CheckboxGroup.test.tsx 与 useCheckboxGroupParent.test.tsx 覆盖了取消传播、禁用项、卸载后校验、SSR 唯一 id 等边界场景。
当你需要在自己的应用中实现"多选 + 全选/半选 + 表单校验"的复选项组时,可以直接参考 demos/parent/css-modules/index.tsx/react/components/checkbox-group/demos/parent/css-modules/index.tsx)(全选联动)与 demos/nested/css-modules/index.tsx/react/components/checkbox-group/demos/nested/css-modules/index.tsx)(嵌套权限级联),再对照本文的 API 表选择正确的 Props 与类型,即可避免大多数类型与状态同步上的常见坑。
【免费下载链接】base-uiUnstyled UI components for building accessible web apps and design systems. From the creators of Radix, Floating UI, and Material UI.项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考