From 382a87cba0f4c81736767ed734ecca4c2da60e85 Mon Sep 17 00:00:00 2001 From: Rylan Date: Sat, 11 Oct 2025 19:13:44 +0800 Subject: [PATCH 1/9] fix(CheckboxGroup): enhance handling of disabled options --- .../components/checkbox/CheckboxGroup.tsx | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/packages/components/checkbox/CheckboxGroup.tsx b/packages/components/checkbox/CheckboxGroup.tsx index f4a60de752..eb7d972c55 100644 --- a/packages/components/checkbox/CheckboxGroup.tsx +++ b/packages/components/checkbox/CheckboxGroup.tsx @@ -1,16 +1,22 @@ import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { isNumber } from 'lodash-es'; +import { CheckContext, type CheckContextValue, type CheckProps } from '../common/Check'; import useConfig from '../hooks/useConfig'; -import { CheckContext, CheckContextValue, CheckProps } from '../common/Check'; -import { CheckboxGroupValue, CheckboxOption, CheckboxOptionObj, TdCheckboxGroupProps, TdCheckboxProps } from './type'; -import { StyledProps } from '../common'; import useControlled from '../hooks/useControlled'; +import useDefaultProps from '../hooks/useDefaultProps'; import Checkbox from './Checkbox'; import { checkboxGroupDefaultProps } from './defaultProps'; -import useDefaultProps from '../hooks/useDefaultProps'; +import type { StyledProps } from '../common'; import type { CheckboxProps } from './Checkbox'; +import type { + CheckboxGroupValue, + CheckboxOption, + CheckboxOptionObj, + TdCheckboxGroupProps, + TdCheckboxProps, +} from './type'; export interface CheckboxGroupProps extends TdCheckboxGroupProps, @@ -18,8 +24,7 @@ export interface CheckboxGroupProps { +const getCheckboxValue = (v: CheckboxOption) => { switch (typeof v) { case 'number': return v as number; @@ -67,6 +72,31 @@ const CheckboxGroup = (props: optionsWithoutCheckAllValues.push(vs); }); + const enabledOptionsValues = useMemo(() => { + const enabledValues = []; + optionsWithoutCheckAll.forEach((option) => { + const isOptionDisabled = typeof option === 'object' && option.disabled; + if (!isOptionDisabled && !disabled) { + const value = getCheckboxValue(option); + enabledValues.push(value); + } + }); + return enabledValues; + }, [optionsWithoutCheckAll, disabled]); + + // 获取被禁用的选项值 + const disabledOptionsValues = useMemo(() => { + const disabledValues = []; + optionsWithoutCheckAll.forEach((option) => { + const isOptionDisabled = typeof option === 'object' && option.disabled; + if (isOptionDisabled || disabled) { + const value = getCheckboxValue(option); + disabledValues.push(value); + } + }); + return disabledValues; + }, [optionsWithoutCheckAll, disabled]); + const [internalValue, setInternalValue] = useControlled(props, 'value', onChange); const [localMax, setLocalMax] = useState(max); @@ -78,16 +108,19 @@ const CheckboxGroup = (props: }, [internalValue]); const checkedSet = useMemo(() => getCheckedSet(), [getCheckedSet]); - // 用于决定全选状态的属性 const indeterminate = useMemo(() => { - const list = Array.from(checkedSet); - return list.length !== 0 && list.length !== optionsWithoutCheckAll.length; - }, [checkedSet, optionsWithoutCheckAll]); + const checkedEnabledValues = enabledOptionsValues.filter((value) => checkedSet.has(value)); + const checkedDisabledValues = disabledOptionsValues.filter((value) => checkedSet.has(value)); + // 存在被禁用且已选中的选项,直接显示半选状态 + if (checkedDisabledValues.length > 0) return true; + // 否则检查未禁用的选项是否处于部分选中状态 + return checkedEnabledValues.length !== 0 && checkedEnabledValues.length !== enabledOptionsValues.length; + }, [checkedSet, enabledOptionsValues, disabledOptionsValues]); const checkAllChecked = useMemo(() => { - const list = Array.from(checkedSet); - return list.length === optionsWithoutCheckAll.length; - }, [checkedSet, optionsWithoutCheckAll]); + const checkedEnabledValues = enabledOptionsValues.filter((value) => checkedSet.has(value)); + return enabledOptionsValues.length > 0 && checkedEnabledValues.length === enabledOptionsValues.length; + }, [checkedSet, enabledOptionsValues]); useEffect(() => { if (!isNumber(max)) { @@ -126,13 +159,20 @@ const CheckboxGroup = (props: } const checkedSet = getCheckedSet(); - // 全选时的逻辑处理 + if (checkProps.checkAll) { + // 保存被禁用选项的当前状态 + const disabledCheckedValues = disabledOptionsValues.filter((value) => checkedSet.has(value)); + // 计算当前启用选项的选中状态 + const checkedEnabledValues = enabledOptionsValues.filter((value) => checkedSet.has(value)); + const allEnabledChecked = + enabledOptionsValues.length > 0 && checkedEnabledValues.length === enabledOptionsValues.length; + checkedSet.clear(); - if (checked) { - optionsWithoutCheckAllValues.forEach((v) => { - checkedSet.add(v); - }); + // 恢复被禁用选项的原有状态 + disabledCheckedValues.forEach((v) => checkedSet.add(v)); + if (!allEnabledChecked) { + enabledOptionsValues.forEach((v) => checkedSet.add(v)); } } else if (checked) { if (checkedSet.size >= localMax && isNumber(max)) return; From 23b81471034fe598cd4ab68afc55b6dd677a93cb Mon Sep 17 00:00:00 2001 From: Rylan Date: Sat, 11 Oct 2025 19:47:54 +0800 Subject: [PATCH 2/9] feat(CheckboxGroup): add `readonly` prop --- .../components/checkbox/CheckboxGroup.tsx | 58 ++++++++-------- .../components/checkbox/_example/group.tsx | 67 +++++++++++++------ .../components/checkbox/checkbox.en-US.md | 2 + packages/components/checkbox/checkbox.md | 1 + packages/components/checkbox/type.ts | 9 ++- 5 files changed, 86 insertions(+), 51 deletions(-) diff --git a/packages/components/checkbox/CheckboxGroup.tsx b/packages/components/checkbox/CheckboxGroup.tsx index eb7d972c55..8712a7a346 100644 --- a/packages/components/checkbox/CheckboxGroup.tsx +++ b/packages/components/checkbox/CheckboxGroup.tsx @@ -53,6 +53,7 @@ const CheckboxGroup = (props: children, max, options = [], + readonly, } = useDefaultProps>(props, checkboxGroupDefaultProps); // 去掉所有 checkAll 之后的 options @@ -72,30 +73,21 @@ const CheckboxGroup = (props: optionsWithoutCheckAllValues.push(vs); }); - const enabledOptionsValues = useMemo(() => { + const { enabledValues, disabledValues } = useMemo(() => { const enabledValues = []; - optionsWithoutCheckAll.forEach((option) => { - const isOptionDisabled = typeof option === 'object' && option.disabled; - if (!isOptionDisabled && !disabled) { - const value = getCheckboxValue(option); - enabledValues.push(value); - } - }); - return enabledValues; - }, [optionsWithoutCheckAll, disabled]); - - // 获取被禁用的选项值 - const disabledOptionsValues = useMemo(() => { const disabledValues = []; optionsWithoutCheckAll.forEach((option) => { - const isOptionDisabled = typeof option === 'object' && option.disabled; - if (isOptionDisabled || disabled) { - const value = getCheckboxValue(option); + const isOptionDisabled = typeof option === 'object' && (option.disabled || option.readonly); + const value = getCheckboxValue(option); + + if (isOptionDisabled || disabled || readonly) { disabledValues.push(value); + } else { + enabledValues.push(value); } }); - return disabledValues; - }, [optionsWithoutCheckAll, disabled]); + return { enabledValues, disabledValues }; + }, [optionsWithoutCheckAll, disabled, readonly]); const [internalValue, setInternalValue] = useControlled(props, 'value', onChange); const [localMax, setLocalMax] = useState(max); @@ -109,18 +101,18 @@ const CheckboxGroup = (props: const checkedSet = useMemo(() => getCheckedSet(), [getCheckedSet]); const indeterminate = useMemo(() => { - const checkedEnabledValues = enabledOptionsValues.filter((value) => checkedSet.has(value)); - const checkedDisabledValues = disabledOptionsValues.filter((value) => checkedSet.has(value)); + const checkableValues = enabledValues.filter((value) => checkedSet.has(value)); + const checkedDisabledValues = disabledValues.filter((value) => checkedSet.has(value)); // 存在被禁用且已选中的选项,直接显示半选状态 if (checkedDisabledValues.length > 0) return true; // 否则检查未禁用的选项是否处于部分选中状态 - return checkedEnabledValues.length !== 0 && checkedEnabledValues.length !== enabledOptionsValues.length; - }, [checkedSet, enabledOptionsValues, disabledOptionsValues]); + return checkableValues.length !== 0 && checkableValues.length !== enabledValues.length; + }, [checkedSet, enabledValues, disabledValues]); const checkAllChecked = useMemo(() => { - const checkedEnabledValues = enabledOptionsValues.filter((value) => checkedSet.has(value)); - return enabledOptionsValues.length > 0 && checkedEnabledValues.length === enabledOptionsValues.length; - }, [checkedSet, enabledOptionsValues]); + const checkableValues = enabledValues.filter((value) => checkedSet.has(value)); + return enabledValues.length > 0 && checkableValues.length === enabledValues.length; + }, [checkedSet, enabledValues]); useEffect(() => { if (!isNumber(max)) { @@ -153,6 +145,7 @@ const CheckboxGroup = (props: checked: checkProps.checkAll ? checkAllChecked : checkedSet.has(checkValue), indeterminate: checkProps.checkAll ? indeterminate : checkProps.indeterminate, disabled: checkProps.disabled || disabled || (checkedSet.size >= localMax && !checkedSet.has(checkValue)), + readonly: checkProps.readonly || readonly, onChange(checked, { e }) { if (typeof checkProps.onChange === 'function') { checkProps.onChange(checked, { e }); @@ -162,17 +155,17 @@ const CheckboxGroup = (props: if (checkProps.checkAll) { // 保存被禁用选项的当前状态 - const disabledCheckedValues = disabledOptionsValues.filter((value) => checkedSet.has(value)); + const disabledCheckedValues = disabledValues.filter((value) => checkedSet.has(value)); // 计算当前启用选项的选中状态 - const checkedEnabledValues = enabledOptionsValues.filter((value) => checkedSet.has(value)); + const checkedenabledOptionsValues = enabledValues.filter((value) => checkedSet.has(value)); const allEnabledChecked = - enabledOptionsValues.length > 0 && checkedEnabledValues.length === enabledOptionsValues.length; + enabledValues.length > 0 && checkedenabledOptionsValues.length === enabledValues.length; checkedSet.clear(); // 恢复被禁用选项的原有状态 disabledCheckedValues.forEach((v) => checkedSet.add(v)); if (!allEnabledChecked) { - enabledOptionsValues.forEach((v) => checkedSet.add(v)); + enabledValues.forEach((v) => checkedSet.add(v)); } } else if (checked) { if (checkedSet.size >= localMax && isNumber(max)) return; @@ -223,7 +216,12 @@ const CheckboxGroup = (props: return vs.checkAll ? ( ) : ( - + ); } default: diff --git a/packages/components/checkbox/_example/group.tsx b/packages/components/checkbox/_example/group.tsx index 2b09d0a3d6..c66d1db8be 100644 --- a/packages/components/checkbox/_example/group.tsx +++ b/packages/components/checkbox/_example/group.tsx @@ -1,7 +1,11 @@ import React, { useState } from 'react'; -import { Checkbox, Space } from 'tdesign-react'; +import { Checkbox, Divider, Space } from 'tdesign-react'; const options = [ + { + label: '全选', + checkAll: true, + }, { value: '北京', label: '北京', @@ -13,34 +17,59 @@ const options = [ { value: '广州', label: '广州', - }, - { - label: '全选', - checkAll: true as const, + disabled: true, }, ]; export default function CheckboxExample() { const [disabled, setDisabled] = useState(false); - const [city, setCity] = useState(['北京']); + const [city, setCity] = useState(['广州']); + const [city2, setCity2] = useState(['上海']); return ( -
选中值: {city.join('、')}
-
- setDisabled(value)}> - 禁用全部 - -
- - { - setCity(value); + setDisabled(value); }} - options={options} - /> + > + 禁用全部 +
+ + + 写法一:使用 options +
选中值: {city.join('、')}
+ { + setCity(value); + }} + options={options} + /> +
+ + + + + 写法二:使用插槽 +
选中值: {city2.join('、')}
+ { + setCity2(value); + }} + > + 全选 + 北京 + 上海 + + 广州 + + +
); } diff --git a/packages/components/checkbox/checkbox.en-US.md b/packages/components/checkbox/checkbox.en-US.md index 2fa387e003..09bba83ebe 100644 --- a/packages/components/checkbox/checkbox.en-US.md +++ b/packages/components/checkbox/checkbox.en-US.md @@ -31,6 +31,8 @@ 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 +value | Array | [] | Typescript:`T` `type 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 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 8d70d02fb8..f34a46dad0 100644 --- a/packages/components/checkbox/checkbox.md +++ b/packages/components/checkbox/checkbox.md @@ -36,6 +36,7 @@ disabled | Boolean | - | 是否禁用组件,默认为 false。CheckboxGroup.di 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 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 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/type.ts b/packages/components/checkbox/type.ts index 2fc5c5e6b2..9386a3b4e3 100644 --- a/packages/components/checkbox/type.ts +++ b/packages/components/checkbox/type.ts @@ -87,6 +87,10 @@ export interface TdCheckboxGroupProps { * 以配置形式设置子元素。示例1:`['北京', '上海']` ,示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」 */ options?: Array; + /** + * 只读状态 + */ + readonly?: boolean; /** * 选中值 * @default [] @@ -107,10 +111,11 @@ export type CheckboxOption = string | number | CheckboxOptionObj; export interface CheckboxOptionObj { label?: string | TNode; - value?: string | number; + value?: string | number | boolean; disabled?: boolean; + readonly?: boolean; name?: string; - checkAll?: true; + checkAll?: boolean; } export type CheckboxGroupValue = Array; From 67566c787657d19a9047e42a42c71a581d74b442 Mon Sep 17 00:00:00 2001 From: Rylan Date: Sat, 11 Oct 2025 19:49:22 +0800 Subject: [PATCH 3/9] feat(Form): add `readonly` prop --- packages/components/form/Form.tsx | 21 ++++++++++++--------- packages/components/form/FormContext.tsx | 8 +++++--- packages/components/form/FormItem.tsx | 7 +++++-- packages/components/form/form.en-US.md | 1 + packages/components/form/form.md | 17 +++++++++-------- packages/components/form/type.ts | 14 +++++++++----- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/components/form/Form.tsx b/packages/components/form/Form.tsx index b5a3671b86..ca5e17571e 100644 --- a/packages/components/form/Form.tsx +++ b/packages/components/form/Form.tsx @@ -1,18 +1,19 @@ -import React, { useRef, useImperativeHandle } from 'react'; +import React, { useImperativeHandle, useRef } from 'react'; import classNames from 'classnames'; -import useConfig from '../hooks/useConfig'; -import noop from '../_util/noop'; import forwardRefWithStatics from '../_util/forwardRefWithStatics'; -import type { TdFormProps } from './type'; -import useInstance from './hooks/useInstance'; -import useForm, { HOOK_MARK } from './hooks/useForm'; -import useWatch from './hooks/useWatch'; -import { StyledProps } from '../common'; +import noop from '../_util/noop'; +import useConfig from '../hooks/useConfig'; +import useDefaultProps from '../hooks/useDefaultProps'; import FormContext from './FormContext'; import FormItem from './FormItem'; import FormList from './FormList'; import { formDefaultProps } from './defaultProps'; -import useDefaultProps from '../hooks/useDefaultProps'; +import useForm, { HOOK_MARK } from './hooks/useForm'; +import useInstance from './hooks/useInstance'; +import useWatch from './hooks/useWatch'; + +import type { StyledProps } from '../common'; +import type { TdFormProps } from './type'; export interface FormProps extends TdFormProps, StyledProps { children?: React.ReactNode; @@ -40,6 +41,7 @@ const Form = forwardRefWithStatics( errorMessage = globalFormConfig.errorMessage, preventSubmitDefault, disabled, + readonly, children, id, onReset, @@ -106,6 +108,7 @@ const Form = forwardRefWithStatics( resetType, rules, disabled, + readonly, formMapRef, floatingFormDataRef, onFormItemValueChange, diff --git a/packages/components/form/FormContext.tsx b/packages/components/form/FormContext.tsx index 676276060a..17f9f119f1 100644 --- a/packages/components/form/FormContext.tsx +++ b/packages/components/form/FormContext.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { TdFormProps, TdFormListProps, NamePath } from './type'; -import { FormItemInstance } from './FormItem'; -import { InternalFormInstance } from './hooks/interface'; +import type { FormItemInstance } from './FormItem'; +import type { InternalFormInstance } from './hooks/interface'; +import type { NamePath, TdFormListProps, TdFormProps } from './type'; const FormContext = React.createContext<{ form?: InternalFormInstance; @@ -17,6 +17,7 @@ const FormContext = React.createContext<{ showErrorMessage: TdFormProps['showErrorMessage']; resetType: TdFormProps['resetType']; disabled: TdFormProps['disabled']; + readonly: TdFormProps['readonly']; rules: TdFormProps['rules']; errorMessage: TdFormProps['errorMessage']; formMapRef: React.RefObject>>; @@ -35,6 +36,7 @@ const FormContext = React.createContext<{ showErrorMessage: undefined, resetType: 'empty', disabled: undefined, + readonly: undefined, rules: undefined, errorMessage: undefined, statusIcon: undefined, diff --git a/packages/components/form/FormItem.tsx b/packages/components/form/FormItem.tsx index e3d2fb861b..398b9166af 100644 --- a/packages/components/form/FormItem.tsx +++ b/packages/components/form/FormItem.tsx @@ -5,7 +5,6 @@ import { ErrorCircleFilledIcon as TdErrorCircleFilledIcon, } from 'tdesign-icons-react'; import { flattenDeep, get, isEqual, isFunction, isObject, isString, merge, set, unset } from 'lodash-es'; -import { StyledProps } from '../common'; import useConfig from '../hooks/useConfig'; import useDefaultProps from '../hooks/useDefaultProps'; import useGlobalIcon from '../hooks/useGlobalIcon'; @@ -17,6 +16,9 @@ import { parseMessage, validate as validateModal } from './formModel'; import { HOOK_MARK } from './hooks/useForm'; import useFormItemInitialData, { ctrlKeyMap } from './hooks/useFormItemInitialData'; import useFormItemStyle from './hooks/useFormItemStyle'; +import { calcFieldValue } from './utils'; + +import type { StyledProps } from '../common'; import type { FormInstanceFunctions, FormItemValidateMessage, @@ -25,7 +27,6 @@ import type { TdFormItemProps, ValueType, } from './type'; -import { calcFieldValue } from './utils'; export interface FormItemProps extends TdFormItemProps, StyledProps { children?: React.ReactNode | React.ReactNode[] | ((form: FormInstanceFunctions) => React.ReactElement); @@ -65,6 +66,7 @@ const FormItem = forwardRef((originalProps, ref labelWidth: labelWidthFromContext, showErrorMessage: showErrorMessageFromContext, disabled: disabledFromContext, + readonly: readonlyFromContext, resetType: resetTypeFromContext, rules: rulesFromContext, statusIcon: statusIconFromContext, @@ -521,6 +523,7 @@ const FormItem = forwardRef((originalProps, ref const childProps = child.props as any; return React.cloneElement(child, { disabled: disabledFromContext, + readonly: readonlyFromContext, ...childProps, [ctrlKey]: formValue, onChange: (value: any, ...args: any[]) => { diff --git a/packages/components/form/form.en-US.md b/packages/components/form/form.en-US.md index f44aae5ce0..1c945b9531 100644 --- a/packages/components/form/form.en-US.md +++ b/packages/components/form/form.en-US.md @@ -19,6 +19,7 @@ labelAlign | String | right | options: left/right/top | N labelWidth | String / Number | '100px' | \- | N layout | String | vertical | options: vertical/inline | N preventSubmitDefault | Boolean | true | \- | N +readonly | Boolean | undefined | \- | N requiredMark | Boolean | true | \- | N requiredMarkPosition | String | left | Display position of required symbols。options: left/right | N resetType | String | empty | options: empty/initial | N diff --git a/packages/components/form/form.md b/packages/components/form/form.md index f4c8de765a..3f41977aa0 100644 --- a/packages/components/form/form.md +++ b/packages/components/form/form.md @@ -101,6 +101,7 @@ labelAlign | String | right | 表单字段标签对齐方式:左对齐、右 labelWidth | String / Number | '100px' | 可以整体设置label标签宽度,默认为100px | N layout | String | vertical | 表单布局,有两种方式:纵向布局 和 行内布局。可选项:vertical/inline | N preventSubmitDefault | Boolean | true | 是否阻止表单提交默认事件(表单提交默认事件会刷新页面),设置为 `true` 可以避免刷新 | N +readonly | Boolean | undefined | 是否整个表单只读 | N requiredMark | Boolean | true | 是否显示必填符号(*),默认显示 | N requiredMarkPosition | String | left | 表单必填符号(*)显示位置。可选项:left/right | N resetType | String | empty | 重置表单的方式,值为 empty 表示重置表单为空,值为 initial 表示重置表单数据为初始值。可选项:empty/initial | N @@ -109,9 +110,9 @@ scrollToFirstError | String | - | 表单校验不通过时,是否自动滚动 showErrorMessage | Boolean | true | 校验不通过时,是否显示错误提示信息,统一控制全部表单项。如果希望控制单个表单项,请给 FormItem 设置该属性 | N statusIcon | TNode | undefined | 校验状态图标,值为 `true` 显示默认图标,默认图标有 成功、失败、警告 等,不同的状态图标不同。`statusIcon` 值为 `false`,不显示图标。`statusIcon` 值类型为渲染函数,则可以自定义右侧状态图标。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N submitWithWarningMessage | Boolean | false | 【讨论中】当校验结果只有告警信息时,是否触发 `submit` 提交事件 | N -supportNumberKey | Boolean | true | 是否支持使用数字作为表单键值,在1.9.3版本后表单组件支持数字作为键值,若仍需要保留数字作为数组下标,请关闭此API或使用 FormList | N -onReset | Function | | TS 类型:`(context: { e?: FormResetEvent }) => void`
表单重置时触发 | N -onSubmit | Function | | TS 类型:`(context: SubmitContext) => void`
表单提交时触发。其中 `context.validateResult` 表示校验结果,`context.firstError` 表示校验不通过的第一个规则提醒。`context.validateResult` 值为 `true` 表示校验通过;如果校验不通过,`context.validateResult` 值为校验结果列表。
【注意】⚠️ 默认情况,输入框按下 Enter 键会自动触发提交事件,如果希望禁用这个默认行为,可以给输入框添加 enter 事件,并在事件中设置 `e.preventDefault()`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/form/type.ts)。
`interface SubmitContext { e?: FormSubmitEvent; validateResult: FormValidateResult; firstError?: string; fields?: any }`

`type FormValidateResult = boolean \| ValidateResultObj`

`type ValidateResultObj = { [key in keyof T]: boolean \| ValidateResultList }`

`type ValidateResultList = Array`

`type AllValidateResult = CustomValidateObj \| ValidateResultType`

`interface ValidateResultType extends FormRule { result: boolean }`

`type ValidateResult = { [key in keyof T]: boolean \| ErrorList }`

`type ErrorList = Array`
| N +supportNumberKey | Boolean | true | 是否支持使用数字作为表单键值,在1.9.3版本后表单组件支持数字作为键值,若仍需要保留数字作为数组下标,请关闭此API | N +onReset | Function | | TS 类型:`(context: { e?: FormResetEvent }) => void`
表单重置时触发。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N +onSubmit | Function | | TS 类型:`(context: SubmitContext) => void`
表单提交时触发。其中 `context.validateResult` 表示校验结果,`context.firstError` 表示校验不通过的第一个规则提醒。`context.validateResult` 值为 `true` 表示校验通过;如果校验不通过,`context.validateResult` 值为校验结果列表。。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/form/type.ts)。
`interface SubmitContext { e?: FormSubmitEvent; validateResult: FormValidateResult; firstError?: string; fields?: any }`

`type FormValidateResult = boolean \| ValidateResultObj`

`type ValidateResultObj = { [key in keyof T]: boolean \| ValidateResultList }`

`type ValidateResultList = Array`

`type AllValidateResult = CustomValidateObj \| ValidateResultType`

`interface ValidateResultType extends FormRule { result: boolean }`

`type ValidateResult = { [key in keyof T]: boolean \| ErrorList }`

`type ErrorList = Array`
| N onValuesChange | Function | | TS 类型:`(changedValues: Record, allValues: Record) => void`
字段值更新时触发的回调事件 | N ### FormInstanceFunctions 组件实例方法 @@ -121,14 +122,14 @@ onValuesChange | Function | | TS 类型:`(changedValues: Record)` | \- | 必需。清空校验结果。可使用 fields 指定清除部分字段的校验结果,fields 值为空则表示清除所有字段校验结果。清除邮箱校验结果示例:`clearValidate(['email'])` -currentElement | \- | `HTMLFormElement` | 必需。获取 form dom 元素 +currentElement | \- | `HTMLFormElement` | 获取 form dom 元素 +getCurrentElement | \- | `HTMLFormElement` | 获取 form dom 元素 getFieldValue | `(field: NamePath) ` | `unknown` | 必需。获取单个字段值 -getFieldsValue | \- | `getFieldsValue` | 必需。获取一组字段名对应的值,当调用 getFieldsValue(true) 时返回所有表单数据。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/form/type.ts)。
`interface getFieldsValue{ (nameList: true): T; (nameList: any[]): Record;}`
+getFieldsValue | `(nameList: string[] \| boolean)` | `getFieldsValue` | 必需。获取一组字段名对应的值,当调用 getFieldsValue(true) 时返回所有表单数据。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/form/type.ts)。
`interface getFieldsValue{ (nameList: true): T; (nameList: any[]): Record;}`
reset | `(params?: FormResetParams)` | \- | 必需。重置表单,表单里面没有重置按钮`
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index 4487fda12b..e388182fbc 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -220,7 +220,7 @@ exports[`ssr snapshot test > ssr test packages/components/checkbox/_example/base exports[`ssr snapshot test > ssr test packages/components/checkbox/_example/controlled.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test packages/components/checkbox/_example/group.tsx 1`] = `"
选中值: 北京
"`; +exports[`ssr snapshot test > ssr test packages/components/checkbox/_example/group.tsx 1`] = `"
写法一:使用 options
选中值: 广州
写法二:使用插槽
选中值: 上海
"`; exports[`ssr snapshot test > ssr test packages/components/checkbox/_example/link.tsx 1`] = `"
"`; From 429d64ab8e363a5d71c2364663ccbd4289931c8c Mon Sep 17 00:00:00 2001 From: Rylan Date: Sat, 11 Oct 2025 20:51:40 +0800 Subject: [PATCH 6/9] refactor(CheckboxGroup): optimize indeterminate state calculation and checkAll logic --- .../components/checkbox/CheckboxGroup.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/components/checkbox/CheckboxGroup.tsx b/packages/components/checkbox/CheckboxGroup.tsx index 8712a7a346..080aa8fac5 100644 --- a/packages/components/checkbox/CheckboxGroup.tsx +++ b/packages/components/checkbox/CheckboxGroup.tsx @@ -100,14 +100,11 @@ const CheckboxGroup = (props: }, [internalValue]); const checkedSet = useMemo(() => getCheckedSet(), [getCheckedSet]); - const indeterminate = useMemo(() => { - const checkableValues = enabledValues.filter((value) => checkedSet.has(value)); - const checkedDisabledValues = disabledValues.filter((value) => checkedSet.has(value)); - // 存在被禁用且已选中的选项,直接显示半选状态 - if (checkedDisabledValues.length > 0) return true; - // 否则检查未禁用的选项是否处于部分选中状态 - return checkableValues.length !== 0 && checkableValues.length !== enabledValues.length; - }, [checkedSet, enabledValues, disabledValues]); +const indeterminate = useMemo(() => { + const allValues = [...enabledValues, ...disabledValues]; + const checkedCount = allValues.filter((value) => checkedSet.has(value)).length; + return checkedCount > 0 && checkedCount < allValues.length; +}, [checkedSet, enabledValues, disabledValues]); const checkAllChecked = useMemo(() => { const checkableValues = enabledValues.filter((value) => checkedSet.has(value)); @@ -154,18 +151,20 @@ const CheckboxGroup = (props: const checkedSet = getCheckedSet(); if (checkProps.checkAll) { - // 保存被禁用选项的当前状态 - const disabledCheckedValues = disabledValues.filter((value) => checkedSet.has(value)); - // 计算当前启用选项的选中状态 - const checkedenabledOptionsValues = enabledValues.filter((value) => checkedSet.has(value)); - const allEnabledChecked = - enabledValues.length > 0 && checkedenabledOptionsValues.length === enabledValues.length; - - checkedSet.clear(); - // 恢复被禁用选项的原有状态 - disabledCheckedValues.forEach((v) => checkedSet.add(v)); + const checkedEnabledValues = enabledValues.filter((value) => checkedSet.has(value)); + const allEnabledChecked = enabledValues.length > 0 && checkedEnabledValues.length === enabledValues.length; if (!allEnabledChecked) { - enabledValues.forEach((v) => checkedSet.add(v)); + enabledValues.forEach((value) => { + if (!checkedSet.has(value)) { + checkedSet.add(value); + } + }); + } else { + enabledValues.forEach((value) => { + if (checkedSet.has(value)) { + checkedSet.delete(value); + } + }); } } else if (checked) { if (checkedSet.size >= localMax && isNumber(max)) return; From 9ac75ca660748800cddd82c470ac974c341324fa Mon Sep 17 00:00:00 2001 From: Rylan Date: Mon, 20 Oct 2025 17:12:21 +0800 Subject: [PATCH 7/9] docs: revert `onSubmit` --- packages/components/form/form.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/form/form.md b/packages/components/form/form.md index 8b2c9ddcdd..7a9e9fc66c 100644 --- a/packages/components/form/form.md +++ b/packages/components/form/form.md @@ -112,7 +112,7 @@ statusIcon | TNode | undefined | 校验状态图标,值为 `true` 显示默认 submitWithWarningMessage | Boolean | false | 【讨论中】当校验结果只有告警信息时,是否触发 `submit` 提交事件 | N supportNumberKey | Boolean | true | 是否支持使用数字作为表单键值,在1.9.3版本后表单组件支持数字作为键值,若仍需要保留数字作为数组下标,请关闭此API或使用 FormList | N onReset | Function | | TS 类型:`(context: { e?: FormResetEvent }) => void`
表单重置时触发。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N -onSubmit | Function | | TS 类型:`(context: SubmitContext) => void`
表单提交时触发。其中 `context.validateResult` 表示校验结果,`context.firstError` 表示校验不通过的第一个规则提醒。`context.validateResult` 值为 `true` 表示校验通过;如果校验不通过,`context.validateResult` 值为校验结果列表。。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/form/type.ts)。
`interface SubmitContext { e?: FormSubmitEvent; validateResult: FormValidateResult; firstError?: string; fields?: any }`

