Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(select): 分组选择器支持过滤 #3322

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
37 changes: 19 additions & 18 deletions src/select/_example/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,27 @@ const OptionGroupSelect = () => {
{ label: '选项八', value: 9 },
];

const groupOptions = [
{
group: '分组一',
children: options1,
},
{
group: '分组二',
children: options2,
},
{
group: '分组三',
divider: true,
children: options3,
},
];

return (
<Space breakLine style={{ width: '100%' }}>
<Select value={value} onChange={onChange} style={{ width: '40%' }}>
<OptionGroup label="分组一" divider={true}>
{options1.map((item, index) => (
<Option label={item.label} value={item.value} key={index} />
))}
</OptionGroup>
<OptionGroup label="分组二" divider={true}>
{options2.map((item, index) => (
<Option label={item.label} value={item.value} key={index} />
))}
</OptionGroup>
<OptionGroup label="分组三" divider={true}>
{options3.map((item, index) => (
<Option label={item.label} value={item.value} key={index} />
))}
</OptionGroup>
</Select>
<Select value={value2} onChange={onChange2} style={{ width: '40%' }} multiple>
<Select value={value} onChange={onChange} style={{ width: '40%' }} options={groupOptions} filterable />
<Select value={value2} onChange={onChange2} style={{ width: '40%' }} multiple filterable>
<Option value="all" label="全选" checkAll></Option>
<OptionGroup label="分组一" divider={true}>
{options1.map((item, index) => (
<Option label={item.label} value={item.value} key={index} />
Expand Down
1 change: 1 addition & 0 deletions src/select/_example/multiple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const MultipleSelect = () => {
console.log('onRemove', options);
}}
>
<Option value="all" label="全选" checkAll></Option>
{options2.map((item) => (
<Option value={item.value} label={item.label} key={item.value} content={item.content}></Option>
))}
Expand Down
28 changes: 5 additions & 23 deletions src/select/base/OptionGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,28 @@
import React, { Children, isValidElement, cloneElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import useConfig from '../../hooks/useConfig';

import { TdOptionGroupProps, SelectValue } from '../type';
import { TdOptionGroupProps } from '../type';
import { optionGroupDefaultProps } from '../defaultProps';
import useDefaultProps from '../../hooks/useDefaultProps';

export interface SelectGOptionGroupProps extends TdOptionGroupProps {
selectedValue?: SelectValue;
onSelect?: (
value: string | number,
context: { label?: React.ReactNode; selected?: boolean; event: React.MouseEvent },
) => void;
divider?: boolean;
children?: React.ReactNode;
multiple?: boolean;
}

const OptionGroup: React.FC<SelectGOptionGroupProps> = (props) => {
const { children, label, selectedValue, onSelect, divider, multiple } = useDefaultProps<SelectGOptionGroupProps>(
props,
optionGroupDefaultProps,
);
const { children, label, divider } = useDefaultProps<SelectGOptionGroupProps>(props, optionGroupDefaultProps);

const { classPrefix } = useConfig();

const childrenWithProps = Children.map(children, (child) => {
if (isValidElement<SelectGOptionGroupProps>(child)) {
const addedProps: SelectGOptionGroupProps = { selectedValue, onSelect, multiple };
return cloneElement<SelectGOptionGroupProps>(child, { ...addedProps });
}
return child;
});

return (
<li
className={classNames(`${classPrefix}-select-option-group`, {
[`${classPrefix}-select-option-group__divider`]: divider,
})}
>
<ul className={`${classPrefix}-select-option-group__header`}>{label}</ul>
<ul className={`${classPrefix}-select__list`}>{childrenWithProps}</ul>
{(label ?? false) && <div className={`${classPrefix}-select-option-group__header`}>{label}</div>}
{children}
</li>
);
return;
Expand Down
59 changes: 43 additions & 16 deletions src/select/base/PopupContent.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import React, { Children, isValidElement, cloneElement, useRef, CSSProperties } from 'react';
import React, { Children, isValidElement, cloneElement, useRef, CSSProperties, useMemo } from 'react';
import classNames from 'classnames';
import { useLocaleReceiver } from '../../locale/LocalReceiver';
import { getSelectValueArr } from '../util/helper';
import { TdSelectProps, SelectValue, TdOptionProps, SelectValueChangeTrigger, SelectOption } from '../type';
import {
TdSelectProps,
SelectValue,
TdOptionProps,
SelectValueChangeTrigger,
SelectOption,
SelectOptionGroup,
} from '../type';
import useConfig from '../../hooks/useConfig';
import usePanelVirtualScroll from '../hooks/usePanelVirtualScroll';
import Option, { SelectOptionProps } from './Option';
import OptionGroup from './OptionGroup';

type OptionsType = TdOptionProps[];
// type OptionsType = TdOptionProps[];

interface SelectPopupProps
extends Pick<
Expand Down Expand Up @@ -86,6 +94,23 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
size,
});

// 全部可选选项
const selectableOptions = useMemo(() => {
const uniqueOptions = {};
propsOptions.forEach((option: SelectOption) => {
if ((option as SelectOptionGroup).group) {
(option as SelectOptionGroup).children.forEach((item) => {
if (!item.disabled && !item.checkAll) {
uniqueOptions[item.value] = item;
}
});
} else if (!(option as TdOptionProps).disabled && !(option as TdOptionProps).checkAll) {
uniqueOptions[(option as TdOptionProps).value] = option;
}
});
return Object.values(uniqueOptions);
}, [propsOptions]);

const { classPrefix } = useConfig();
if (!children && !propsOptions) {
return null;
Expand Down Expand Up @@ -131,27 +156,29 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
// 渲染 options
const renderOptions = (options: SelectOption[]) => {
if (options) {
const uniqueOptions = [];
options.forEach((option: SelectOptionProps) => {
const index = uniqueOptions.findIndex((item) => item.label === option.label && item.value === option.value);
if (index === -1) {
uniqueOptions.push(option);
}
});
const selectableOption = uniqueOptions.filter((v) => !v.disabled && !v.checkAll);
// 通过 options API配置的
return (
<ul className={`${classPrefix}-select__list`}>
{(uniqueOptions as OptionsType).map(
({ value: optionValue, label, disabled, content, children, ...restData }, index) => (
{options.map((item, index) => {
const { group, divider, ...rest } = item as SelectOptionGroup;
if (group) {
return (
<OptionGroup label={group} divider={divider} key={index}>
{renderOptions(rest.children)}
</OptionGroup>
);
}

const { value: optionValue, label, disabled, content, children, ...restData } = item as TdOptionProps;
return (
<Option
key={index}
max={max}
label={label}
value={optionValue}
onSelect={onSelect}
selectedValue={value}
optionLength={selectableOption.length}
optionLength={selectableOptions.length}
multiple={multiple}
size={size}
disabled={disabled}
Expand All @@ -171,8 +198,8 @@ const PopupContent = React.forwardRef<HTMLDivElement, SelectPopupProps>((props,
>
{children}
</Option>
),
)}
);
})}
</ul>
);
}
Expand Down
76 changes: 58 additions & 18 deletions src/select/base/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const Select = forwardRefWithStatics(
const values = getSelectValueArr(value, value[closest], true, valueType, keys);

// 处理onChange回调中的selectedOptions参数
const { currentSelectedOptions } = getSelectedOptions(values, multiple, valueType, keys, tmpPropOptions);
const { currentSelectedOptions } = getSelectedOptions(values, multiple, valueType, keys, valueToOption);
onChange(values, { e, trigger, selectedOptions: currentSelectedOptions });
return;
}
Expand All @@ -172,7 +172,7 @@ const Select = forwardRefWithStatics(
e?.stopPropagation?.();
const values = getSelectValueArr(value, value[index], true, valueType, keys);
// 处理onChange回调中的selectedOptions参数
const { currentSelectedOptions } = getSelectedOptions(values, multiple, valueType, keys, tmpPropOptions);
const { currentSelectedOptions } = getSelectedOptions(values, multiple, valueType, keys, valueToOption);

onChange(values, { e, trigger, selectedOptions: currentSelectedOptions });
if (isFunction(onRemove)) {
Expand All @@ -192,16 +192,29 @@ const Select = forwardRefWithStatics(
return;
}

const values = currentOptions
.filter((option) => !option.checkAll && !option.disabled)
.map((option) => (valueType === 'object' ? option : option[keys?.value || 'value']));
// const values = currentOptions
// .filter((option) => !option.checkAll && !option.disabled)
// .map((option) => (valueType === 'object' ? option : option[keys?.value || 'value']));

const values = [];
currentOptions.forEach((option) => {
if (option.group) {
option.children.forEach((item) => {
if (!item.disabled && !item.checkAll) {
values.push(valueType === 'object' ? item : item[keys?.value || 'value']);
}
});
} else if (!option.disabled && !option.checkAll) {
values.push(valueType === 'object' ? option : option[keys?.value || 'value']);
}
});

const { currentSelectedOptions, allSelectedValue } = getSelectedOptions(
values,
multiple,
valueType,
keys,
tmpPropOptions,
valueToOption,
);

const checkAllValue =
Expand Down Expand Up @@ -234,7 +247,7 @@ const Select = forwardRefWithStatics(
multiple,
valueType,
keys,
tmpPropOptions,
valueToOption,
selectedValue,
);

Expand All @@ -257,18 +270,45 @@ const Select = forwardRefWithStatics(
return;
}

if (filter && isFunction(filter)) {
// 如果有自定义的filter方法 使用自定义的filter方法
if (Array.isArray(tmpPropOptions)) {
filteredOptions = tmpPropOptions.filter((option) => filter(value, option));
} else if (Array.isArray(Object.values(valueToOption))) {
filteredOptions = Object.values(valueToOption).filter((option) => filter(value, option));
// if (filter && isFunction(filter)) {
// // 如果有自定义的filter方法 使用自定义的filter方法
// if (Array.isArray(tmpPropOptions)) {
// filteredOptions = tmpPropOptions.filter((option) => filter(value, option));
// } else if (Array.isArray(Object.values(valueToOption))) {
// filteredOptions = Object.values(valueToOption).filter((option) => filter(value, option));
// }
// } else if (Array.isArray(tmpPropOptions)) {
// const upperValue = value.toUpperCase();
// filteredOptions = tmpPropOptions.filter((option) => (option?.label || '').toUpperCase().includes(upperValue)); // 不区分大小写
// }

const filterLabels = [];
const filterMethods = (option: SelectOption) => {
if (filter && isFunction(filter)) {
return filter(value, option);
}
} else if (Array.isArray(tmpPropOptions)) {
const upperValue = value.toUpperCase();
filteredOptions = tmpPropOptions.filter((option) => (option?.label || '').toUpperCase().includes(upperValue)); // 不区分大小写
}
const isSameLabelOptionExist = filteredOptions.find((option) => option.label === value);
return (option?.label || '').toUpperCase().includes(upperValue);
};

tmpPropOptions.forEach((option) => {
if (option.group) {
filteredOptions.push({
...option,
children: option.children?.filter((child) => {
if (filterMethods(child)) {
filterLabels.push(child.label);
return true;
}
return false;
}),
});
} else if (filterMethods(option)) {
filterLabels.push(option.label);
filteredOptions.push(option);
}
});
const isSameLabelOptionExist = filterLabels.includes(value);
if (creatable && !isSameLabelOptionExist) {
filteredOptions = filteredOptions.concat([{ label: value, value }]);
}
Expand Down Expand Up @@ -381,7 +421,7 @@ const Select = forwardRefWithStatics(
multiple,
valueType,
keys,
tmpPropOptions,
valueToOption,
value,
);
onChange(values, {
Expand Down
15 changes: 12 additions & 3 deletions src/select/hooks/useOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import get from 'lodash/get';
import { SelectKeysType, SelectOption, SelectValue } from '../type';
import { getValueToOption } from '../util/helper';
import Option from '../base/Option';
import OptionGroup from '../base/OptionGroup';

// 处理 options 的逻辑
function UseOptions(
Expand All @@ -23,20 +24,28 @@ function UseOptions(
let transformedOptions = options;

const arrayChildren = React.Children.toArray(children);
const optionChildren = arrayChildren.filter((v: ReactElement) => v.type === Option);
const optionChildren = arrayChildren.filter((v: ReactElement) => v.type === Option || v.type === OptionGroup);
const isChildrenFilterable = arrayChildren.length > 0 && optionChildren.length === arrayChildren.length;
if (reserveKeyword && currentOptions.length && isChildrenFilterable) return;

if (isChildrenFilterable) {
transformedOptions = arrayChildren?.map<SelectOption>((v) => {
const handlerOptionElement = (v) => {
if (React.isValidElement<SelectOption>(v)) {
if (v.type === OptionGroup) {
return {
...v.props,
group: v.props.label,
children: v.props.children?.map((v) => handlerOptionElement(v)),
};
}
return {
...v.props,
label: v.props.label || v.props.children,
};
}
return { label: v };
});
};
transformedOptions = arrayChildren?.map<SelectOption>((v) => handlerOptionElement(v));
}
if (keys) {
// 如果有定制 keys 先做转换
Expand Down
Loading
Loading