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

Add displayInInput option to allow externally controlled search #548

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions docs/demo/external-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: external search
nav:
title: Demo
path: /demo
---

<code src="../../examples/external-search.tsx"></code>
96 changes: 96 additions & 0 deletions examples/external-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import '../assets/index.less';
import Cascader from '../src';

const addressOptions = [
{
label: '福建',
value: 'fj',
children: [
{
label: '福州',
value: 'fuzhou',
children: [
{
label: '马尾-mw',
value: 'mawei',
},
],
},
{
label: '泉州-qz',
value: 'quanzhou',
},
],
},
{
label: '浙江',
value: 'zj',
children: [
{
label: '杭州',
value: 'hangzhou',
children: [
{
label: '余杭',
value: 'yuhang',
},
{
label: '福州',
value: 'fuzhou',
children: [
{
label: '马尾',
value: 'mawei',
},
],
},
],
},
],
},
{
label: '北京',
value: 'bj',
children: [
{
label: '朝阳区',
value: 'chaoyang',
},
{
label: '海淀区',
value: 'haidian',
},
],
},
];

const Demo = () => {
const [searchValue, setSearchValue] = useState('');
return (
<>
<Cascader
options={addressOptions}
showSearch={{ displayInInput: false }}
searchValue={searchValue}
style={{ width: 300 }}
dropdownRender={menu => {
return (
<div>
<input
value={searchValue}
onChange={e => setSearchValue(e.target.value)}
placeholder="External Search"
Comment on lines +81 to +83
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

建议添加输入验证和错误处理

当前实现缺少输入验证和错误处理机制:

  1. 没有对搜索值进行长度限制
  2. 没有处理特殊字符
  3. 缺少防抖处理

建议添加以下功能:

+import { useMemo } from 'react';
+import debounce from 'lodash/debounce';
+
 const Demo = () => {
   const [searchValue, setSearchValue] = useState('');
+  const [error, setError] = useState('');
+
+  const handleSearch = useMemo(
+    () =>
+      debounce((value: string) => {
+        if (value.length > 50) {
+          setError('搜索内容不能超过50个字符');
+          return;
+        }
+        setError('');
+        setSearchValue(value);
+      }, 300),
+    [],
+  );
+
   return (
     // ...
     <input
       value={searchValue}
-      onChange={e => setSearchValue(e.target.value)}
+      onChange={e => handleSearch(e.target.value)}
       placeholder="External Search"
+      maxLength={50}
     />
+    {error && <div className="error-message">{error}</div>}

Committable suggestion skipped: line range outside the PR's diff.

/>
Comment on lines +80 to +84
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

改进输入框的可访问性和用户体验

搜索输入框缺少一些重要的可访问性和用户体验特性:

  1. 缺少 aria-label 属性
  2. 未指定输入框类型
  3. 建议添加搜索图标以提升可识别性

建议修改如下:

 <input
   value={searchValue}
   onChange={e => setSearchValue(e.target.value)}
   placeholder="External Search"
+  type="search"
+  aria-label="外部搜索输入框"
+  className="external-search-input"
 />

另外,建议在输入框旁添加搜索图标:

import { SearchOutlined } from '@ant-design/icons';

// 在输入框前添加图标
<span className="search-icon">
  <SearchOutlined />
</span>

{menu}
</div>
);
}}
animation="slide-up"
notFoundContent="Empty Content!"
/>
</>
);
};

export default Demo;
1 change: 1 addition & 0 deletions src/Cascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface ShowSearchType<
inputValue: string,
fieldNames: FieldNames<OptionType, ValueField>,
) => number;
displayInInput?: boolean;
matchInputWidth?: boolean;
limit?: number | false;
}
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useSearchConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function useSearchConfig(showSearch?: CascaderProps['showSearch']
let searchConfig: ShowSearchType = {
matchInputWidth: true,
limit: 50,
displayInInput: true,
};

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

return [true, searchConfig];
return [!!searchConfig.displayInInput, searchConfig];
}, [showSearch]);
}
45 changes: 44 additions & 1 deletion tests/search.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fireEvent, render } from '@testing-library/react';
import KeyCode from 'rc-util/lib/KeyCode';
import { resetWarned } from 'rc-util/lib/warning';
import React from 'react';
import React, { useState } from 'react';
import Cascader from '../src';
import { optionsForActiveMenuItems } from './demoOptions';
import type { ReactWrapper } from './enzyme';
Expand Down Expand Up @@ -73,6 +73,49 @@
expect(onChange).toHaveBeenCalledWith(['bamboo', 'little', 'fish'], expect.anything());
});