`type FormValidateResult = boolean \| ValidateResultObj`

`type ValidateResultObj = { [key in keyof T]: boolean \| ValidateResultList }`

`type ValidateResultList = Array`

`type AllValidateResult = CustomValidateObj \| ValidateResultType`

`interface ValidateResultType extends FormRule { result: boolean }`

`type ValidateResult = { [key in keyof T]: boolean \| ErrorList }`

`type ErrorList = Array`
| N +onSubmit | Function | | TS 类型:`(context: SubmitContext) => void`
表单提交时触发。其中 `context.validateResult` 表示校验结果,`context.firstError` 表示校验不通过的第一个规则提醒。`context.validateResult` 值为 `true` 表示校验通过;如果校验不通过,`context.validateResult` 值为校验结果列表。
【注意】⚠️ 默认情况,输入框按下 Enter 键会自动触发提交事件,如果希望禁用这个默认行为,可以给输入框添加 enter 事件,并在事件中设置 `e.preventDefault()`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/form/type.ts)。
`interface SubmitContext { e?: FormSubmitEvent; validateResult: FormValidateResult; firstError?: string; fields?: any }`

`type FormValidateResult = boolean \| ValidateResultObj`

`type ValidateResultObj = { [key in keyof T]: boolean \| ValidateResultList }`

`type ValidateResultList = Array`

`type AllValidateResult = CustomValidateObj \| ValidateResultType`

`interface ValidateResultType extends FormRule { result: boolean }`

`type ValidateResult = { [key in keyof T]: boolean \| ErrorList }`

`type ErrorList = Array`
| N onValuesChange | Function | | TS 类型:`(changedValues: Record, allValues: Record) => void`
字段值更新时触发的回调事件 | N ### FormInstanceFunctions 组件实例方法 From bf8177798187462be9dbd6b69a5a1fe1625b8f68 Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 31 Oct 2025 19:35:16 +0800 Subject: [PATCH 8/9] docs: revert --- packages/components/checkbox/checkbox.en-US.md | 1 - packages/components/form/type.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/checkbox/checkbox.en-US.md b/packages/components/checkbox/checkbox.en-US.md index 09bba83ebe..e224330c89 100644 --- a/packages/components/checkbox/checkbox.en-US.md +++ b/packages/components/checkbox/checkbox.en-US.md @@ -32,7 +32,6 @@ 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 -value | Array | [] | Typescript:`T` `type 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 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/form/type.ts b/packages/components/form/type.ts index 5cf1683468..7b791a006f 100644 --- a/packages/components/form/type.ts +++ b/packages/components/form/type.ts @@ -107,7 +107,7 @@ export interface TdFormProps { */ onReset?: (context: { e?: FormResetEvent }) => void; /** - * 表单提交时触发。其中 `context.validateResult` 表示校验结果,`context.firstError` 表示校验不通过的第一个规则提醒。`context.validateResult` 值为 `true` 表示校验通过;如果校验不通过,`context.validateResult` 值为校验结果列表。 + * 表单提交时触发。其中 `context.validateResult` 表示校验结果,`context.firstError` 表示校验不通过的第一个规则提醒。`context.validateResult` 值为 `true` 表示校验通过;如果校验不通过,`context.validateResult` 值为校验结果列表。
【注意】⚠️ 默认情况,输入框按下 Enter 键会自动触发提交事件,如果希望禁用这个默认行为,可以给输入框添加 enter 事件,并在事件中设置 `e.preventDefault()` */ onSubmit?: (context: SubmitContext) => void; /** From f11b55f82b313f55b149a4b3503e676e5a59336f Mon Sep 17 00:00:00 2001 From: Rylan Date: Fri, 31 Oct 2025 19:38:26 +0800 Subject: [PATCH 9/9] feat(Form): improve pattern validation to handle string regex input --- packages/components/form/formModel.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/components/form/formModel.ts b/packages/components/form/formModel.ts index ed8b36e525..70ef43c441 100644 --- a/packages/components/form/formModel.ts +++ b/packages/components/form/formModel.ts @@ -2,16 +2,17 @@ import isDate from 'validator/lib/isDate'; import isEmail from 'validator/lib/isEmail'; -import { isEmpty, isNumber } from 'lodash-es'; import isURL from 'validator/lib/isURL'; +import { isEmpty, isNumber } from 'lodash-es'; import { getCharacterLength } from '@tdesign/common-js/utils/helper'; -import { + +import type { + AllValidateResult, + CustomValidateResolveType, CustomValidator, FormRule, - ValueType, - AllValidateResult, ValidateResultType, - CustomValidateResolveType, + ValueType, } from './type'; // `{} / [] / '' / undefined / null` 等内容被认为是空; 0 和 false 被认为是正常数据,部分数据的值就是 0 或者 false @@ -48,7 +49,10 @@ const VALIDATE_MAP = { enum: (val: ValueType, strs: Array): boolean => strs.includes(val), idcard: (val: ValueType): boolean => /^(\d{18,18}|\d{15,15}|\d{17,17}x)$/i.test(val), telnumber: (val: ValueType): boolean => /^1[3-9]\d{9}$/.test(val), - pattern: (val: ValueType, regexp: RegExp): boolean => regexp.test(val), + pattern: (val: ValueType, regexp: RegExp | string): boolean => { + const reg = typeof regexp === 'string' ? new RegExp(regexp) : regexp; + return reg.test(val); + }, // 自定义校验规则,可能是异步校验 validator: (val: ValueType, validate: CustomValidator): ReturnType => validate(val), };