Skip to content

Commit 2826817

Browse files
EmilyyyLiu刘欢Copilot
authored
feat: combine search props (#1152)
* feat: combine searchProps =>showSearch * feat: change ts name * feat: set autoClearSearchValue default value * feat: set autoClearSearchValue default value * feat: use hooks => useSearchConfig * feat: Delete useless cod(SearchConfig) * docs: add showSearch discreption * test: add test to “combine showSearch” * test: fix lint error * feat: change useSearchConfig * fix: Remove optionLabelProp from showSearch * fix: Remove tokenSeparators from showSearch * fix: change ts * fix: SearchConfig.***-> showSearch.*** * feat: change useSearchConfig * feat: fix ci * Update src/Select.tsx Co-authored-by: Copilot <[email protected]> * Update src/Select.tsx Co-authored-by: Copilot <[email protected]> * feat: change useSearchConfig1 * feat: change useSearchConfig2 * test: test showsearch and mode * test: test showsearch and mode --------- Co-authored-by: 刘欢 <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 7aa0c21 commit 2826817

File tree

4 files changed

+235
-22
lines changed

4 files changed

+235
-22
lines changed

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default () => (
8989
| open | control select open | boolean | |
9090
| defaultOpen | control select default open | boolean | |
9191
| placeholder | select placeholder | React Node | |
92-
| showSearch | whether show search input in single mode | boolean | true |
92+
| showSearch | whether show search input in single mode | boolean \| Object | true |
9393
| allowClear | whether allowClear | boolean | { clearIcon?: ReactNode } | false |
9494
| tags | when tagging is enabled the user can select from pre-existing options or create a new tag by picking the first choice, which is what the user has typed into the search box so far. | boolean | false |
9595
| tagRender | render custom tags. | (props: CustomTagProps) => ReactNode | - |
@@ -99,16 +99,12 @@ export default () => (
9999
| combobox | enable combobox mode(can not set multiple at the same time) | boolean | false |
100100
| multiple | whether multiple select | boolean | false |
101101
| disabled | whether disabled select | boolean | false |
102-
| filterOption | whether filter options by input value. default filter by option's optionFilterProp prop's value | boolean | true/Function(inputValue:string, option:Option) |
103-
| optionFilterProp | which prop value of option will be used for filter if filterOption is true | String | 'value' |
104-
| filterSort | Sort function for search options sorting, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction. | Function(optionA:Option, optionB: Option) | - |
105102
| optionLabelProp | render option value or option children as content of select | String: 'value'/'children' | 'value' |
106103
| defaultValue | initial selected option(s) | String \| String[] | - |
107104
| value | current selected option(s) | String \| String[] \| {key:String, label:React.Node} \| {key:String, label:React.Node}[] | - |
108105
| labelInValue | whether to embed label in value, see above value type. Not support `combobox` mode | boolean | false |
109106
| backfill | whether backfill select option to search input (Only works in single and combobox mode) | boolean | false |
110107
| onChange | called when select an option or input value change(combobox) | function(value, option:Option \| Option[]) | - |
111-
| onSearch | called when input changed | function | - |
112108
| onBlur | called when blur | function | - |
113109
| onFocus | called when focus | function | - |
114110
| onPopupScroll | called when menu is scrolled | function | - |
@@ -120,7 +116,6 @@ export default () => (
120116
| getInputElement | customize input element | function(): Element | - |
121117
| showAction | actions trigger the dropdown to show | String[]? | - |
122118
| autoFocus | focus select after mount | boolean | - |
123-
| autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true |
124119
| prefix | specify the select prefix icon or text | ReactNode | - |
125120
| suffixIcon | specify the select arrow icon | ReactNode | - |
126121
| clearIcon | specify the clear icon | ReactNode | - |
@@ -141,6 +136,17 @@ export default () => (
141136
| focus | focus select programmably | - | - |
142137
| blur | blur select programmably | - | - |
143138

139+
### showSearch
140+
141+
| name | description | type | default |
142+
| --- | --- | --- | --- |
143+
| autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true |
144+
| filterOption | whether filter options by input value. default filter by option's optionFilterProp prop's value | boolean\| (inputValue: string, option: Option) => boolean | true |
145+
| filterSort | Sort function for search options sorting, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction. | Function(optionA:Option, optionB: Option) | - |
146+
| optionFilterProp | which prop value of option will be used for filter if filterOption is true | String | 'value' |
147+
| searchValue | The current input "search" text | string | - |
148+
| onSearch | called when input changed | function | - |
149+
144150
### Option props
145151

146152
| name | description | type | default |

src/Select.tsx

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import type { FlattenOptionData } from './interface';
5656
import { hasValue, isComboNoValue, toArray } from './utils/commonUtil';
5757
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
5858
import warningProps, { warningNullOptions } from './utils/warningPropsUtil';
59+
import useSearchConfig from './hooks/useSearchConfig';
5960

6061
const OMIT_DOM_PROPS = ['inputValue'];
6162

@@ -110,18 +111,29 @@ type ArrayElementType<T> = T extends (infer E)[] ? E : T;
110111

111112
export type SemanticName = BaseSelectSemanticName;
112113
export type PopupSemantic = 'listItem' | 'list';
114+
export interface SearchConfig<OptionType> {
115+
searchValue?: string;
116+
autoClearSearchValue?: boolean;
117+
onSearch?: (value: string) => void;
118+
filterOption?: boolean | FilterFunc<OptionType>;
119+
filterSort?: (optionA: OptionType, optionB: OptionType, info: { searchValue: string }) => number;
120+
optionFilterProp?: string;
121+
}
113122
export interface SelectProps<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType>
114-
extends BaseSelectPropsWithoutPrivate {
123+
extends Omit<BaseSelectPropsWithoutPrivate, 'showSearch'> {
115124
prefixCls?: string;
116125
id?: string;
117126

118127
backfill?: boolean;
119128

120129
// >>> Field Names
121130
fieldNames?: FieldNames;
122-
123-
searchValue?: string;
124-
onSearch?: (value: string) => void;
131+
/** @deprecated please use showSearch.onSearch */
132+
onSearch?: SearchConfig<OptionType>['onSearch'];
133+
showSearch?: boolean | SearchConfig<OptionType>;
134+
/** @deprecated please use showSearch.searchValue */
135+
searchValue?: SearchConfig<OptionType>['searchValue'];
136+
/** @deprecated please use showSearch.autoClearSearchValue */
125137
autoClearSearchValue?: boolean;
126138

127139
// >>> Select
@@ -135,10 +147,14 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
135147
* In TreeSelect, `false` will highlight match item.
136148
* It's by design.
137149
*/
138-
filterOption?: boolean | FilterFunc<OptionType>;
139-
filterSort?: (optionA: OptionType, optionB: OptionType, info: { searchValue: string }) => number;
150+
/** @deprecated please use showSearch.filterOption */
151+
filterOption?: SearchConfig<OptionType>['filterOption'];
152+
/** @deprecated please use showSearch.filterSort */
153+
filterSort?: SearchConfig<OptionType>['filterSort'];
154+
/** @deprecated please use showSearch.optionFilterProp */
140155
optionFilterProp?: string;
141156
optionLabelProp?: string;
157+
142158
children?: React.ReactNode;
143159
options?: OptionType[];
144160
optionRender?: (
@@ -177,22 +193,13 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
177193
prefixCls = 'rc-select',
178194
backfill,
179195
fieldNames,
180-
181196
// Search
182-
searchValue,
183-
onSearch,
184-
autoClearSearchValue = true,
185-
197+
showSearch,
186198
// Select
187199
onSelect,
188200
onDeselect,
189201
onActive,
190202
popupMatchSelectWidth = true,
191-
192-
// Options
193-
filterOption,
194-
filterSort,
195-
optionFilterProp,
196203
optionLabelProp,
197204
options,
198205
optionRender,
@@ -216,6 +223,16 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
216223
...restProps
217224
} = props;
218225

226+
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch, props);
227+
const {
228+
filterOption,
229+
searchValue,
230+
optionFilterProp,
231+
filterSort,
232+
onSearch,
233+
autoClearSearchValue = true,
234+
} = searchConfig;
235+
219236
const mergedId = useId(id);
220237
const multiple = isMultiple(mode);
221238
const childrenAsData = !!(!options && children);
@@ -698,6 +715,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
698715
// >>> Trigger
699716
direction={direction}
700717
// >>> Search
718+
showSearch={mergedShowSearch}
701719
searchValue={mergedSearchValue}
702720
onSearch={onInternalSearch}
703721
autoClearSearchValue={autoClearSearchValue}

src/hooks/useSearchConfig.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { SearchConfig, DefaultOptionType, SelectProps } from '@/Select';
2+
import * as React from 'react';
3+
4+
// Convert `showSearch` to unique config
5+
export default function useSearchConfig(
6+
showSearch: boolean | SearchConfig<DefaultOptionType> | undefined,
7+
props: SelectProps,
8+
) {
9+
const {
10+
filterOption,
11+
searchValue,
12+
optionFilterProp,
13+
filterSort,
14+
onSearch,
15+
autoClearSearchValue,
16+
} = props;
17+
return React.useMemo<[boolean | undefined, SearchConfig<DefaultOptionType>]>(() => {
18+
const isObject = typeof showSearch === 'object';
19+
const searchConfig = {
20+
filterOption,
21+
searchValue,
22+
optionFilterProp,
23+
filterSort,
24+
onSearch,
25+
autoClearSearchValue,
26+
...(isObject ? showSearch : {}),
27+
};
28+
29+
return [isObject ? true : showSearch, searchConfig];
30+
}, [
31+
showSearch,
32+
filterOption,
33+
searchValue,
34+
optionFilterProp,
35+
filterSort,
36+
onSearch,
37+
autoClearSearchValue,
38+
]);
39+
}

tests/Select.test.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2521,4 +2521,154 @@ describe('Select.Basic', () => {
25212521
expect(input).toHaveClass(customClassNames.input);
25222522
expect(input).toHaveStyle(customStyle.input);
25232523
});
2524+
2525+
describe('combine showSearch', () => {
2526+
let errorSpy;
2527+
2528+
beforeAll(() => {
2529+
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => null);
2530+
});
2531+
2532+
beforeEach(() => {
2533+
errorSpy.mockReset();
2534+
resetWarned();
2535+
});
2536+
2537+
afterAll(() => {
2538+
errorSpy.mockRestore();
2539+
});
2540+
const currentSearchFn = jest.fn();
2541+
const legacySearchFn = jest.fn();
2542+
2543+
const LegacyDemo = (props) => {
2544+
return (
2545+
<Select
2546+
open
2547+
{...props}
2548+
options={[
2549+
{ value: 'a', label: '1' },
2550+
{ value: 'b', label: '2' },
2551+
{ value: 'c', label: '12' },
2552+
]}
2553+
/>
2554+
);
2555+
};
2556+
const CurrentDemo = (props) => {
2557+
return (
2558+
<Select
2559+
open
2560+
showSearch={props}
2561+
options={[
2562+
{ value: 'a', label: '1' },
2563+
{ value: 'b', label: '2' },
2564+
{ value: 'c', label: '12' },
2565+
]}
2566+
/>
2567+
);
2568+
};
2569+
it('onSearch', () => {
2570+
const { container } = render(
2571+
<>
2572+
<div id="test1">
2573+
<LegacyDemo onSearch={legacySearchFn} />
2574+
</div>
2575+
<div id="test2">
2576+
<CurrentDemo onSearch={currentSearchFn} />
2577+
</div>
2578+
</>,
2579+
);
2580+
const legacyInput = container.querySelector<HTMLInputElement>('#test1 input');
2581+
const currentInput = container.querySelector<HTMLInputElement>('#test2 input');
2582+
fireEvent.change(legacyInput, { target: { value: '2' } });
2583+
fireEvent.change(currentInput, { target: { value: '2' } });
2584+
expect(currentSearchFn).toHaveBeenCalledWith('2');
2585+
expect(legacySearchFn).toHaveBeenCalledWith('2');
2586+
});
2587+
it('searchValue', () => {
2588+
const { container } = render(
2589+
<>
2590+
<div id="test1">
2591+
<LegacyDemo searchValue="1" showSearch />
2592+
</div>
2593+
<div id="test2">
2594+
<CurrentDemo searchValue="1" />
2595+
</div>
2596+
</>,
2597+
);
2598+
const legacyInput = container.querySelector<HTMLInputElement>('#test1 input');
2599+
const currentInput = container.querySelector<HTMLInputElement>('#test2 input');
2600+
expect(legacyInput).toHaveValue('1');
2601+
expect(currentInput).toHaveValue('1');
2602+
expect(legacyInput.value).toBe(currentInput.value);
2603+
});
2604+
it('option:sort,FilterProp ', () => {
2605+
const { container } = render(
2606+
<>
2607+
<div id="test1">
2608+
<LegacyDemo
2609+
searchValue="2"
2610+
showSearch
2611+
filterSort={(a, b) => {
2612+
return Number(b.label) - Number(a.label);
2613+
}}
2614+
optionFilterProp="label"
2615+
/>
2616+
</div>
2617+
<div id="test2">
2618+
<CurrentDemo
2619+
searchValue="2"
2620+
filterSort={(a, b) => {
2621+
return Number(b.label) - Number(a.label);
2622+
}}
2623+
optionFilterProp="label"
2624+
/>
2625+
</div>
2626+
</>,
2627+
);
2628+
const items = container.querySelectorAll<HTMLDivElement>('.rc-select-item-option');
2629+
expect(items.length).toBe(4);
2630+
expect(items[0].title).toBe('12');
2631+
expect(items[2].title).toBe('12');
2632+
});
2633+
it('autoClearSearchValue', () => {
2634+
const { container } = render(
2635+
<>
2636+
<div id="test1">
2637+
<LegacyDemo showSearch autoClearSearchValue={false} />
2638+
</div>
2639+
<div id="test2">
2640+
<CurrentDemo autoClearSearchValue={false} />
2641+
</div>
2642+
</>,
2643+
);
2644+
const legacyInput = container.querySelector<HTMLInputElement>('#test1 input');
2645+
const currentInput = container.querySelector<HTMLInputElement>('#test2 input');
2646+
fireEvent.change(legacyInput, { target: { value: 'a' } });
2647+
fireEvent.change(currentInput, { target: { value: 'a' } });
2648+
expect(legacyInput).toHaveValue('a');
2649+
expect(currentInput).toHaveValue('a');
2650+
const items = container.querySelectorAll<HTMLDivElement>('.rc-select-item-option');
2651+
fireEvent.click(items[0]);
2652+
fireEvent.click(items[1]);
2653+
expect(legacyInput).toHaveValue('a');
2654+
expect(currentInput).toHaveValue('a');
2655+
});
2656+
2657+
it.each([
2658+
// [description, props, shouldExist]
2659+
['showSearch=false and mode=undefined', { showSearch: false }, false],
2660+
['showSearch=undefined and mode=undefined', {}, false],
2661+
['showSearch=undefined and mode=tags', { mode: 'tags' }, true],
2662+
['showSearch=false and mode=tags', { showSearch: false, mode: 'tags' }, true],
2663+
['showSearch=true and mode=undefined', { showSearch: true }, true],
2664+
])('%s', (_, props: { showSearch?: boolean; mode?: 'tags' }, shouldExist) => {
2665+
const { container } = render(<Select options={[{ value: 'a', label: '1' }]} {...props} />);
2666+
const inputNode = container.querySelector('input');
2667+
if (shouldExist) {
2668+
expect(inputNode).not.toHaveAttribute('readonly');
2669+
} else {
2670+
expect(inputNode).toHaveAttribute('readonly');
2671+
}
2672+
});
2673+
});
25242674
});

0 commit comments

Comments
 (0)