Base UI CheckboxGroup 完全指南:类型 API、状态推导与受控/非受控多选实现
2026/9/15 18:51:04 网站建设 项目流程

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.tscreateTypes(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类型默认值说明
defaultValuestring[]-(省略时视为空数组EMPTY_ARRAY组内初始应处于勾选状态的复选框名称。要渲染受控组,请改用value
valuestring[]-组内应处于勾选状态的复选框名称。要渲染非受控组,请改用defaultValue
onValueChange(value: string[], eventDetails: CheckboxGroup.ChangeEventDetails) => void-组内任一复选框被勾选/取消勾选时触发,新值作为第一个参数传入。
allValuesstring[]-组内所有复选框的名称列表,创建父级复选框(parent checkbox)时必填
disabledbooleanfalse是否忽略用户交互(禁用整个组)。
classNamestring \| ((state: CheckboxGroup.State) => string \| undefined)-应用到根元素的 CSS 类,或根据组件状态返回类的函数。
styleReact.CSSProperties \| ((state: CheckboxGroup.State) => React.CSSProperties \| undefined)-应用到根元素的样式,或根据状态返回样式对象的函数。
renderReactElement \| ((props: HTMLProps, state: CheckboxGroup.State) => ReactElement)-将根元素替换为其他标签或与其他组件组合(如Fieldset.Root render={<CheckboxGroup />})。

关于默认值的两个细节来自源码实现(CheckboxGroup.tsx 第 60-67 行):

  • defaultValue省略时,内部会退化为空数组defaultValueProp ?? EMPTY_ARRAY
  • valuedefaultValue通过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.tsxField分组中有明确验证:

  • [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; };

disabledfieldDisabled || disabledProp合并而来(CheckboxGroup.tsx 第 59 行),其余字段来自Field上下文。该 State 被用于三类场景:

  1. className/style的函数式形式,例如className={(state) => state.filled ? 'is-filled' : ''}
  2. render回调的第二个参数state
  3. 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 行):

CanonicalAlias使用建议
CheckboxGroup.StateCheckboxGroupState已导入CheckboxGroup命名空间时用左侧;否则用右侧
CheckboxGroup.PropsCheckboxGroupProps同上
CheckboxGroup.ChangeEventReasonCheckboxGroupChangeEventReason同上
CheckboxGroup.ChangeEventDetailsCheckboxGroupChangeEventDetails同上
// 方式一:使用命名空间类型(已 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决定是否提交(见上文"事件详情"部分);
  • parentuseCheckboxGroupParent的返回值,包含getParentProps/getChildProps/registerChildId/disabledStatesRef,专门服务于"父级复选框"场景;
  • validation来自Field根组件,使CheckboxGroup可以直接参与字段校验(见下文"表单与校验");
  • registerControlId用于标签作用域:注释明确指出,复选框如果看到同一个registerControlId,就与组共享标签作用域,组本身才是字段的控件,而不是组内某个任意复选框。

父级复选框:allValues + parent 的联动原理

官方三步用法

文档 page.mdx/react/components/checkbox-group/page.mdx) 的 "Parent checkbox" 一节给出了创建"全选/取消全选"父级复选框的步骤:

  1. <CheckboxGroup>成为受控组件(传入valueonValueChange);
  2. 把组内所有子复选框的值数组传给allValues
  3. 在父级<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.lengthindeterminate = value.length !== allValues.length && value.length > 0。当父级在mixed态被点击时,下一状态为'on'(全选);'on'时点击则变为'off'(全不选)——测试preserves initial state if mixed when parent is clickeddoes 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):外层CheckboxGroupmainPermissionsallValues,内层以userManagementPermissionsallValues;勾选外层"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的推荐写法——通过renderCheckboxGroupFieldset.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.tsxField分组对校验行为有非常细的测试覆盖,包括:

  • 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的三种命名方式:

  1. aria-labelledby+ 兄弟标签<div id="protocols-label">Allowed network protocols</div>配合<CheckboxGroup aria-labelledby="protocols-label">
  2. 包裹式<label>:每个复选项用<label><Checkbox.Root value="http" /> HTTP</label>包裹——默认情况下Checkbox.Root渲染为<span>,因此支持包裹标签;
  3. 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.LabelhtmlFor不会错误地指向组内某个任意复选框——组本身才是字段控件。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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询