diff --git a/README.md b/README.md index 151b91a0..2f6e0b03 100644 --- a/README.md +++ b/README.md @@ -114,12 +114,6 @@ React.render( - - autoClearSearchValue - boolean - true - Whether the current search will be cleared on selecting an item. Only applies when checkable - options Object @@ -234,9 +228,28 @@ React.render( >true hide popup on select + + showSearch + boolean | object + false + Whether show search input in single mode + +### showSearch + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| autoClearSearchValue | Whether the current search will be cleared on selecting an item. Only applies when checkable| boolean | true | +| filter | The function will receive two arguments, inputValue and option, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded | function(inputValue, path): boolean | - | | +| limit | Set the count of filtered items | number \| false | 50 | | +| matchInputWidth | Whether the width of list matches input, ([how it looks](https://github.com/ant-design/ant-design/issues/25779)) | boolean | true | | +| render | Used to render filtered options | function(inputValue, path): ReactNode | - | | +| sort | Used to sort filtered options | function(a, b, inputValue) | - | | +| searchValue | The current input "search" text | string | - | - | +| onSearch | called when input changed | function | - | - | + ### option diff --git a/src/Cascader.tsx b/src/Cascader.tsx index 268c370c..689f3e4f 100644 --- a/src/Cascader.tsx +++ b/src/Cascader.tsx @@ -40,7 +40,7 @@ export interface BaseOptionType { export type DefaultOptionType = BaseOptionType & Record; -export interface ShowSearchType< +export interface SearchConfig< OptionType extends DefaultOptionType = DefaultOptionType, ValueField extends keyof OptionType = keyof OptionType, > { @@ -63,6 +63,9 @@ export interface ShowSearchType< ) => number; matchInputWidth?: boolean; limit?: number | false; + searchValue?: string; + onSearch?: (value: string) => void; + autoClearSearchValue?: boolean; } export type ShowCheckedStrategy = typeof SHOW_PARENT | typeof SHOW_CHILD; @@ -88,9 +91,12 @@ interface BaseCascaderProps< showCheckedStrategy?: ShowCheckedStrategy; // Search + /** @deprecated please use showSearch.autoClearSearchValue */ autoClearSearchValue?: boolean; - showSearch?: boolean | ShowSearchType; + showSearch?: boolean | SearchConfig; + /** @deprecated please use showSearch.searchValue */ searchValue?: string; + /** @deprecated please use showSearch.onSearch */ onSearch?: (value: string) => void; // Trigger @@ -205,9 +211,6 @@ const Cascader = React.forwardRef((props, re checkable, // Search - autoClearSearchValue = true, - searchValue, - onSearch, showSearch, // Trigger @@ -267,6 +270,8 @@ const Cascader = React.forwardRef((props, re ); // =========================== Search =========================== + const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch, props); + const { autoClearSearchValue = true, searchValue, onSearch } = searchConfig; const [mergedSearchValue, setSearchValue] = useMergedState('', { value: searchValue, postState: search => search || '', @@ -274,14 +279,11 @@ const Cascader = React.forwardRef((props, re const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => { setSearchValue(searchText); - if (info.source !== 'blur' && onSearch) { onSearch(searchText); } }; - const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch); - const searchOptions = useSearchOptions( mergedSearchValue, mergedOptions, diff --git a/src/hooks/useSearchConfig.ts b/src/hooks/useSearchConfig.ts index a8a96eb9..f6be7bab 100644 --- a/src/hooks/useSearchConfig.ts +++ b/src/hooks/useSearchConfig.ts @@ -1,17 +1,21 @@ import warning from '@rc-component/util/lib/warning'; import * as React from 'react'; -import type { CascaderProps, ShowSearchType } from '../Cascader'; +import type { CascaderProps, SearchConfig } from '../Cascader'; // Convert `showSearch` to unique config -export default function useSearchConfig(showSearch?: CascaderProps['showSearch']) { - return React.useMemo<[boolean, ShowSearchType]>(() => { +export default function useSearchConfig(showSearch?: CascaderProps['showSearch'], props?: any) { + const { autoClearSearchValue, searchValue, onSearch } = props; + return React.useMemo<[boolean, SearchConfig]>(() => { if (!showSearch) { return [false, {}]; } - let searchConfig: ShowSearchType = { + let searchConfig: SearchConfig = { matchInputWidth: true, limit: 50, + autoClearSearchValue, + searchValue, + onSearch, }; if (showSearch && typeof showSearch === 'object') { @@ -30,5 +34,5 @@ export default function useSearchConfig(showSearch?: CascaderProps['showSearch'] } return [true, searchConfig]; - }, [showSearch]); + }, [showSearch, autoClearSearchValue, searchValue, onSearch]); } diff --git a/src/hooks/useSearchOptions.ts b/src/hooks/useSearchOptions.ts index 3d99f603..856bd7ac 100644 --- a/src/hooks/useSearchOptions.ts +++ b/src/hooks/useSearchOptions.ts @@ -1,12 +1,12 @@ import * as React from 'react'; -import type { DefaultOptionType, InternalFieldNames, ShowSearchType } from '../Cascader'; +import type { DefaultOptionType, InternalFieldNames, SearchConfig } from '../Cascader'; export const SEARCH_MARK = '__rc_cascader_search_mark__'; -const defaultFilter: ShowSearchType['filter'] = (search, options, { label = '' }) => +const defaultFilter: SearchConfig['filter'] = (search, options, { label = '' }) => options.some(opt => String(opt[label]).toLowerCase().includes(search.toLowerCase())); -const defaultRender: ShowSearchType['render'] = (inputValue, path, prefixCls, fieldNames) => +const defaultRender: SearchConfig['render'] = (inputValue, path, prefixCls, fieldNames) => path.map(opt => opt[fieldNames.label as string]).join(' / '); const useSearchOptions = ( @@ -14,7 +14,7 @@ const useSearchOptions = ( options: DefaultOptionType[], fieldNames: InternalFieldNames, prefixCls: string, - config: ShowSearchType, + config: SearchConfig, enableHalfPath?: boolean, ) => { const { filter = defaultFilter, render = defaultRender, limit = 50, sort } = config; diff --git a/src/index.tsx b/src/index.tsx index b73a5f68..77527ff9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,7 @@ export type { DefaultOptionType, CascaderProps, FieldNames, - ShowSearchType, + SearchConfig, CascaderRef, } from './Cascader'; export { Panel }; diff --git a/tests/search.spec.tsx b/tests/search.spec.tsx index ba6c6713..3c9fed0e 100644 --- a/tests/search.spec.tsx +++ b/tests/search.spec.tsx @@ -308,6 +308,7 @@ describe('Cascader.Search', () => { { '{"label":"bamboo","disabled":true,"value":"bamboo"}', ); }); + + it('onSearch and searchValue in showSearch', () => { + const onSearch = jest.fn(); + const wrapper = mount(); + + // Leaf + doSearch(wrapper, 'toy'); + let itemList = wrapper.find('div.rc-cascader-menu-item-content'); + expect(itemList).toHaveLength(2); + expect(itemList.at(0).text()).toEqual('Label Bamboo / Label Little / Toy Fish'); + expect(itemList.at(1).text()).toEqual('Label Bamboo / Label Little / Toy Cards'); + expect(onSearch).toHaveBeenCalledWith('toy'); + + // Parent + doSearch(wrapper, 'Label Little'); + itemList = wrapper.find('div.rc-cascader-menu-item-content'); + expect(itemList).toHaveLength(2); + expect(itemList.at(0).text()).toEqual('Label Bamboo / Label Little / Toy Fish'); + expect(itemList.at(1).text()).toEqual('Label Bamboo / Label Little / Toy Cards'); + expect(onSearch).toHaveBeenCalledWith('Label Little'); + }); + + it('searchValue in showSearch', () => { + const { container } = render( + , + ); + expect(container.querySelectorAll('.rc-cascader-menu-item')).toHaveLength(1); + expect( + (container.querySelector('.rc-cascader-selection-search-input') as HTMLInputElement)?.value, + ).toBe('little'); + }); + it('autoClearSearchValue in showSearch', () => { + const { container } = render( + , + ); + + const inputNode = container.querySelector( + '.rc-cascader-selection-search-input', + ); + fireEvent.change(inputNode as HTMLInputElement, { target: { value: 'little' } }); + expect(inputNode).toHaveValue('little'); + fireEvent.click(document.querySelector('.rc-cascader-checkbox') as HTMLElement); + expect(inputNode).toHaveValue('little'); + }); });