diff --git a/README.md b/README.md index 91814eb77..51ef9512e 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ export default () => ( | open | control select open | boolean | | | defaultOpen | control select default open | boolean | | | placeholder | select placeholder | React Node | | -| showSearch | whether show search input in single mode | boolean | true | +| showSearch | whether show search input in single mode | boolean \| Object | true | | allowClear | whether allowClear | boolean | { clearIcon?: ReactNode } | false | | 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 | | tagRender | render custom tags. | (props: CustomTagProps) => ReactNode | - | @@ -99,16 +99,12 @@ export default () => ( | combobox | enable combobox mode(can not set multiple at the same time) | boolean | false | | multiple | whether multiple select | boolean | false | | disabled | whether disabled select | boolean | false | -| filterOption | whether filter options by input value. default filter by option's optionFilterProp prop's value | boolean | true/Function(inputValue:string, option:Option) | -| optionFilterProp | which prop value of option will be used for filter if filterOption is true | String | 'value' | -| 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) | - | | optionLabelProp | render option value or option children as content of select | String: 'value'/'children' | 'value' | | defaultValue | initial selected option(s) | String \| String[] | - | | value | current selected option(s) | String \| String[] \| {key:String, label:React.Node} \| {key:String, label:React.Node}[] | - | | labelInValue | whether to embed label in value, see above value type. Not support `combobox` mode | boolean | false | | backfill | whether backfill select option to search input (Only works in single and combobox mode) | boolean | false | | onChange | called when select an option or input value change(combobox) | function(value, option:Option \| Option[]) | - | -| onSearch | called when input changed | function | - | | onBlur | called when blur | function | - | | onFocus | called when focus | function | - | | onPopupScroll | called when menu is scrolled | function | - | @@ -120,7 +116,6 @@ export default () => ( | getInputElement | customize input element | function(): Element | - | | showAction | actions trigger the dropdown to show | String[]? | - | | autoFocus | focus select after mount | boolean | - | -| autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true | | prefix | specify the select prefix icon or text | ReactNode | - | | suffixIcon | specify the select arrow icon | ReactNode | - | | clearIcon | specify the clear icon | ReactNode | - | @@ -141,6 +136,17 @@ export default () => ( | focus | focus select programmably | - | - | | blur | blur select programmably | - | - | +### showSearch + +| name | description | type | default | +| --- | --- | --- | --- | +| autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true | +| filterOption | whether filter options by input value. default filter by option's optionFilterProp prop's value | boolean\| (inputValue: string, option: Option) => boolean | true | +| 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) | - | +| optionFilterProp | which prop value of option will be used for filter if filterOption is true | String | 'value' | +| searchValue | The current input "search" text | string | - | +| onSearch | called when input changed | function | - | + ### Option props | name | description | type | default | diff --git a/src/Select.tsx b/src/Select.tsx index 8bad7c365..2a08e1101 100644 --- a/src/Select.tsx +++ b/src/Select.tsx @@ -56,6 +56,7 @@ import type { FlattenOptionData } from './interface'; import { hasValue, isComboNoValue, toArray } from './utils/commonUtil'; import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil'; import warningProps, { warningNullOptions } from './utils/warningPropsUtil'; +import useSearchConfig from './hooks/useSearchConfig'; const OMIT_DOM_PROPS = ['inputValue']; @@ -110,8 +111,16 @@ type ArrayElementType = T extends (infer E)[] ? E : T; export type SemanticName = BaseSelectSemanticName; export type PopupSemantic = 'listItem' | 'list'; +export interface SearchConfig { + searchValue?: string; + autoClearSearchValue?: boolean; + onSearch?: (value: string) => void; + filterOption?: boolean | FilterFunc; + filterSort?: (optionA: OptionType, optionB: OptionType, info: { searchValue: string }) => number; + optionFilterProp?: string; +} export interface SelectProps - extends BaseSelectPropsWithoutPrivate { + extends Omit { prefixCls?: string; id?: string; @@ -119,9 +128,12 @@ export interface SelectProps>> Field Names fieldNames?: FieldNames; - - searchValue?: string; - onSearch?: (value: string) => void; + /** @deprecated pleace use SearchConfig.onSearch */ + onSearch?: SearchConfig['onSearch']; + showSearch?: boolean | SearchConfig; + /** @deprecated pleace use SearchConfig.searchValue */ + searchValue?: SearchConfig['searchValue']; + /** @deprecated pleace use SearchConfig.autoClearSearchValue */ autoClearSearchValue?: boolean; // >>> Select @@ -135,10 +147,14 @@ export interface SelectProps; - filterSort?: (optionA: OptionType, optionB: OptionType, info: { searchValue: string }) => number; + /** @deprecated pleace use SearchConfig.filterOption */ + filterOption?: SearchConfig['filterOption']; + /** @deprecated pleace use SearchConfig.filterSort */ + filterSort?: SearchConfig['filterSort']; + /** @deprecated pleace use SearchConfig.optionFilterProp */ optionFilterProp?: string; optionLabelProp?: string; + children?: React.ReactNode; options?: OptionType[]; optionRender?: ( @@ -177,22 +193,13 @@ const Select = React.forwardRef>> Trigger direction={direction} // >>> Search + showSearch={mergedShowSearch} searchValue={mergedSearchValue} onSearch={onInternalSearch} autoClearSearchValue={autoClearSearchValue} diff --git a/src/hooks/useSearchConfig.ts b/src/hooks/useSearchConfig.ts new file mode 100644 index 000000000..1fe8579c8 --- /dev/null +++ b/src/hooks/useSearchConfig.ts @@ -0,0 +1,48 @@ +import type { SearchConfig, DefaultOptionType } from '@/Select'; +import * as React from 'react'; +const legacySearchProps = [ + 'filterOption', + 'searchValue', + 'optionFilterProp', + 'filterSort', + 'onSearch', + 'autoClearSearchValue', +]; +// Convert `showSearch` to unique config +export default function useSearchConfig( + showSearch: boolean | SearchConfig | undefined, + props: any, +) { + const { + filterOption, + searchValue, + optionFilterProp, + filterSort, + onSearch, + autoClearSearchValue, + } = props || {}; + return React.useMemo<[boolean | undefined, SearchConfig]>(() => { + const legacyShowSearch: SearchConfig = {}; + legacySearchProps.forEach((name) => { + const val = props?.[name]; + if (val !== undefined) legacyShowSearch[name] = val; + }); + const searchConfig: SearchConfig = + typeof showSearch === 'object' ? showSearch : legacyShowSearch; + if (showSearch === undefined) { + return [undefined, searchConfig]; + } + if (!showSearch) { + return [false, {}]; + } + return [true, searchConfig]; + }, [ + showSearch, + filterOption, + searchValue, + optionFilterProp, + filterSort, + onSearch, + autoClearSearchValue, + ]); +} diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 4f3bd2f2a..3c90ef594 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -2521,4 +2521,137 @@ describe('Select.Basic', () => { expect(input).toHaveClass(customClassNames.input); expect(input).toHaveStyle(customStyle.input); }); + + describe('combine showSearch', () => { + let errorSpy; + + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => null); + }); + + beforeEach(() => { + errorSpy.mockReset(); + resetWarned(); + }); + + afterAll(() => { + errorSpy.mockRestore(); + }); + const currentSearchFn = jest.fn(); + const legacySearchFn = jest.fn(); + + const LegacyDemo = (props) => { + return ( + + ); + }; + it('onSearch', () => { + const { container } = render( + <> +
+ +
+
+ +
+ , + ); + const legacyInput = container.querySelector('#test1 input'); + const currentInput = container.querySelector('#test2 input'); + fireEvent.change(legacyInput, { target: { value: '2' } }); + fireEvent.change(currentInput, { target: { value: '2' } }); + expect(currentSearchFn).toHaveBeenCalledWith('2'); + expect(legacySearchFn).toHaveBeenCalledWith('2'); + }); + it('searchValue', () => { + const { container } = render( + <> +
+ +
+
+ +
+ , + ); + const legacyInput = container.querySelector('#test1 input'); + const currentInput = container.querySelector('#test2 input'); + expect(legacyInput).toHaveValue('1'); + expect(currentInput).toHaveValue('1'); + expect(legacyInput.value).toBe(currentInput.value); + }); + it('option:sort,FilterProp ', () => { + const { container } = render( + <> +
+ { + return Number(b.label) - Number(a.label); + }} + optionFilterProp="label" + /> +
+
+ { + return Number(b.label) - Number(a.label); + }} + optionFilterProp="label" + /> +
+ , + ); + const items = container.querySelectorAll('.rc-select-item-option'); + expect(items.length).toBe(4); + expect(items[0].title).toBe('12'); + expect(items[2].title).toBe('12'); + }); + it('autoClearSearchValue', () => { + const { container } = render( + <> +
+ +
+
+ +
+ , + ); + const legacyInput = container.querySelector('#test1 input'); + const currentInput = container.querySelector('#test2 input'); + fireEvent.change(legacyInput, { target: { value: 'a' } }); + fireEvent.change(currentInput, { target: { value: 'a' } }); + expect(legacyInput).toHaveValue('a'); + expect(currentInput).toHaveValue('a'); + const items = container.querySelectorAll('.rc-select-item-option'); + fireEvent.click(items[0]); + fireEvent.click(items[1]); + expect(legacyInput).toHaveValue('a'); + expect(currentInput).toHaveValue('a'); + }); + }); });