Skip to content
Merged
22 changes: 14 additions & 8 deletions packages/components/select/base/Option.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import { get, isNumber, isString } from 'lodash-es';

import useConfig from '../../hooks/useConfig';
import useDomRefCallback from '../../hooks/useDomRefCallback';
import useRipple from '../../hooks/useRipple';
import { getKeyMapping } from '../util/helper';

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

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

let selected: boolean;
let indeterminate: boolean;
// 处理存在禁用项时,全选状态无法来回切换的问题
const [allSelectableChecked, setAllSelectableChecked] = useState(!selected);

const titleContent = useMemo(() => {
// 外部设置 props,说明希望受控
const controlledTitle = Reflect.has(props, 'title');
Expand All @@ -76,6 +80,7 @@ const Option: React.FC<SelectOptionProps> = (props) => {

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

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

useRipple(optionRef);

const { valueKey } = getKeyMapping(keys);
// 处理单选场景
if (!multiple) {
selected =
isNumber(selectedValue) || isString(selectedValue)
? value === selectedValue
: value === get(selectedValue, keys?.value || 'value');
: value === get(selectedValue, valueKey);
}

// 处理多选场景
if (multiple && Array.isArray(selectedValue)) {
selected = selectedValue.some((item) => {
if (isNumber(item) || isString(item)) {
// 如果非 object 类型
return item === value;
}
return get(item, keys?.value || 'value') === value;
return get(item, valueKey) === value;
});
if (props.checkAll) {
selected = selectedValue.length === props.optionLength;
Expand All @@ -116,7 +121,8 @@ const Option: React.FC<SelectOptionProps> = (props) => {
onSelect(value, { label: String(label), selected, event, restData });
}
if (checkAll) {
props.onCheckAllChange?.(selected, event);
props.onCheckAllChange?.(allSelectableChecked, event);
setAllSelectableChecked(!allSelectableChecked);
}
};

Expand Down
9 changes: 4 additions & 5 deletions packages/components/select/base/PopupContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,16 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
size,
});

// 全部可选选项
const selectableOptions = useMemo(() => {
const optionsExcludedCheckAll = useMemo(() => {
const uniqueOptions = {};
propsOptions?.forEach((option: SelectOption) => {
if ((option as SelectOptionGroup).group) {
(option as SelectOptionGroup).children.forEach((item) => {
if (!item.disabled && !item.checkAll) {
if (!item.checkAll) {
uniqueOptions[item.value] = item;
}
});
} else if (!(option as TdOptionProps).disabled && !(option as TdOptionProps).checkAll) {
} else if (!(option as TdOptionProps).checkAll) {
uniqueOptions[(option as TdOptionProps).value] = option;
}
});
Expand Down Expand Up @@ -186,7 +185,7 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
value={optionValue}
onSelect={onSelect}
selectedValue={value}
optionLength={selectableOptions.length}
optionLength={optionsExcludedCheckAll.length}
multiple={multiple}
size={size}
disabled={disabled}
Expand Down
84 changes: 45 additions & 39 deletions packages/components/select/base/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React, {
Children,
KeyboardEvent,
WheelEvent,
cloneElement,
isValidElement,
useCallback,
Expand All @@ -27,7 +25,7 @@ import SelectInput, { type SelectInputValue, type SelectInputValueChangeContext
import Tag from '../../tag';
import { selectDefaultProps } from '../defaultProps';
import useOptions, { isSelectOptionGroup } from '../hooks/useOptions';
import { getSelectValueArr, getSelectedOptions } from '../util/helper';
import { getKeyMapping, getSelectValueArr, getSelectedOptions } from '../util/helper';
import Option from './Option';
import OptionGroup from './OptionGroup';
import PopupContent from './PopupContent';
Expand Down Expand Up @@ -121,10 +119,11 @@ const Select = forwardRefWithStatics(
);

const selectedLabel = useMemo(() => {
const { labelKey } = getKeyMapping(keys);
if (multiple) {
return selectedOptions.map((selectedOption) => get(selectedOption || {}, keys?.label || 'label') || '');
return selectedOptions.map((selectedOption) => get(selectedOption || {}, labelKey) || '');
}
return get(selectedOptions[0] || {}, keys?.label || 'label') || undefined;
return get(selectedOptions[0] || {}, labelKey) || undefined;
}, [selectedOptions, keys, multiple]);

const handleShowPopup = (visible: boolean, ctx: PopupVisibleChangeContext) => {
Expand Down Expand Up @@ -188,37 +187,40 @@ const Select = forwardRefWithStatics(
return;
}

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

const values = [];
currentOptions.forEach((option) => {
if (isSelectOptionGroup(option)) {
option.children.forEach((item) => {
if (isSelectableOption(item)) {
values.push(getOptionValue(item));
}
});
} else if (isSelectableOption(option)) {
values.push(getOptionValue(option));
const enabledOptions = currentOptions.filter(
(opt) => !isSelectOptionGroup(opt) && !opt.checkAll && !opt.disabled,
);

const currentValues = Array.isArray(value) ? value : [];
const disabledSelectedOptions = currentOptions.filter((opt) => {
if (isSelectOptionGroup(opt) || opt.checkAll) return false;
if (!opt.disabled) return false;
if (isObjectType) {
return currentValues.some((v) => get(v, valueKey) === opt[valueKey]);
}
return currentValues.includes(opt[valueKey]);
});

const { currentSelectedOptions, allSelectedValue } = getSelectedOptions(
values,
multiple,
valueType,
keys,
valueToOption,
);
let checkAllValue: SelectValue[];

if (checkAll) {
// 全选:选中所有未禁用的选项 + 保留已选中的禁用选项
const enabledValues = enabledOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
const disabledValues = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
checkAllValue = [...disabledValues, ...enabledValues];
} else {
// 取消全选:只保留已选中的禁用选项
checkAllValue = disabledSelectedOptions.map((opt) => (isObjectType ? opt : opt[valueKey]));
}

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

onChange?.(checkAllValue, {
e,
trigger: !checkAll ? 'check' : 'uncheck',
trigger: checkAll ? 'check' : 'uncheck',
selectedOptions: currentSelectedOptions,
});
};
Expand Down Expand Up @@ -323,7 +325,7 @@ const Select = forwardRefWithStatics(
return;
}
if (isFunction(onSearch)) {
onSearch(value, { e: context.e as KeyboardEvent<HTMLDivElement> });
onSearch(value, { e: context.e as React.KeyboardEvent<HTMLDivElement> });
return;
}
};
Expand Down Expand Up @@ -403,18 +405,22 @@ const Select = forwardRefWithStatics(
return '';
}
return ({ value: val }) =>
val.slice(0, minCollapsedNum ? minCollapsedNum : val.length).map((v: string, key: number) => {
const filterOption: SelectOption & { disabled?: boolean } = options?.find((option) => option.label === v);
val.slice(0, minCollapsedNum ? minCollapsedNum : val.length).map((_, index: number) => {
const { valueKey, labelKey, disabledKey } = getKeyMapping(keys);
const targetVal = get(selectedOptions[index], valueKey);
const targetLabel = get(selectedOptions[index], labelKey);
const targetOption = valueToOption[targetVal];
if (!targetOption) return null;
return (
<Tag
key={key}
closable={!filterOption?.disabled && !disabled && !readonly}
key={index}
closable={!get(targetOption, disabledKey) && !disabled && !readonly}
size={size}
{...tagProps}
onClose={({ e }) => {
e.stopPropagation();
e?.nativeEvent?.stopImmediatePropagation?.();
const values = getSelectValueArr(value, value[key], true, valueType, keys);
const values = getSelectValueArr(value, value[index], true, valueType, keys);

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

onRemove?.({
value: value[key],
data: { label: v, value: value[key] },
value: targetVal,
data: { label: targetLabel, value: targetVal },
e: e as unknown as React.MouseEvent<HTMLDivElement, MouseEvent>,
});
}}
>
{v}
{targetLabel}
</Tag>
);
});
Expand Down Expand Up @@ -496,11 +502,11 @@ const Select = forwardRefWithStatics(

const { onMouseEnter, onMouseLeave } = props;

const handleEnter = (_, context: { inputValue: string; e: KeyboardEvent<HTMLDivElement> }) => {
const handleEnter = (_, context: { inputValue: string; e: React.KeyboardEvent<HTMLDivElement> }) => {
onEnter?.({ ...context, value });
};

const handleScroll = ({ e }: { e: WheelEvent<HTMLDivElement> }) => {
const handleScroll = ({ e }: { e: React.WheelEvent<HTMLDivElement> }) => {
toggleIsScrolling(true);

onScroll?.({ e });
Expand Down
19 changes: 10 additions & 9 deletions packages/components/select/hooks/useOptions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState, useEffect, ReactNode, ReactElement } from 'react';
import { get } from 'lodash-es';
import type { SelectKeysType, SelectOption, SelectOptionGroup, SelectValue, TdOptionProps } from '../type';
import { getValueToOption, type ValueToOption } from '../util/helper';
import React, { ReactElement, ReactNode, useEffect, useState } from 'react';
import Option from '../base/Option';
import OptionGroup from '../base/OptionGroup';
import { getKeyMapping, getValueToOption, type ValueToOption } from '../util/helper';

import type { SelectKeysType, SelectOption, SelectOptionGroup, SelectValue, TdOptionProps } from '../type';

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

// 处理 options 的逻辑
function UseOptions(
function useOptions(
keys: SelectKeysType,
options: SelectOption[],
children: ReactNode,
Expand Down Expand Up @@ -54,11 +55,12 @@ function UseOptions(
transformedOptions = arrayChildren?.map<SelectOption>((v) => handlerOptionElement(v));
}
if (keys) {
const { valueKey, labelKey } = getKeyMapping(keys);
// 如果有定制 keys 先做转换
transformedOptions = transformedOptions?.map<SelectOption>((option) => ({
...option,
value: get(option, keys?.value || 'value'),
label: get(option, keys?.label || 'label'),
value: get(option, valueKey),
label: get(option, labelKey),
}));
}
setCurrentOptions(transformedOptions);
Expand All @@ -70,8 +72,7 @@ function UseOptions(

// 同步 value 对应的 options
useEffect(() => {
const valueKey = keys?.value || 'value';
const labelKey = keys?.label || 'label';
const { valueKey, labelKey } = getKeyMapping(keys);

setSelectedOptions((oldSelectedOptions: SelectOption[]) => {
const createOptionFromValue = (item: OptionValueType) => {
Expand Down Expand Up @@ -117,4 +118,4 @@ function UseOptions(
};
}

export default UseOptions;
export default useOptions;
Loading
Loading