Skip to content
77 changes: 57 additions & 20 deletions packages/components/checkbox/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
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<T extends CheckboxGroupValue = CheckboxGroupValue>
extends TdCheckboxGroupProps<T>,
StyledProps {
children?: React.ReactNode;
}

// 将 checkBox 的 value 转换为 string|number
const getCheckboxValue = (v: CheckboxOption): string | number => {
const getCheckboxValue = (v: CheckboxOption) => {
switch (typeof v) {
case 'number':
return v as number;
Expand Down Expand Up @@ -48,6 +53,7 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
children,
max,
options = [],
readonly,
} = useDefaultProps<CheckboxGroupProps<T>>(props, checkboxGroupDefaultProps);

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

const { enabledValues, disabledValues } = useMemo(() => {
const enabledValues = [];
const disabledValues = [];
optionsWithoutCheckAll.forEach((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 { enabledValues, disabledValues };
}, [optionsWithoutCheckAll, disabled, readonly]);

const [internalValue, setInternalValue] = useControlled(props, 'value', onChange);
const [localMax, setLocalMax] = useState(max);

Expand All @@ -78,16 +100,16 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(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 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 list = Array.from(checkedSet);
return list.length === optionsWithoutCheckAll.length;
}, [checkedSet, optionsWithoutCheckAll]);
const checkableValues = enabledValues.filter((value) => checkedSet.has(value));
return enabledValues.length > 0 && checkableValues.length === enabledValues.length;
}, [checkedSet, enabledValues]);

useEffect(() => {
if (!isNumber(max)) {
Expand Down Expand Up @@ -120,18 +142,28 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(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 });
}

const checkedSet = getCheckedSet();
// 全选时的逻辑处理

if (checkProps.checkAll) {
checkedSet.clear();
if (checked) {
optionsWithoutCheckAllValues.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((value) => {
if (!checkedSet.has(value)) {
checkedSet.add(value);
}
});
} else {
enabledValues.forEach((value) => {
if (checkedSet.has(value)) {
checkedSet.delete(value);
}
});
}
} else if (checked) {
Expand Down Expand Up @@ -183,7 +215,12 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
return vs.checkAll ? (
<Checkbox {...vs} key={`checkAll_${index}`} indeterminate={indeterminate} />
) : (
<Checkbox {...vs} key={index} disabled={vs.disabled || disabled} />
<Checkbox
{...vs}
key={index}
disabled={vs.disabled || disabled}
readonly={vs.readonly || readonly}
/>
);
}
default:
Expand Down
67 changes: 48 additions & 19 deletions packages/components/checkbox/_example/group.tsx
Original file line number Diff line number Diff line change
@@ -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: '北京',
Expand All @@ -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 (
<Space direction="vertical">
<div>选中值: {city.join('、')}</div>
<div>
<Checkbox checked={disabled} onChange={(value) => setDisabled(value)}>
禁用全部
</Checkbox>
</div>

<Checkbox.Group
disabled={disabled}
value={city}
<Checkbox
checked={disabled}
onChange={(value) => {
setCity(value);
setDisabled(value);
}}
options={options}
/>
>
禁用全部
</Checkbox>

<Space direction="vertical">
<strong>写法一:使用 options</strong>
<div>选中值: {city.join('、')}</div>
<Checkbox.Group
disabled={disabled}
value={city}
onChange={(value) => {
setCity(value);
}}
options={options}
/>
</Space>

<Divider />

<Space direction="vertical">
<strong>写法二:使用插槽</strong>
<div>选中值: {city2.join('、')}</div>
<Checkbox.Group
disabled={disabled}
value={city2}
onChange={(value) => {
setCity2(value);
}}
>
<Checkbox checkAll>全选</Checkbox>
<Checkbox value="北京">北京</Checkbox>
<Checkbox value="上海">上海</Checkbox>
<Checkbox value="广州" disabled>
广州
</Checkbox>
</Checkbox.Group>
</Space>
</Space>
);
}
1 change: 1 addition & 0 deletions packages/components/checkbox/checkbox.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ disabled | Boolean | - | \- | N
max | Number | undefined | \- | N
name | String | - | \- | N
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
readonly | Boolean | undefined | \- | N
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
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
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
1 change: 1 addition & 0 deletions packages/components/checkbox/checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
readonly | Boolean | undefined | 只读状态 | N
value | Array | [] | 选中值。TS 类型:`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N
defaultValue | Array | [] | 选中值。非受控属性。TS 类型:`T` `type CheckboxGroupValue = Array<string \| number \| boolean>`。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/checkbox/type.ts) | N
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
9 changes: 7 additions & 2 deletions packages/components/checkbox/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export interface TdCheckboxGroupProps<T = CheckboxGroupValue> {
* 以配置形式设置子元素。示例1:`['北京', '上海']` ,示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」
*/
options?: Array<CheckboxOption>;
/**
* 只读状态
*/
readonly?: boolean;
/**
* 选中值
* @default []
Expand All @@ -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<string | number | boolean>;
Expand Down
21 changes: 12 additions & 9 deletions packages/components/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React, { useEffect, useImperativeHandle, useRef } from 'react';
import React, { useRef, useImperativeHandle, useEffect } 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;
Expand All @@ -39,6 +40,7 @@ const Form = forwardRefWithStatics(
rules,
errorMessage = globalFormConfig.errorMessage,
disabled,
readonly,
children,
id,
onReset,
Expand Down Expand Up @@ -96,6 +98,7 @@ const Form = forwardRefWithStatics(
resetType,
rules,
disabled,
readonly,
formMapRef,
floatingFormDataRef,
onFormItemValueChange,
Expand Down
8 changes: 5 additions & 3 deletions packages/components/form/FormContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Map<any, React.RefObject<FormItemInstance>>>;
Expand All @@ -35,6 +36,7 @@ const FormContext = React.createContext<{
showErrorMessage: undefined,
resetType: 'empty',
disabled: undefined,
readonly: undefined,
rules: undefined,
errorMessage: undefined,
statusIcon: undefined,
Expand Down
7 changes: 5 additions & 2 deletions packages/components/form/FormItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -65,6 +66,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
labelWidth: labelWidthFromContext,
showErrorMessage: showErrorMessageFromContext,
disabled: disabledFromContext,
readonly: readonlyFromContext,
resetType: resetTypeFromContext,
rules: rulesFromContext,
statusIcon: statusIconFromContext,
Expand Down Expand Up @@ -521,6 +523,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
const childProps = child.props as any;
return React.cloneElement(child, {
disabled: disabledFromContext,
readonly: readonlyFromContext,
...childProps,
[ctrlKey]: formValue,
onChange: (value: any, ...args: any[]) => {
Expand Down
1 change: 1 addition & 0 deletions packages/components/form/form.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading