From 6a87e276071ca170819c1658cad8eb3ca0e755c8 Mon Sep 17 00:00:00 2001 From: Grant-ccc <24774762722@qq.com> Date: Sat, 25 Jul 2026 17:25:08 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(Checkbox):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E9=A3=8E=E6=A0=BC=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20ESLint=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持 Checkbox.Button 按钮风格 - 新增 8 个单元测试用例 - 修复 Check.tsx 嵌套三元表达式 Closes #2562 --- .../checkbox/__tests__/checkbox.test.tsx | 109 ++++++++++++++++++ packages/components/common/Check.tsx | 15 ++- 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/packages/components/checkbox/__tests__/checkbox.test.tsx b/packages/components/checkbox/__tests__/checkbox.test.tsx index a2553948dd..5b5840faae 100644 --- a/packages/components/checkbox/__tests__/checkbox.test.tsx +++ b/packages/components/checkbox/__tests__/checkbox.test.tsx @@ -124,3 +124,112 @@ describe('CheckboxGroup', () => { expect(container.firstChild.lastChild).toHaveClass('t-is-disabled'); }); }); + +describe('Checkbox.Button', () => { + test('button style', () => { + const { container, queryByText } = render(选中按钮); + expect(container.firstChild).toHaveClass('t-checkbox-button', 't-is-checked'); + expect(queryByText('选中按钮')).toBeInTheDocument(); + }); + + test('button disabled', () => { + const fn = vi.fn(); + const { container } = render(); + expect(container.firstChild).toHaveClass('t-is-disabled'); + fireEvent.click(container.firstChild); + expect(fn).toBeCalledTimes(0); + }); +}); + +describe('CheckboxGroup button style', () => { + test('theme=button with children', () => { + const { container } = render( + + 北京 + 上海 + 广州 + , + ); + expect(container.firstChild).toHaveClass('t-checkbox-group'); + expect(container.firstChild.firstChild).toHaveClass('t-checkbox-button', 't-is-checked'); + }); + + test('variant outline', () => { + const { container } = render( + + 北京 + 上海 + , + ); + expect(container.firstChild).toHaveClass('t-checkbox-group__outline'); + }); + + test('variant default-filled', () => { + const { container } = render( + + 北京 + 上海 + , + ); + expect(container.firstChild).toHaveClass('t-checkbox-group--filled', 't-checkbox-group--default-filled'); + }); + + test('variant primary-filled', () => { + const { container } = render( + + 北京 + 上海 + , + ); + expect(container.firstChild).toHaveClass('t-checkbox-group--primary-filled'); + }); + + test('direction vertical', () => { + const { container } = render( + + 北京 + 上海 + , + ); + expect(container.firstChild).toHaveClass('t-checkbox-group--vertical'); + }); + + test('size small', () => { + const { container } = render( + + 北京 + , + ); + expect(container.firstChild).toHaveClass('t-size-s'); + }); + + test('size large', () => { + const { container } = render( + + 北京 + , + ); + expect(container.firstChild).toHaveClass('t-size-l'); + }); + + test('theme=button with options', () => { + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass('t-checkbox-group', 't-checkbox-group--primary-filled'); + expect(container.firstChild.firstChild).toHaveClass('t-checkbox-button', 't-is-checked'); + }); + + test('button onChange', () => { + const fn = vi.fn(); + const { container } = render( + + 北京 + 上海 + , + ); + fireEvent.click(container.firstChild.childNodes[1]); + expect(fn).toBeCalledTimes(1); + expect(fn).toBeCalledWith(['bj', 'sh'], expect.any(Object)); + }); +}); diff --git a/packages/components/common/Check.tsx b/packages/components/common/Check.tsx index c20ee652b3..812f33fd8f 100644 --- a/packages/components/common/Check.tsx +++ b/packages/components/common/Check.tsx @@ -11,7 +11,7 @@ import type { TdCheckboxProps } from '../checkbox/type'; import type { StyledProps } from '../common'; export interface CheckProps extends TdCheckboxProps, StyledProps { - type: 'radio' | 'radio-button' | 'checkbox'; + type: 'radio' | 'radio-button' | 'checkbox' | 'checkbox-button'; allowUncheck?: boolean; title?: string; children?: React.ReactNode; @@ -55,7 +55,9 @@ const Check = forwardRef((_props, ref) => { const TOnChange: ( checked: boolean, - context: { e: ChangeEvent | MouseEvent }, + context: { + e: ChangeEvent | MouseEvent; + }, ) => void = onChange; const [internalChecked, setInternalChecked] = useControlled(props, 'checked', TOnChange); @@ -69,10 +71,17 @@ const Check = forwardRef((_props, ref) => { const readOnly = props.readOnly || props.readonly; const isDisabled = disabled || readOnly; + // 处理 button 类型转换为标准类型 + const inputType = (() => { + if (type === 'radio-button') return 'radio'; + if (type === 'checkbox-button') return 'checkbox'; + return type; + })(); + const input = ( Date: Sat, 25 Jul 2026 17:40:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(Checkbox):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E9=A3=8E=E6=A0=BC=E5=AE=8C=E6=95=B4=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Checkbox.Button 子组件 - 支持 theme/variant/direction 属性 - 更新 TypeScript 类型定义 - 新增按钮风格 Demo 示例 - 更新文档说明 Related: #2562 --- packages/components/checkbox/Checkbox.tsx | 12 +- .../components/checkbox/CheckboxGroup.tsx | 110 +++++++++++------- .../components/checkbox/_example/button.tsx | 102 ++++++++++++++++ .../components/checkbox/checkbox.en-US.md | 8 ++ packages/components/checkbox/checkbox.md | 8 ++ packages/components/checkbox/defaultProps.ts | 9 +- packages/components/checkbox/type.ts | 31 ++++- 7 files changed, 228 insertions(+), 52 deletions(-) create mode 100644 packages/components/checkbox/_example/button.tsx diff --git a/packages/components/checkbox/Checkbox.tsx b/packages/components/checkbox/Checkbox.tsx index 23b526d21c..c7567a0904 100644 --- a/packages/components/checkbox/Checkbox.tsx +++ b/packages/components/checkbox/Checkbox.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import forwardRefWithStatics from '../_util/forwardRefWithStatics'; import Check from '../common/Check'; @@ -6,15 +6,21 @@ import useDefaultProps from '../hooks/useDefaultProps'; import CheckboxGroup from './CheckboxGroup'; import { checkboxDefaultProps } from './defaultProps'; +import type { Ref } from 'react'; import type { CheckProps } from '../common/Check'; export type CheckboxProps = Omit; const Checkbox = forwardRefWithStatics( - (props: CheckboxProps, ref: React.Ref) => ( + (props: CheckboxProps, ref: Ref) => ( (props, checkboxDefaultProps)} /> ), - { Group: CheckboxGroup }, + { + Group: CheckboxGroup, + Button: forwardRef((props, ref) => ( + (props, checkboxDefaultProps)} /> + )), + }, ); Checkbox.displayName = 'Checkbox'; diff --git a/packages/components/checkbox/CheckboxGroup.tsx b/packages/components/checkbox/CheckboxGroup.tsx index 24e84c3a7f..fdfea243e4 100644 --- a/packages/components/checkbox/CheckboxGroup.tsx +++ b/packages/components/checkbox/CheckboxGroup.tsx @@ -1,15 +1,16 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { isNumber } from 'lodash-es'; import { CheckContext } from '../common/Check'; +import useCommonClassName from '../hooks/useCommonClassName'; import useConfig from '../hooks/useConfig'; import useControlled from '../hooks/useControlled'; import useDefaultProps from '../hooks/useDefaultProps'; import Checkbox from './Checkbox'; import { checkboxGroupDefaultProps } from './defaultProps'; -import type { ReactElement } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import type { StyledProps } from '../common'; import type { CheckContextValue, CheckProps } from '../common/Check'; import type { CheckboxProps } from './Checkbox'; @@ -23,7 +24,7 @@ import type { export interface CheckboxGroupProps extends TdCheckboxGroupProps, StyledProps { - children?: React.ReactNode; + children?: ReactNode; } const getCheckboxValue = (v: CheckboxOption) => { @@ -47,6 +48,7 @@ const getCheckboxValue = (v: CheckboxOption) => { const CheckboxGroup = (props: CheckboxGroupProps) => { type ItemType = T[number]; const { classPrefix } = useConfig(); + const { SIZE: sizeMap } = useCommonClassName(); const { onChange, disabled, @@ -55,6 +57,10 @@ const CheckboxGroup = (props: children, max, options = [], + size, + variant, + theme, + direction, ...resetProps } = useDefaultProps>(props, checkboxGroupDefaultProps); @@ -71,15 +77,15 @@ const CheckboxGroup = (props: ) || []; const optionsWithoutCheckAll = intervalOptions.filter((t) => typeof t !== 'object' || !t.checkAll); - const optionsWithoutCheckAllValues = []; + const optionsWithoutCheckAllValues: (string | number | boolean | undefined)[] = []; optionsWithoutCheckAll.forEach((v: string | number) => { const vs = getCheckboxValue(v); optionsWithoutCheckAllValues.push(vs); }); const { enabledValues, disabledValues } = useMemo(() => { - const enabledValues = []; - const disabledValues = []; + const enabledValues: (string | number | boolean | undefined)[] = []; + const disabledValues: (string | number | boolean | undefined)[] = []; optionsWithoutCheckAll.forEach((option) => { const isOptionDisabled = typeof option === 'object' && (option.disabled || option.readOnly || option.readonly); const value = getCheckboxValue(option); @@ -96,6 +102,8 @@ const CheckboxGroup = (props: const [internalValue, setInternalValue] = useControlled(props, 'value', onChange); const [localMax, setLocalMax] = useState(max); + const checkboxGroupRef = useRef(null); + const getCheckedSet = useCallback(() => { if (!Array.isArray(internalValue)) { return new Set([]); @@ -194,45 +202,59 @@ const CheckboxGroup = (props: // options 和 children 的抉择,在未明确说明时,暂时以 options 优先 const useOptions = Array.isArray(options) && options.length !== 0; + // 根据 theme 选择渲染组件 + const Comp = theme === 'button' ? Checkbox.Button : Checkbox; + + const renderOptions = (): ReactNode => { + return options.map((v: CheckboxOption, index: number) => { + switch (typeof v) { + case 'string': + return ( + + {v} + + ); + case 'number': + return ( + + {String(v)} + + ); + case 'object': { + const vs = v as CheckboxOptionObj; + // CheckAll 的 checkBox 不存在 value,故用 checkAll_index 来保证尽量不和用户的 value 冲突. + return vs.checkAll ? ( + + ) : ( + + ); + } + default: + return null; + } + }); + }; + + // 构建 className + const groupClassName = classNames(`${classPrefix}-checkbox-group`, sizeMap[size], className, { + // 边框型 + [`${classPrefix}-checkbox-group__outline`]: theme === 'button' && variant === 'outline', + // 填充型 + [`${classPrefix}-checkbox-group--filled`]: theme === 'button' && variant?.includes('filled'), + [`${classPrefix}-checkbox-group--default-filled`]: theme === 'button' && variant === 'default-filled', + [`${classPrefix}-checkbox-group--primary-filled`]: theme === 'button' && variant === 'primary-filled', + // 纵向排列 + [`${classPrefix}-checkbox-group--vertical`]: direction === 'vertical', + }); + return ( -
- - {useOptions - ? options.map((v: any, index) => { - switch (typeof v) { - case 'string': - return ( - - {v} - - ); - case 'number': { - return ( - - {String(v)} - - ); - } - case 'object': { - const vs = v as CheckboxOptionObj; - // CheckAll 的 checkBox 不存在 value,故用 checkAll_index 来保证尽量不和用户的 value 冲突. - return vs.checkAll ? ( - - ) : ( - - ); - } - default: - return null; - } - }) - : children} - +
+ {useOptions ? renderOptions() : children}
); }; diff --git a/packages/components/checkbox/_example/button.tsx b/packages/components/checkbox/_example/button.tsx new file mode 100644 index 0000000000..2255f9fd5c --- /dev/null +++ b/packages/components/checkbox/_example/button.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { Checkbox, Space } from 'tdesign-react'; + +export default function CheckboxButtonExample() { + const [outlineValue, setOutlineValue] = useState(['bj', 'gz']); + const [filledDefaultValue, setFilledDefaultValue] = useState(['bj', 'sh']); + const [filledPrimaryValue, setFilledPrimaryValue] = useState(['gz']); + const [verticalValue, setVerticalValue] = useState(['bj', 'sz']); + + return ( + + {/* 场景 1:边框型多选按钮 */} +
+

边框型多选按钮

+ theme="button" variant="outline" value={outlineValue} onChange={setOutlineValue}> + 北京 + 上海 + 广州 + 深圳 + +
已选: {outlineValue.join('、') || '无'}
+
+ + {/* 场景 2:填充型 - 默认/白色高亮 */} +
+

填充型多选按钮 - 默认高亮(白色卡片)

+ + theme="button" + variant="default-filled" + value={filledDefaultValue} + onChange={setFilledDefaultValue} + > + 北京 + 上海 + 广州 + 深圳 + +
已选: {filledDefaultValue.join('、') || '无'}
+
+ + {/* 场景 3:填充型 - 主题色高亮 */} +
+

填充型多选按钮 - 主题色高亮(蓝色)

+ + theme="button" + variant="primary-filled" + value={filledPrimaryValue} + onChange={setFilledPrimaryValue} + > + 北京 + 上海 + 广州 + 深圳 + +
已选: {filledPrimaryValue.join('、') || '无'}
+
+ + {/* 场景 4:纵向排列 */} +
+

纵向排列(卡片式)

+ + theme="button" + variant="primary-filled" + direction="vertical" + value={verticalValue} + onChange={setVerticalValue} + > + 北京 + 上海 + 广州 + 深圳 + +
已选: {verticalValue.join('、') || '无'}
+
+ + {/* 附加:禁用状态展示 */} +
+

禁用状态

+ + theme="button" variant="outline" defaultValue={['bj']}> + + 选中禁用 + + 上海 + + theme="button" variant="default-filled" defaultValue={['gz']}> + + 选中禁用 + + 深圳 + + theme="button" variant="primary-filled" defaultValue={['sh']}> + + 选中禁用 + + 北京 + + +
+
+ ); +} diff --git a/packages/components/checkbox/checkbox.en-US.md b/packages/components/checkbox/checkbox.en-US.md index b8aac877fa..462108fbe5 100644 --- a/packages/components/checkbox/checkbox.en-US.md +++ b/packages/components/checkbox/checkbox.en-US.md @@ -1,5 +1,9 @@ :: BASE_DOC :: +### Button Style + +{{ button }} + ## API ### Checkbox Props @@ -27,11 +31,15 @@ name | type | default | description | required -- | -- | -- | -- | -- className | String | - | className of component | N style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N +direction | String | horizontal | arrangement of checkbox buttons. Options: horizontal/vertical | N disabled | Boolean | - | \- | N max | Number | undefined | \- | N name | String | - | \- | N options | Array | - | Typescript:`Array` `type CheckboxOption = string \| number \| CheckboxOptionObj` `interface CheckboxOptionObj { label?: string \| TNode; value?: string \| number; disabled?: boolean; name?: string; checkAll?: true }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N readOnly | Boolean | undefined | \- | N +size | String | medium | component size, only valid when theme is button. Options: small/medium/large. Typescript: `SizeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +theme | String | checkbox | style of checkbox group when rendering with options. Options: checkbox/button | N value | Array | [] | Typescript:`T` `type CheckboxGroupValue = Array`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N defaultValue | Array | [] | uncontrolled property。Typescript:`T` `type CheckboxGroupValue = Array`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N +variant | String | outline | button style variant. Options: outline/primary-filled/default-filled | N onChange | Function | | Typescript:`(value: T, context: CheckboxGroupChangeContext) => void`
[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts)。
`interface CheckboxGroupChangeContext { e: ChangeEvent; current: CheckboxOption \| TdCheckboxProps; type: 'check' \| 'uncheck' }`
| N diff --git a/packages/components/checkbox/checkbox.md b/packages/components/checkbox/checkbox.md index e8a1d80f7d..6c37ac06cb 100644 --- a/packages/components/checkbox/checkbox.md +++ b/packages/components/checkbox/checkbox.md @@ -1,6 +1,10 @@ :: BASE_DOC :: +### 按钮风格 + +{{ button }} + ### 最多选中的数量 {{ max }} @@ -32,11 +36,15 @@ onClick | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
-- | -- | -- | -- | -- className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +direction | String | horizontal | 多选框按钮排列方式。可选项:horizontal/vertical | N disabled | Boolean | - | 是否禁用组件,默认为 false。CheckboxGroup.disabled 优先级低于 Checkbox.disabled | N max | Number | undefined | 支持最多选中的数量 | N name | String | - | 统一设置内部复选框 HTML 属性 | N options | Array | - | 以配置形式设置子元素。示例1:`['北京', '上海']` ,示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」。TS 类型:`Array` `type CheckboxOption = string \| number \| CheckboxOptionObj` `interface CheckboxOptionObj { label?: string \| TNode; value?: string \| number; disabled?: boolean; name?: string; checkAll?: true }`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N readOnly | Boolean | undefined | 只读状态 | N +size | String | medium | 组件尺寸,仅在 theme 为 button 生效。可选项:small/medium/large。TS 类型:`SizeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +theme | String | checkbox | 用于在使用 options 方式渲染时决定组件的风格。可选项:checkbox/button | N value | Array | [] | 选中值。TS 类型:`T` `type CheckboxGroupValue = Array`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N defaultValue | Array | [] | 选中值。非受控属性。TS 类型:`T` `type CheckboxGroupValue = Array`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N +variant | String | outline | 多选组件按钮形式。可选项:outline/primary-filled/default-filled | N onChange | Function | | TS 类型:`(value: T, context: CheckboxGroupChangeContext) => void`
值变化时触发,`context.current` 表示当前变化的数据值,如果是全选则为空;`context.type` 表示引起选中数据变化的是选中或是取消选中;`context.option` 表示当前变化的数据项。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts)。
`interface CheckboxGroupChangeContext { e: ChangeEvent; current: CheckboxOption \| TdCheckboxProps; type: 'check' \| 'uncheck' }`
| N diff --git a/packages/components/checkbox/defaultProps.ts b/packages/components/checkbox/defaultProps.ts index 38f90f3812..1e3b03d0e7 100644 --- a/packages/components/checkbox/defaultProps.ts +++ b/packages/components/checkbox/defaultProps.ts @@ -13,4 +13,11 @@ export const checkboxDefaultProps: TdCheckboxProps = { readOnly: false, }; -export const checkboxGroupDefaultProps: TdCheckboxGroupProps = { max: undefined, defaultValue: [] }; +export const checkboxGroupDefaultProps: TdCheckboxGroupProps = { + max: undefined, + defaultValue: [], + direction: 'horizontal', + size: 'medium', + theme: 'checkbox', + variant: 'outline', +}; diff --git a/packages/components/checkbox/type.ts b/packages/components/checkbox/type.ts index b7b0f6a378..95a2cdb843 100644 --- a/packages/components/checkbox/type.ts +++ b/packages/components/checkbox/type.ts @@ -4,8 +4,8 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode } from '../common'; -import { MouseEvent, ChangeEvent } from 'react'; +import type { TNode, SizeEnum } from "../common"; +import type { MouseEvent, ChangeEvent } from "react"; export interface TdCheckboxProps { /** @@ -68,7 +68,10 @@ export interface TdCheckboxProps { /** * 值变化时触发 */ - onChange?: (checked: boolean, context: { e: ChangeEvent }) => void; + onChange?: ( + checked: boolean, + context: { e: ChangeEvent }, + ) => void; /** * 点击时触发,一般用于外层阻止冒泡场景 */ @@ -80,6 +83,11 @@ export interface TdCheckboxGroupProps { * 是否禁用组件,默认为 false。CheckboxGroup.disabled 优先级低于 Checkbox.disabled */ disabled?: boolean; + /** + * 多选框按钮排列方式 + * @default horizontal + */ + direction?: "horizontal" | "vertical"; /** * 支持最多选中的数量 */ @@ -102,6 +110,16 @@ export interface TdCheckboxGroupProps { * 只读状态 */ readOnly?: boolean; + /** + * 组件尺寸 + * @default medium + */ + size?: SizeEnum; + /** + * 用于在使用 options 方式渲染时决定组件的风格 + * @default checkbox + */ + theme?: "checkbox" | "button"; /** * 选中值 * @default [] @@ -112,6 +130,11 @@ export interface TdCheckboxGroupProps { * @default [] */ defaultValue?: T; + /** + * 多选组件按钮形式 + * @default outline + */ + variant?: "outline" | "primary-filled" | "default-filled"; /** * 值变化时触发,`context.current` 表示当前变化的数据值,如果是全选则为空;`context.type` 表示引起选中数据变化的是选中或是取消选中;`context.option` 表示当前变化的数据项 */ @@ -139,5 +162,5 @@ export interface CheckboxGroupChangeContext { e: ChangeEvent; current: CheckboxOption | TdCheckboxProps; option: CheckboxOption | TdCheckboxProps; - type: 'check' | 'uncheck'; + type: "check" | "uncheck"; }