it('externally controlled search',() => {
const onSearch = jest.fn();
const onChange = jest.fn();

function doExternalSearch(wrapper: ReactWrapper, search: string) {
wrapper.find('input[data-test="external-search"]').simulate('change', {
target: {
value: search,
},
});
}


const ExternallyControlledSearch = () => {
const [searchValue,setSearchValue] = useState('')
return (
<>
<input data-test="external-search" value={searchValue} onChange={e => setSearchValue(e.target.value)} />
<Cascader options={options} searchValue={searchValue} onChange={onChange} onSearch={onSearch} open showSearch={{displayInInput:false}} />,
</>
);
}
Comment on lines +89 to +97
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

需要修复测试组件中的语法和类型问题

ExternallyControlledSearch 组件存在以下问题:

  1. JSX中存在多余的逗号
  2. 代码格式不统一
  3. 缺少类型注释
-const ExternallyControlledSearch = () => {
-  const [searchValue,setSearchValue] = useState('')
+interface ExternallyControlledSearchProps {}
+
+const ExternallyControlledSearch: React.FC<ExternallyControlledSearchProps> = () => {
+  const [searchValue, setSearchValue] = useState<string>('')
   return (
     <>
       <input data-test="external-search" value={searchValue} onChange={e => setSearchValue(e.target.value)} />
-      <Cascader options={options} searchValue={searchValue} onChange={onChange} onSearch={onSearch} open showSearch={{displayInInput:false}} />,
+      <Cascader
+        options={options}
+        searchValue={searchValue}
+        onChange={onChange}
+        onSearch={onSearch}
+        open
+        showSearch={{ displayInInput: false }}
+      />
     </>      
   );
 }
📝 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
const ExternallyControlledSearch = () => {
const [searchValue,setSearchValue] = useState('')
return (
<>
<input data-test="external-search" value={searchValue} onChange={e => setSearchValue(e.target.value)} />
<Cascader options={options} searchValue={searchValue} onChange={onChange} onSearch={onSearch} open showSearch={{displayInInput:false}} />,
</>
);
}
interface ExternallyControlledSearchProps {}
const ExternallyControlledSearch: React.FC<ExternallyControlledSearchProps> = () => {
const [searchValue, setSearchValue] = useState<string>('')
return (
<>
<input data-test="external-search" value={searchValue} onChange={e => setSearchValue(e.target.value)} />
<Cascader
options={options}
searchValue={searchValue}
onChange={onChange}
onSearch={onSearch}
open
showSearch={{ displayInInput: false }}
/>
</>
);
}

const wrapper = mount(<ExternallyControlledSearch/>)

// Leaf
doExternalSearch(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');

// Parent
doExternalSearch(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');

// Change
wrapper.clickOption(0, 0);
expect(onChange).toHaveBeenCalledWith(['bamboo', 'little', 'fish'], expect.anything());
})

Check notice

Code scanning / CodeQL

Semicolon insertion Note test

Avoid automated semicolon insertion (94% of all statements in
the enclosing function
have an explicit semicolon).

it('changeOnSelect', () => {
const onChange = jest.fn();
const wrapper = mount(
Expand Down
Loading