Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export default () => (
| direction | direction of dropdown | 'ltr' \| 'rtl' | 'ltr' |
| optionRender | Custom rendering options | (oriOption: FlattenOptionData\<BaseOptionType\> , info: { index: number }) => React.ReactNode | - |
| labelRender | Custom rendering label | (props: LabelInValueType) => React.ReactNode | - |
| activeOptionFilter | Custom filter function for active option when searching. | (searchValue: string, option: OptionType) => boolean | - |
| maxCount | The max number of items can be selected | number | - |

### Methods
Expand Down
34 changes: 34 additions & 0 deletions docs/examples/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,40 @@ class Combobox extends React.Component {
options={this.state.options}
onChange={this.onAsyncChange}
/>

<h3>Active Option Filter - Middle Match</h3>
<Select
style={{ width: 500 }}
showSearch
allowClear
mode="combobox"
placeholder="Search value can be matched anywhere in the option's value. Try input 'ran'"
activeOptionFilter={(searchValue, option) => {
return String(option.value).includes(searchValue);
}}
>
{['apple', 'banana', 'orange', 'grape'].map((i) => (
<Option value={i} key={i}>
{i}
</Option>
))}
</Select>

<h3>No Active Highlight</h3>
<Select
style={{ width: 500 }}
showSearch
allowClear
mode="combobox"
placeholder="No option will be actively highlighted."
activeOptionFilter={() => false}
>
{['apple', 'banana', 'orange', 'grape'].map((i) => (
<Option value={i} key={i}>
{i}
</Option>
))}
</Select>
</div>
</div>
);
Expand Down
10 changes: 7 additions & 3 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
listHeight,
listItemHeight,
optionRender,
activeOptionFilter,
classNames: contextClassNames,
styles: contextStyles,
} = React.useContext(SelectContext);
Expand Down Expand Up @@ -159,9 +160,12 @@
if (!multiple && open && rawValues.size === 1) {
const value: RawValueType = Array.from(rawValues)[0];
// Scroll to the option closest to the searchValue if searching.
const index = memoFlattenOptions.findIndex(({ data }) =>
searchValue ? String(data.value).startsWith(searchValue) : data.value === value,
);
const index = memoFlattenOptions.findIndex(({ data }) => {
if (activeOptionFilter) {
Copy link
Member

Choose a reason for hiding this comment

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

感觉有点怪异,如果是 value 被选中了,展开不应该是用当前这个逻辑。否则会导致选中的是一个,展开激活的却是另一个。它应该是 default active 的逻辑

return activeOptionFilter(searchValue, data);

Check warning on line 165 in src/OptionList.tsx

View check run for this annotation

Codecov / codecov/patch

src/OptionList.tsx#L165

Added line #L165 was not covered by tests
}
return searchValue ? String(data.value).startsWith(searchValue) : data.value === value;
});

Comment on lines 160 to 169
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

主动过滤逻辑对分组项存在空指针风险

findIndex 的回调中直接使用 data.value,然而当当前项为分组节点时 data 可能为 undefined,会导致运行时抛出 TypeError: Cannot read properties of undefined (reading 'value')。同时把 activeOptionFilter 应用到分组节点也没有意义。

建议在回调里先排除分组节点或缺少 data 的情况:

-      const index = memoFlattenOptions.findIndex(({ data }) => {
-        if (activeOptionFilter) {
-          return activeOptionFilter(searchValue, data);
-        }
-        return searchValue ? String(data.value).startsWith(searchValue) : data.value === value;
-      });
+      const index = memoFlattenOptions.findIndex(({ group, data }) => {
+        // 忽略分组标题或异常节点
+        if (group || !data) {
+          return false;
+        }
+
+        if (activeOptionFilter) {
+          return activeOptionFilter(searchValue, data);
+        }
+        return searchValue
+          ? String(data.value).startsWith(searchValue)
+          : data.value === value;
+      });

这样可以避免空指针,同时保持逻辑的一致性。

📝 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
if (!multiple && open && rawValues.size === 1) {
const value: RawValueType = Array.from(rawValues)[0];
// Scroll to the option closest to the searchValue if searching.
const index = memoFlattenOptions.findIndex(({ data }) =>
searchValue ? String(data.value).startsWith(searchValue) : data.value === value,
);
const index = memoFlattenOptions.findIndex(({ data }) => {
if (activeOptionFilter) {
return activeOptionFilter(searchValue, data);
}
return searchValue ? String(data.value).startsWith(searchValue) : data.value === value;
});
if (!multiple && open && rawValues.size === 1) {
const value: RawValueType = Array.from(rawValues)[0];
// Scroll to the option closest to the searchValue if searching.
- const index = memoFlattenOptions.findIndex(({ data }) => {
- if (activeOptionFilter) {
- return activeOptionFilter(searchValue, data);
- }
- return searchValue ? String(data.value).startsWith(searchValue) : data.value === value;
- });
+ const index = memoFlattenOptions.findIndex(({ group, data }) => {
+ // 忽略分组标题或异常节点
+ if (group || !data) {
+ return false;
+ }
+
+ if (activeOptionFilter) {
+ return activeOptionFilter(searchValue, data);
+ }
+ return searchValue
+ ? String(data.value).startsWith(searchValue)
+ : data.value === value;
+ });
🤖 Prompt for AI Agents
In src/OptionList.tsx around lines 160 to 169, the findIndex callback accesses
data.value without checking if data exists, causing a TypeError when the item is
a group node with undefined data. To fix this, add a guard clause in the
callback to skip items where data is undefined or the item is a group node
before accessing data.value or applying activeOptionFilter, ensuring no null
pointer errors and preserving the filtering logic.

if (index !== -1) {
setActive(index);
Expand Down
4 changes: 4 additions & 0 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
listHeight?: number;
listItemHeight?: number;
labelRender?: (props: LabelInValueType) => React.ReactNode;
activeOptionFilter?: (searchValue: string, option: OptionType) => boolean;

// >>> Icon
menuItemSelectedIcon?: RenderNode;
Expand Down Expand Up @@ -213,6 +214,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
maxCount,
classNames,
styles,
activeOptionFilter,
...restProps
} = props;

Expand Down Expand Up @@ -647,6 +649,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
optionRender,
classNames,
styles,
activeOptionFilter,
};
}, [
maxCount,
Expand All @@ -667,6 +670,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
optionRender,
classNames,
styles,
activeOptionFilter,
]);

// ========================== Warning ===========================
Expand Down
1 change: 1 addition & 0 deletions src/SelectContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface SelectContextProps {
listItemHeight?: number;
childrenAsData?: boolean;
maxCount?: number;
activeOptionFilter?: SelectProps['activeOptionFilter'];
}

const SelectContext = React.createContext<SelectContextProps>(null);
Expand Down
Loading