Skip to content

feat: Merge searchValue,autoClearSearchValue and onSearch into the showSearch field #593

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

Merged
merged 5 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,6 @@ React.render(
</tr>
</thead>
<tbody>
<tr>
<td>autoClearSearchValue</td>
<td>boolean</td>
<td>true</td>
<td>Whether the current search will be cleared on selecting an item. Only applies when checkable</td>
</tr>
<tr>
<td>options</td>
<td>Object</td>
Expand Down Expand Up @@ -234,9 +228,28 @@ React.render(
<td>>true</td>
<td>hide popup on select</td>
</tr>
<tr>
<td>showSearch</td>
<td>boolean | object</td>
<td>false</td>
<td>Whether show search input in single mode</td>
</tr>
</tbody>
</table>

### 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

<table class="table table-bordered table-striped">
Expand Down
18 changes: 10 additions & 8 deletions src/Cascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface BaseOptionType {

export type DefaultOptionType = BaseOptionType & Record<string, any>;

export interface ShowSearchType<
export interface SearchConfig<
OptionType extends DefaultOptionType = DefaultOptionType,
ValueField extends keyof OptionType = keyof OptionType,
> {
Expand All @@ -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;
Expand All @@ -88,9 +91,12 @@ interface BaseCascaderProps<
showCheckedStrategy?: ShowCheckedStrategy;

// Search
/** @deprecated please use showSearch.autoClearSearchValue */
autoClearSearchValue?: boolean;
showSearch?: boolean | ShowSearchType<OptionType>;
showSearch?: boolean | SearchConfig<OptionType>;
/** @deprecated please use showSearch.searchValue */
searchValue?: string;
/** @deprecated please use showSearch.onSearch */
onSearch?: (value: string) => void;

// Trigger
Expand Down Expand Up @@ -205,9 +211,6 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
checkable,

// Search
autoClearSearchValue = true,
searchValue,
onSearch,
showSearch,

// Trigger
Expand Down Expand Up @@ -267,21 +270,20 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
);

// =========================== Search ===========================
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch, props);
const { autoClearSearchValue = true, searchValue, onSearch } = searchConfig;
const [mergedSearchValue, setSearchValue] = useMergedState('', {
value: searchValue,
postState: search => search || '',
});

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,
Expand Down
14 changes: 9 additions & 5 deletions src/hooks/useSearchConfig.ts
Original file line number Diff line number Diff line change
@@ -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]>(() => {
Comment on lines +6 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

props 解构存在潜在运行时异常

props 为空或未传入时,const { autoClearSearchValue, searchValue, onSearch } = props; 会直接对 undefined 进行解构,运行时将抛出 TypeError: Cannot destructure property ... of 'undefined' or 'null'.
建议在形参或解构处增加默认值,避免意外崩溃。

-export default function useSearchConfig(showSearch?: CascaderProps['showSearch'], props?: any) {
-  const { autoClearSearchValue, searchValue, onSearch } = props;
+export default function useSearchConfig(
+  showSearch?: CascaderProps['showSearch'],
+  props: Partial<Pick<CascaderProps, 'autoClearSearchValue' | 'searchValue' | 'onSearch'>> = {},
+) {
+  const { autoClearSearchValue, searchValue, onSearch } = props;

这样既提供了类型约束,也为缺省情况兜底。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function useSearchConfig(showSearch?: CascaderProps['showSearch'], props?: any) {
const { autoClearSearchValue, searchValue, onSearch } = props;
return React.useMemo<[boolean, SearchConfig]>(() => {
export default function useSearchConfig(
showSearch?: CascaderProps['showSearch'],
props: Partial<Pick<CascaderProps, 'autoClearSearchValue' | 'searchValue' | 'onSearch'>> = {},
) {
const { autoClearSearchValue, searchValue, onSearch } = props;
return React.useMemo<[boolean, SearchConfig]>(() => {
// …rest of implementation
});
}
🤖 Prompt for AI Agents
In src/hooks/useSearchConfig.ts around lines 6 to 8, the destructuring of props
without a default value can cause a runtime TypeError if props is undefined or
null. To fix this, provide a default empty object for props either in the
function parameter (e.g., props = {}) or during destructuring to ensure safe
access to its properties and prevent crashes.

if (!showSearch) {
return [false, {}];
}

let searchConfig: ShowSearchType = {
let searchConfig: SearchConfig = {
matchInputWidth: true,
limit: 50,
autoClearSearchValue,
searchValue,
onSearch,
};

if (showSearch && typeof showSearch === 'object') {
Expand All @@ -30,5 +34,5 @@ export default function useSearchConfig(showSearch?: CascaderProps['showSearch']
}

return [true, searchConfig];
}, [showSearch]);
}, [showSearch, autoClearSearchValue, searchValue, onSearch]);
}
8 changes: 4 additions & 4 deletions src/hooks/useSearchOptions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
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 = (
search: string,
options: DefaultOptionType[],
fieldNames: InternalFieldNames,
prefixCls: string,
config: ShowSearchType,
config: SearchConfig,
enableHalfPath?: boolean,
) => {
const { filter = defaultFilter, render = defaultRender, limit = 50, sort } = config;
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type {
DefaultOptionType,
CascaderProps,
FieldNames,
ShowSearchType,
SearchConfig,
CascaderRef,
} from './Cascader';
export { Panel };
Expand Down
76 changes: 76 additions & 0 deletions tests/search.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ describe('Cascader.Search', () => {
<Cascader
open
searchValue="little"
showSearch
options={[
{
label: 'bamboo',
Expand Down Expand Up @@ -352,4 +353,79 @@ describe('Cascader.Search', () => {
'{"label":"bamboo","disabled":true,"value":"bamboo"}',
);
});

it('onSearch and searchValue in showSearch', () => {
const onSearch = jest.fn();
const wrapper = mount(<Cascader options={options} open showSearch={{ onSearch }} />);

// 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(
<Cascader
open
showSearch={{ searchValue: 'little' }}
options={[
{
label: 'bamboo',
value: 'bamboo',
children: [
{
label: 'little',
value: 'little',
},
],
},
]}
/>,
);
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(
<Cascader
open
checkable
showSearch={{ autoClearSearchValue: false }}
options={[
{
label: 'bamboo',
value: 'bamboo',
children: [
{
label: 'little',
value: 'little',
},
],
},
]}
/>,
);

const inputNode = container.querySelector<HTMLInputElement>(
'.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');
});
});
Loading