Skip to content

Commit 97f150c

Browse files
authored
feat(CheckboxGroup/Form): add readonly prop (#3885)
* fix(CheckboxGroup): enhance handling of disabled options * feat(CheckboxGroup): add `readonly` prop * feat(Form): add `readonly` prop * docs(Form): revert API descriptions * chore: update snapshots * refactor(CheckboxGroup): optimize indeterminate state calculation and checkAll logic * docs: revert `onSubmit` * docs: revert * feat(Form): improve pattern validation to handle string regex input
1 parent cab9d43 commit 97f150c

File tree

14 files changed

+396
-171
lines changed

14 files changed

+396
-171
lines changed

packages/components/checkbox/CheckboxGroup.tsx

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
22
import classNames from 'classnames';
33
import { isNumber } from 'lodash-es';
4+
import { CheckContext, type CheckContextValue, type CheckProps } from '../common/Check';
45
import useConfig from '../hooks/useConfig';
5-
import { CheckContext, CheckContextValue, CheckProps } from '../common/Check';
6-
import { CheckboxGroupValue, CheckboxOption, CheckboxOptionObj, TdCheckboxGroupProps, TdCheckboxProps } from './type';
7-
import { StyledProps } from '../common';
86
import useControlled from '../hooks/useControlled';
7+
import useDefaultProps from '../hooks/useDefaultProps';
98
import Checkbox from './Checkbox';
109
import { checkboxGroupDefaultProps } from './defaultProps';
11-
import useDefaultProps from '../hooks/useDefaultProps';
1210

11+
import type { StyledProps } from '../common';
1312
import type { CheckboxProps } from './Checkbox';
13+
import type {
14+
CheckboxGroupValue,
15+
CheckboxOption,
16+
CheckboxOptionObj,
17+
TdCheckboxGroupProps,
18+
TdCheckboxProps,
19+
} from './type';
1420

1521
export interface CheckboxGroupProps<T extends CheckboxGroupValue = CheckboxGroupValue>
1622
extends TdCheckboxGroupProps<T>,
1723
StyledProps {
1824
children?: React.ReactNode;
1925
}
2026

21-
// 将 checkBox 的 value 转换为 string|number
22-
const getCheckboxValue = (v: CheckboxOption): string | number => {
27+
const getCheckboxValue = (v: CheckboxOption) => {
2328
switch (typeof v) {
2429
case 'number':
2530
return v as number;
@@ -48,6 +53,7 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
4853
children,
4954
max,
5055
options = [],
56+
readonly,
5157
} = useDefaultProps<CheckboxGroupProps<T>>(props, checkboxGroupDefaultProps);
5258

5359
// 去掉所有 checkAll 之后的 options
@@ -67,6 +73,22 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
6773
optionsWithoutCheckAllValues.push(vs);
6874
});
6975

76+
const { enabledValues, disabledValues } = useMemo(() => {
77+
const enabledValues = [];
78+
const disabledValues = [];
79+
optionsWithoutCheckAll.forEach((option) => {
80+
const isOptionDisabled = typeof option === 'object' && (option.disabled || option.readonly);
81+
const value = getCheckboxValue(option);
82+
83+
if (isOptionDisabled || disabled || readonly) {
84+
disabledValues.push(value);
85+
} else {
86+
enabledValues.push(value);
87+
}
88+
});
89+
return { enabledValues, disabledValues };
90+
}, [optionsWithoutCheckAll, disabled, readonly]);
91+
7092
const [internalValue, setInternalValue] = useControlled(props, 'value', onChange);
7193
const [localMax, setLocalMax] = useState(max);
7294

@@ -78,16 +100,16 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
78100
}, [internalValue]);
79101
const checkedSet = useMemo(() => getCheckedSet(), [getCheckedSet]);
80102

81-
// 用于决定全选状态的属性
82-
const indeterminate = useMemo(() => {
83-
const list = Array.from(checkedSet);
84-
return list.length !== 0 && list.length !== optionsWithoutCheckAll.length;
85-
}, [checkedSet, optionsWithoutCheckAll]);
103+
const indeterminate = useMemo(() => {
104+
const allValues = [...enabledValues, ...disabledValues];
105+
const checkedCount = allValues.filter((value) => checkedSet.has(value)).length;
106+
return checkedCount > 0 && checkedCount < allValues.length;
107+
}, [checkedSet, enabledValues, disabledValues]);
86108

