Skip to content

Commit e92e6af

Browse files
authored
fix(Select): optimize disabled condition check for multiple selection (#3879)
* fix(Select): improve filter option logic to use dynamic label key * fix(Select): update option selection logic * fix(PopupContent): simplify checkAll logic for selected values * fix(Select): enhance tag rendering logic * fix(Select): improve checkAll handling * fix(Select): update checkAll handling * chore(PopupContent): rename variable for clarity * fix(Select): improve target option retrieval logic for grouped options * refactor: extract key retrieval logic into a utility function * fix(Select): improve target option retrieval logic
1 parent 7693dbc commit e92e6af

File tree

5 files changed

+126
-95
lines changed

5 files changed

+126
-95
lines changed

packages/components/select/base/Option.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, { useEffect, useMemo } from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22
import classNames from 'classnames';
33
import { get, isNumber, isString } from 'lodash-es';
44

55
import useConfig from '../../hooks/useConfig';
66
import useDomRefCallback from '../../hooks/useDomRefCallback';
77
import useRipple from '../../hooks/useRipple';
8+
import { getKeyMapping } from '../util/helper';
89

910
import type { StyledProps } from '../../common';
1011
import type { SelectKeysType, SelectOption, SelectValue, TdOptionProps, TdSelectProps } from '../type';
@@ -58,11 +59,14 @@ const Option: React.FC<SelectOptionProps> = (props) => {
5859
isVirtual,
5960
} = props;
6061

61-
let selected: boolean;
62-
let indeterminate: boolean;
6362
const label = propLabel || value;
6463
const disabled = propDisabled || (multiple && Array.isArray(selectedValue) && max && selectedValue.length >= max);
6564

65+
let selected: boolean;
66+
let indeterminate: boolean;
67+
// 处理存在禁用项时,全选状态无法来回切换的问题
68+
const [allSelectableChecked, setAllSelectableChecked] = useState(!selected);
69+
6670
const titleContent = useMemo(() => {
6771
// 外部设置 props,说明希望受控
6872
const controlledTitle = Reflect.has(props, 'title');
@@ -76,6 +80,7 @@ const Option: React.FC<SelectOptionProps> = (props) => {
7680

7781
// 使用斜八角动画
7882
const [optionRef, setRefCurrent] = useDomRefCallback();
83+
useRipple(optionRef);
7984

8085
useEffect(() => {
8186
if (isVirtual && optionRef) {
@@ -87,23 +92,23 @@ const Option: React.FC<SelectOptionProps> = (props) => {
8792
// eslint-disable-next-line
8893
}, [isVirtual, optionRef]);
8994

90-
useRipple(optionRef);
91-
95+
const { valueKey } = getKeyMapping(keys);
9296
// 处理单选场景
9397
if (!multiple) {
9498
selected =
9599
isNumber(selectedValue) || isString(selectedValue)
96100
? value === selectedValue
97-
: value === get(selectedValue, keys?.value || 'value');
101+
: value === get(selectedValue, valueKey);
98102
}
103+
99104
// 处理多选场景
100105
if (multiple && Array.isArray(selectedValue)) {
101106
selected = selectedValue.some((item) => {
102107
if (isNumber(item) || isString(item)) {
103108
// 如果非 object 类型
104109
return item === value;
105110
}
106-
return get(item, keys?.value || 'value') === value;
111+
return get(item, valueKey) === value;
107112
});
108113
if (props.checkAll) {
109114
selected = selectedValue.length === props.optionLength;
@@ -116,7 +121,8 @@ const Option: React.FC<SelectOptionProps> = (props) => {
116121
onSelect(value, { label: String(label), selected, event, restData });
117122
}
118123
if (checkAll) {
119-
props.onCheckAllChange?.(selected, event);
124+
props.onCheckAllChange?.(allSelectableChecked, event);
125+
setAllSelectableChecked(!allSelectableChecked);
120126
}
121127
};
122128

packages/components/select/base/PopupContent.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,16 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
9696
size,
9797
});
9898

99-
// 全部可选选项
100-
const selectableOptions = useMemo(() => {
99+
const optionsExcludedCheckAll = useMemo(() => {
101100
const uniqueOptions = {};
102101
propsOptions?.forEach((option: SelectOption) => {
103102
if ((option as SelectOptionGroup).group) {
104103
(option as SelectOptionGroup).children.forEach((item) => {
105-
if (!item.disabled && !item.checkAll) {
104+
if (!item.checkAll) {
106105
uniqueOptions[item.value] = item;
107106
}
108107
});
109-
} else if (!(option as TdOptionProps).disabled && !(option as TdOptionProps).checkAll) {
108+
} else if (!(option as TdOptionProps).checkAll) {
110109
uniqueOptions[(option as TdOptionProps).value] = option;
111110
}
112111
});
@@ -186,7 +185,7 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
186185
value={optionValue}
187186
onSelect={onSelect}
188187
selectedValue={value}
189-
optionLength={selectableOptions.length}
188+
optionLength={optionsExcludedCheckAll.length}
190189
multiple={multiple}
191190
size={size}
192191
disabled={disabled}

packages/components/select/base/Select.tsx

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React, {
22
Children,
3-
KeyboardEvent,
4-
WheelEvent,
53
cloneElement,
64
isValidElement,
75
useCallback,
@@ -27,7 +25,7 @@ import SelectInput, { type SelectInputValue, type SelectInputValueChangeContext
2725
import Tag from '../../tag';
2826
import { selectDefaultProps } from '../defaultProps';
2927
import useOptions, { isSelectOptionGroup } from '../hooks/useOptions';
30-
import { getSelectValueArr, getSelectedOptions } from '../util/helper';
28+
import { getKeyMapping, getSelectValueArr, getSelectedOptions } from '../util/helper';
3129
import Option from './Option';
3230
import OptionGroup from './OptionGroup';
3331
import PopupContent from './PopupContent';
@@ -121,10 +119,11 @@ const Select = forwardRefWithStatics(
121119
);
122120

123121
const selectedLabel = useMemo(() => {
122+
const { labelKey } = getKeyMapping(keys);
124123
if (multiple) {
125-
return selectedOptions.map((selectedOption) => get(selectedOption || {}, keys?.label || 'label') || '');
124+
return selectedOptions.map((selectedOption) => get(selectedOption || {}, labelKey) || '');
126125
}
127-
return get(selectedOptions[0] || {}, keys?.label || 'label') || undefined;
126+
return get(selectedOptions[0] || {}, labelKey) || undefined;
128127
}, [selectedOptions, keys, multiple]);
129128

130129
const handleShowPopup = (visible: boolean, ctx: PopupVisibleChangeContext) => {
@@ -188,37 +187,40 @@ const Select = forwardRefWithStatics(
188187
return;
189188
}
190189

191-
const isSelectableOption = (opt: TdOptionProps) => !opt.checkAll && !opt.disabled;
192-
const getOptionValue = (option: SelectOption) =>
193-
valueType === 'object' ? option : option[keys?.value || 'value'];
190+
const { valueKey } = getKeyMapping(keys);
191+
const isObjectType = valueType === 'object';
194192

195-
const values = [];
196-
currentOptions.forEach((option) => {
197-
if (isSelectOptionGroup(option)) {
198-
option.children.forEach((item) => {
199-
if (isSelectableOption(item)) {
200-
values.push(getOptionValue(item));
201-
}
202-
});
203-
} else if (isSelectableOption(option)) {
204-
values.push(getOptionValue(option));
193+
const enabledOptions = currentOptions.filter(
194+
(opt) => !isSelectOptionGroup(opt) && !opt.checkAll && !opt.disabled,
195+
);
196+
197+
const currentValues = Array.isArray(value) ? value : [];
198+
const disabledSelectedOptions = currentOptions.filter((opt) => {
199+
if (isSelectOptionGroup(opt) || opt.checkAll) return false;
200+
if (!opt.disabled) return false;
201+
if (isObjectType) {
202+
return currentValues.some((v) => get(v, valueKey) === opt[valueKey]);
205203
}
204+
return currentValues.includes(opt[valueKey]);
206205
});
207206

208-
const { currentSelectedOptions, allSelectedValue } = getSelectedOptions(
209-
values,
210-
multiple,
211-
valueType,
212-
keys,
213-
valueToOption,
214-
);
207+
let checkAllValue: SelectValue[];
208+
209+
if (checkAll) {
210+
// 全选:选中所有未禁用的选项 + 保留已选中的禁用选项
211+
const enabledValues = enabledOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
212+
const disabledValues = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
213+
checkAllValue = [...disabledValues, ...enabledValues];
214+
} else {
215+
// 取消全选:只保留已选中的禁用选项
216+
checkAllValue = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
217+
}
215218

216-
const checkAllValue =
217-
!checkAll && allSelectedValue.length !== (props.value as Array<SelectOption>)?.length ? allSelectedValue : [];
219+
const { currentSelectedOptions } = getSelectedOptions(checkAllValue, multiple, valueType, keys, valueToOption);
218220

219221
onChange?.(checkAllValue, {
220222
e,
221-
trigger: !checkAll ? 'check' : 'uncheck',
223+
trigger: checkAll ? 'check' : 'uncheck',
222224
selectedOptions: currentSelectedOptions,
223225
});
224226
};
@@ -323,7 +325,7 @@ const Select = forwardRefWithStatics(
323325
return;
324326
}
325327
if (isFunction(onSearch)) {
326-
onSearch(value, { e: context.e as KeyboardEvent<HTMLDivElement> });
328+
onSearch(value, { e: context.e as React.KeyboardEvent<HTMLDivElement> });
327329
return;
328330
}
329331
};
@@ -403,18 +405,22 @@ const Select = forwardRefWithStatics(
403405
return '';
404406
}
405407
return ({ value: val }) =>
406-
val.slice(0, minCollapsedNum ? minCollapsedNum : val.length).map((v: string, key: number) => {
407-
const filterOption: SelectOption & { disabled?: boolean } = options?.find((option) => option.label === v);
408+
val.slice(0, minCollapsedNum ? minCollapsedNum : val.length).map((_, index: number) => {
409+
const { valueKey, labelKey, disabledKey } = getKeyMapping(keys);
410+
const targetVal = get(selectedOptions[index], valueKey);
411+
const targetLabel = get(selectedOptions[index], labelKey);
412+
const targetOption = valueToOption[targetVal];
413+
if (!targetOption) return null;
408414
return (
409415
<Tag
410-
key={key}
411-
closable={!filterOption?.disabled && !disabled && !readonly}
416+
key={index}
417+
closable={!get(targetOption, disabledKey) && !disabled && !readonly}
412418
size={size}
413419
{...tagProps}
414420
onClose={({ e }) => {
415421
e.stopPropagation();
416422
e?.nativeEvent?.stopImmediatePropagation?.();
417-
const values = getSelectValueArr(value, value[key], true, valueType, keys);
423+
const values = getSelectValueArr(value, value[index], true, valueType, keys);
418424

419425
const { currentSelectedOptions } = getSelectedOptions(
420426
values,
@@ -432,13 +438,13 @@ const Select = forwardRefWithStatics(
432438
tagProps?.onClose?.({ e });
433439

434440
onRemove?.({
435-
value: value[key],
436-
data: { label: v, value: value[key] },
441+
value: targetVal,
442+
data: { label: targetLabel, value: targetVal },
437443
e: e as unknown as React.MouseEvent<HTMLDivElement, MouseEvent>,
438444
});
439445
}}
440446
>
441-
{v}
447+
{targetLabel}
442448
</Tag>
443449
);
444450
});
@@ -496,11 +502,11 @@ const Select = forwardRefWithStatics(
496502

497503
const { onMouseEnter, onMouseLeave } = props;
498504

499-
const handleEnter = (_, context: { inputValue: string; e: KeyboardEvent<HTMLDivElement> }) => {
505+
const handleEnter = (_, context: { inputValue: string; e: React.KeyboardEvent<HTMLDivElement> }) => {
500506
onEnter?.({ ...context, value });
501507
};
502508

503-
const handleScroll = ({ e }: { e: WheelEvent<HTMLDivElement> }) => {
509+
const handleScroll = ({ e }: { e: React.WheelEvent<HTMLDivElement> }) => {
504510
toggleIsScrolling(true);
505511

506512
onScroll?.({ e });

packages/components/select/hooks/useOptions.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { useState, useEffect, ReactNode, ReactElement } from 'react';
21
import { get } from 'lodash-es';
3-
import type { SelectKeysType, SelectOption, SelectOptionGroup, SelectValue, TdOptionProps } from '../type';
4-
import { getValueToOption, type ValueToOption } from '../util/helper';
2+
import React, { ReactElement, ReactNode, useEffect, useState } from 'react';
53
import Option from '../base/Option';
64
import OptionGroup from '../base/OptionGroup';
5+
import { getKeyMapping, getValueToOption, type ValueToOption } from '../util/helper';
6+
7+
import type { SelectKeysType, SelectOption, SelectOptionGroup, SelectValue, TdOptionProps } from '../type';
78

89
export function isSelectOptionGroup(option: SelectOption): option is SelectOptionGroup {
910
return !!option && 'group' in option && 'children' in option;
@@ -12,7 +13,7 @@ export function isSelectOptionGroup(option: SelectOption): option is SelectOptio
1213
type OptionValueType = SelectValue<SelectOption>;
1314

1415
// 处理 options 的逻辑
15-
function UseOptions(
16+
function useOptions(
1617
keys: SelectKeysType,
1718
options: SelectOption[],
1819
children: ReactNode,
@@ -54,11 +55,12 @@ function UseOptions(
5455
transformedOptions = arrayChildren?.map<SelectOption>((v) => handlerOptionElement(v));
5556
}
5657
if (keys) {
58+
const { valueKey, labelKey } = getKeyMapping(keys);
5759
// 如果有定制 keys 先做转换
5860
transformedOptions = transformedOptions?.map<SelectOption>((option) => ({
5961
...option,
60-
value: get(option, keys?.value || 'value'),
61-
label: get(option, keys?.label || 'label'),
62+
value: get(option, valueKey),
63+
label: get(option, labelKey),
6264
}));
6365
}
6466
setCurrentOptions(transformedOptions);
@@ -70,8 +72,7 @@ function UseOptions(
7072

7173
// 同步 value 对应的 options
7274
useEffect(() => {
73-
const valueKey = keys?.value || 'value';
74-
const labelKey = keys?.label || 'label';
75+
const { valueKey, labelKey } = getKeyMapping(keys);
7576

7677
setSelectedOptions((oldSelectedOptions: SelectOption[]) => {
7778
const createOptionFromValue = (item: OptionValueType) => {
@@ -117,4 +118,4 @@ function UseOptions(
117118
};
118119
}
119120

120-
export default UseOptions;
121+
export default useOptions;

0 commit comments

Comments
 (0)