87109
const checkAllChecked = useMemo(() => {
88-
const list = Array.from(checkedSet);
89-
return list.length === optionsWithoutCheckAll.length;
90-
}, [checkedSet, optionsWithoutCheckAll]);
110+
const checkableValues = enabledValues.filter((value) => checkedSet.has(value));
111+
return enabledValues.length > 0 && checkableValues.length === enabledValues.length;
112+
}, [checkedSet, enabledValues]);
91113

92114
useEffect(() => {
93115
if (!isNumber(max)) {
@@ -120,18 +142,28 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
120142
checked: checkProps.checkAll ? checkAllChecked : checkedSet.has(checkValue),
121143
indeterminate: checkProps.checkAll ? indeterminate : checkProps.indeterminate,
122144
disabled: checkProps.disabled || disabled || (checkedSet.size >= localMax && !checkedSet.has(checkValue)),
145+
readonly: checkProps.readonly || readonly,
123146
onChange(checked, { e }) {
124147
if (typeof checkProps.onChange === 'function') {
125148
checkProps.onChange(checked, { e });
126149
}
127150

128151
const checkedSet = getCheckedSet();
129-
// 全选时的逻辑处理
152+
130153
if (checkProps.checkAll) {
131-
checkedSet.clear();
132-
if (checked) {
133-
optionsWithoutCheckAllValues.forEach((v) => {
134-
checkedSet.add(v);
154+
const checkedEnabledValues = enabledValues.filter((value) => checkedSet.has(value));
155+
const allEnabledChecked = enabledValues.length > 0 && checkedEnabledValues.length === enabledValues.length;
156+
if (!allEnabledChecked) {
157+
enabledValues.forEach((value) => {
158+
if (!checkedSet.has(value)) {
159+
checkedSet.add(value);
160+
}
161+
});
162+
} else {
163+
enabledValues.forEach((value) => {
164+
if (checkedSet.has(value)) {
165+
checkedSet.delete(value);
166+
}
135167
});
136168
}
137169
} else if (checked) {
@@ -183,7 +215,12 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
183215
return vs.checkAll ? (
184216
<Checkbox {...vs} key={`checkAll_${index}`} indeterminate={indeterminate} />
185217
) : (
186-
<Checkbox {...vs} key={index} disabled={vs.disabled || disabled} />
218+
<Checkbox
219+
{...vs}
220+
key={index}
221+
disabled={vs.disabled || disabled}
222+
readonly={vs.readonly || readonly}
223+
/>
187224
);
188225
}
189226
default:
Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import React, { useState } from 'react';
2-
import { Checkbox, Space } from 'tdesign-react';
2+
import { Checkbox, Divider, Space } from 'tdesign-react';
33

44
const options = [
5+
{
6+
label: '全选',
7+
checkAll: true,
8+
},
59
{
610
value: '北京',
711
label: '北京',
@@ -13,34 +17,59 @@ const options = [
1317
{
1418
value: '广州',
1519
label: '广州',
16-
},
17-
{
18-
label: '全选',
19-
checkAll: true as const,
20+
disabled: true,
2021
},
2122
];
2223

2324
export default function CheckboxExample() {
2425
const [disabled, setDisabled] = useState(false);
25-
const [city, setCity] = useState(['北京']);
26+
const [city, setCity] = useState(['广州']);
27+
const [city2, setCity2] = useState(['上海']);
2628

2729
return (
2830
<Space direction="vertical">
29-
<div>选中值: {city.join('、')}</div>
30-
<div>
31-
<Checkbox checked={disabled} onChange={(value) => setDisabled(value)}>
32-
禁用全部
33-
</Checkbox>
34-
</div>
35-
36-
<Checkbox.Group
37-
disabled={disabled}
38-
value={city}
31+
<Checkbox
32+
checked={disabled}
3933
onChange={(value) => {
40-
setCity(value);
34+
setDisabled(value);
4135
}}
42-
options={options}
43-
/>
36+
>
37+
禁用全部
38+
</Checkbox>
39+
40+
<Space direction="vertical">
41+
<strong>写法一:使用 options</strong>
42+
<div>选中值: {city.join('、')}</div>
43+
<Checkbox.Group
44+
disabled={disabled}
45+
value={city}
46+
onChange={(value) => {
47+
setCity(value);
48+
}}
49+
options={options}
50+
/>
51+
</Space>
52+
53+
<Divider />
54+
55+
<Space direction="vertical">
56+
<strong>写法二:使用插槽</strong>
57+
<div>选中值: {city2.join('、')}</div>
58+
<Checkbox.Group
59+
disabled={disabled}
60+
value={city2}
61+
onChange={(value) => {
62+
setCity2(value);
63+
}}
64+
>
65+
<Checkbox checkAll>全选</Checkbox>
66+
<Checkbox value="北京">北京</Checkbox>
67+
<Checkbox value="上海">上海</Checkbox>
68+
<Checkbox value="广州" disabled>
69+
广州
70+
</Checkbox>
71+
</Checkbox.Group>
72+
</Space>
4473
</Space>
4574
);
4675
}

packages/components/checkbox/checkbox.en-US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ disabled | Boolean | - | \- | N
3131
max | Number | undefined | \- | N
3232
name | String | - | \- | N
3333
options | Array | - | Typescript:`Array<CheckboxOption>` `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
34+
readonly | Boolean | undefined | \- | N
3435
value | Array | [] | Typescript:`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N
3536
defaultValue | Array | [] | uncontrolled property。Typescript:`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N
3637
onChange | Function | | Typescript:`(value: T, context: CheckboxGroupChangeContext) => void`<br/>[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts)。<br/>`interface CheckboxGroupChangeContext { e: ChangeEvent; current: CheckboxOption \| TdCheckboxProps; type: 'check' \| 'uncheck' }`<br/> | N

packages/components/checkbox/checkbox.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ disabled | Boolean | - | 是否禁用组件,默认为 false。CheckboxGroup.di
3636
max | Number | undefined | 支持最多选中的数量 | N
3737
name | String | - | 统一设置内部复选框 HTML 属性 | N
3838
options | Array | - | 以配置形式设置子元素。示例1:`['北京', '上海']` ,示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」。TS 类型:`Array<CheckboxOption>` `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
39+
readonly | Boolean | undefined | 只读状态 | N
3940
value | Array | [] | 选中值。TS 类型:`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N
4041
defaultValue | Array | [] | 选中值。非受控属性。TS 类型:`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N
4142
onChange | Function | | TS 类型:`(value: T, context: CheckboxGroupChangeContext) => void`<br/>值变化时触发,`context.current` 表示当前变化的数据值,如果是全选则为空;`context.type` 表示引起选中数据变化的是选中或是取消选中;`context.option` 表示当前变化的数据项。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts)。<br/>`interface CheckboxGroupChangeContext { e: ChangeEvent; current: CheckboxOption \| TdCheckboxProps; type: 'check' \| 'uncheck' }`<br/> | N

packages/components/checkbox/type.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export interface TdCheckboxGroupProps<T = CheckboxGroupValue> {
8787
* 以配置形式设置子元素。示例1:`['北京', '上海']` ,示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」
8888
*/
8989
options?: Array<CheckboxOption>;
90+
/**
91+
* 只读状态
92+
*/
93+
readonly?: boolean;
9094
/**
9195
* 选中值
9296
* @default []
@@ -107,10 +111,11 @@ export type CheckboxOption = string | number | CheckboxOptionObj;
107111

108112
export interface CheckboxOptionObj {
109113
label?: string | TNode;
110-
value?: string | number;
114+
value?: string | number | boolean;
111115
disabled?: boolean;
116+
readonly?: boolean;
112117
name?: string;
113-
checkAll?: true;
118+
checkAll?: boolean;
114119
}
115120

116121
export type CheckboxGroupValue = Array<string | number | boolean>;

packages/components/form/Form.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import React, { useEffect, useImperativeHandle, useRef } from 'react';
1+
import React, { useRef, useImperativeHandle, useEffect } from 'react';
22
import classNames from 'classnames';
3-
import useConfig from '../hooks/useConfig';
4-
import noop from '../_util/noop';
53
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
6-
import type { TdFormProps } from './type';
7-
import useInstance from './hooks/useInstance';
8-
import useForm, { HOOK_MARK } from './hooks/useForm';
9-
import useWatch from './hooks/useWatch';
10-
import { StyledProps } from '../common';
4+
import noop from '../_util/noop';
5+
import useConfig from '../hooks/useConfig';
6+
import useDefaultProps from '../hooks/useDefaultProps';
117
import FormContext from './FormContext';
128
import FormItem from './FormItem';
139
import FormList from './FormList';
1410
import { formDefaultProps } from './defaultProps';
15-
import useDefaultProps from '../hooks/useDefaultProps';
11+
import useForm, { HOOK_MARK } from './hooks/useForm';
12+
import useInstance from './hooks/useInstance';
13+
import useWatch from './hooks/useWatch';
14+
15+
import type { StyledProps } from '../common';
16+
import type { TdFormProps } from './type';
1617

1718
export interface FormProps extends TdFormProps, StyledProps {
1819
children?: React.ReactNode;
@@ -39,6 +40,7 @@ const Form = forwardRefWithStatics(
3940
rules,
4041
errorMessage = globalFormConfig.errorMessage,
4142
disabled,
43+
readonly,
4244
children,
4345
id,
4446
onReset,
@@ -96,6 +98,7 @@ const Form = forwardRefWithStatics(
9698
resetType,
9799
rules,
98100
disabled,
101+
readonly,
99102
formMapRef,
100103
floatingFormDataRef,
101104
onFormItemValueChange,

packages/components/form/FormContext.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
2-
import { TdFormProps, TdFormListProps, NamePath } from './type';
3-
import { FormItemInstance } from './FormItem';
4-
import { InternalFormInstance } from './hooks/interface';
2+
import type { FormItemInstance } from './FormItem';
3+
import type { InternalFormInstance } from './hooks/interface';
4+
import type { NamePath, TdFormListProps, TdFormProps } from './type';
55

66
const FormContext = React.createContext<{
77
form?: InternalFormInstance;
@@ -17,6 +17,7 @@ const FormContext = React.createContext<{
1717
showErrorMessage: TdFormProps['showErrorMessage'];
1818
resetType: TdFormProps['resetType'];
1919
disabled: TdFormProps['disabled'];
20+
readonly: TdFormProps['readonly'];
2021
rules: TdFormProps['rules'];
2122
errorMessage: TdFormProps['errorMessage'];
2223
formMapRef: React.RefObject<Map<any, React.RefObject<FormItemInstance>>>;
@@ -35,6 +36,7 @@ const FormContext = React.createContext<{
3536
showErrorMessage: undefined,
3637
resetType: 'empty',
3738
disabled: undefined,
39+
readonly: undefined,
3840
rules: undefined,
3941
errorMessage: undefined,
4042
statusIcon: undefined,

packages/components/form/FormItem.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
ErrorCircleFilledIcon as TdErrorCircleFilledIcon,
66
} from 'tdesign-icons-react';
77
import { flattenDeep, get, isEqual, isFunction, isObject, isString, merge, set, unset } from 'lodash-es';
8-
import { StyledProps } from '../common';
98
import useConfig from '../hooks/useConfig';
109
import useDefaultProps from '../hooks/useDefaultProps';
1110
import useGlobalIcon from '../hooks/useGlobalIcon';
@@ -17,6 +16,9 @@ import { parseMessage, validate as validateModal } from './formModel';
1716
import { HOOK_MARK } from './hooks/useForm';
1817
import useFormItemInitialData, { ctrlKeyMap } from './hooks/useFormItemInitialData';
1918
import useFormItemStyle from './hooks/useFormItemStyle';
19+
import { calcFieldValue } from './utils';
20+
21+
import type { StyledProps } from '../common';
2022
import type {
2123
FormInstanceFunctions,
2224
FormItemValidateMessage,
@@ -25,7 +27,6 @@ import type {
2527
TdFormItemProps,
2628
ValueType,
2729
} from './type';
28-
import { calcFieldValue } from './utils';
2930

3031
export interface FormItemProps extends TdFormItemProps, StyledProps {
3132
children?: React.ReactNode | React.ReactNode[] | ((form: FormInstanceFunctions) => React.ReactElement);
@@ -65,6 +66,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
6566
labelWidth: labelWidthFromContext,
6667
showErrorMessage: showErrorMessageFromContext,
6768
disabled: disabledFromContext,
69+
readonly: readonlyFromContext,
6870
resetType: resetTypeFromContext,
6971
rules: rulesFromContext,
7072
statusIcon: statusIconFromContext,
@@ -521,6 +523,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
521523
const childProps = child.props as any;
522524
return React.cloneElement(child, {
523525
disabled: disabledFromContext,
526+
readonly: readonlyFromContext,
524527
...childProps,
525528
[ctrlKey]: formValue,
526529
onChange: (value: any, ...args: any[]) => {

packages/components/form/form.en-US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ labelAlign | String | right | options: left/right/top | N
1919
labelWidth | String / Number | '100px' | \- | N
2020
layout | String | vertical | options: vertical/inline | N
2121
preventSubmitDefault | Boolean | true | \- | N
22+
readonly | Boolean | undefined | \- | N
2223
requiredMark | Boolean | true | \- | N
2324
requiredMarkPosition | String | left | Display position of required symbols。options: left/right | N
2425
resetType | String | empty | options: empty/initial | N

0 commit comments

Comments
 (0)