Skip to content
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
18 changes: 14 additions & 4 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import useBaseProps from './hooks/useBaseProps';
import type { FlattenOptionData } from './interface';
import { isPlatformMac } from './utils/platformUtil';
import { isValidCount } from './utils/valueUtil';
import { toArray } from './utils/commonUtil';

// export interface OptionListProps<OptionsType extends object[]> {
export type OptionListProps = Record<string, never>;
Expand All @@ -29,6 +30,10 @@ function isTitleType(content: any) {
return typeof content === 'string' || typeof content === 'number';
}

function includes(test: React.ReactNode, search: string) {
return toArray(test).join('').toUpperCase().includes(search.toUpperCase());
}
Comment on lines +33 to +35

Choose a reason for hiding this comment

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

medium

为了提高代码的可维护性和重用性,建议将此 includes 函数移动到共享的工具文件(例如 src/utils/commonUtil.ts)中。在 src/hooks/useFilterOptions.ts 中也存在一个功能非常相似的函数。通过将此逻辑提取到公共位置,可以消除代码重复,并确保在整个组件中进行筛选时行为一致。

例如,您可以在 src/utils/commonUtil.ts 中创建一个名为 caseInsensitiveIncludes 的函数,然后在此处和 useFilterOptions.ts 中导入和使用它。


Comment on lines +33 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

includes 对 ReactNode 的文本提取不可靠,建议递归提取纯文本以对齐 useFilterOptions 行为

当前 join 可能丢失元素内的文本(如 America),从而错过匹配,导致未能滚到期望位置。可改为递归提取 children 文本:

-function includes(test: React.ReactNode, search: string) {
-  return toArray(test).join('').toUpperCase().includes(search.toUpperCase());
-}
+function nodeText(n: React.ReactNode): string {
+  if (n == null || n === false) return '';
+  if (typeof n === 'string' || typeof n === 'number') return String(n);
+  if (Array.isArray(n)) return n.map(nodeText).join('');
+  if (React.isValidElement(n)) return nodeText((n as any).props?.children);
+  return '';
+}
+function includes(test: React.ReactNode, search: string) {
+  return nodeText(test).toUpperCase().includes(search.toUpperCase());
+}
📝 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
function includes(test: React.ReactNode, search: string) {
return toArray(test).join('').toUpperCase().includes(search.toUpperCase());
}
function nodeText(n: React.ReactNode): string {
if (n == null || n === false) return '';
if (typeof n === 'string' || typeof n === 'number') return String(n);
if (Array.isArray(n)) return n.map(nodeText).join('');
if (React.isValidElement(n)) return nodeText((n as any).props?.children);
return '';
}
function includes(test: React.ReactNode, search: string) {
return nodeText(test).toUpperCase().includes(search.toUpperCase());
}
🤖 Prompt for AI Agents
In src/OptionList.tsx around lines 33 to 36, the current includes function
flattens a ReactNode via toArray().join('') which can drop nested element text
(e.g., <mark>Am</mark>erica) and miss matches; replace it with a recursive
text-extraction routine that walks the ReactNode tree: if the node is
null/boolean return empty string, if string/number convert to string, if an
array map and concat, and if a React element access its props.children and
recursively extract text; then uppercase and perform the includes check so
behavior matches useFilterOptions and nested/marked text is reliably matched.

/**
* Using virtual list of option display.
* Will fallback to dom if use customize render.
Expand Down Expand Up @@ -60,6 +65,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
listHeight,
listItemHeight,
optionRender,
optionFilterProp,
classNames: contextClassNames,
styles: contextStyles,
} = React.useContext(SelectContext);
Expand Down Expand Up @@ -159,9 +165,13 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
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 (searchValue) {
const matchValue = optionFilterProp ? data[optionFilterProp] : data.value;
return includes(matchValue, searchValue);
}
return data.value === value;
});

Comment on lines +168 to 175
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

定位所选项/匹配项时不应使用 data.value;应使用扁平项的 item.value,且当 optionFilterProp 未命中时需兜底 fieldNames.value

当自定义 fieldNames(例如 value 映射为 id)时,data.value 可能为 undefined,导致无法在未搜索时滚动到已选项;另外,搜索时未提供 optionFilterProp 时,也应优先按 fieldNames.value 查找,否则会偏离 useFilterOptions 的字段选择。

-      const index = memoFlattenOptions.findIndex(({ data }) => {
-        if (searchValue) {
-          const matchValue = optionFilterProp ? data[optionFilterProp] : data.value;
-          return includes(matchValue, searchValue);
-        }
-        return data.value === value;
-      });
+      const index = memoFlattenOptions.findIndex(({ data, value: itemValue, group }) => {
+        if (group) return false;
+        if (searchValue) {
+          const field = optionFilterProp || fieldNames?.value || 'value';
+          const matchValue = data?.[field];
+          return includes(matchValue, searchValue);
+        }
+        // 未搜索时按已选值精确命中,应使用扁平后的 itemValue
+        return itemValue === value;
+      });
+      // 兜底:如未找到匹配项但处于搜索态,则滚到第一个可用选项(即“置顶”期望)
+      const finalIndex = index !== -1 ? index : (searchValue ? getEnabledActiveIndex(0) : -1);

并同步使用 finalIndex:

-      if (index !== -1) {
-        setActive(index);
+      if (finalIndex !== -1) {
+        setActive(finalIndex);
         timeoutId = setTimeout(() => {
-          scrollIntoView(index);
+          scrollIntoView(finalIndex);
         });
       }
🤖 Prompt for AI Agents
In src/OptionList.tsx around lines 168 to 175, the current findIndex uses
data.value which can be undefined when callers pass custom fieldNames (e.g.
value mapped to id) and also doesn't fallback to fieldNames.value when
optionFilterProp is not provided; update the logic to read the flattened item
(use item.value) and when matching use optionFilterProp ? data[optionFilterProp]
: data[fieldNames.value] (or item.value as final fallback) for search matches,
and when not searching compare item.value to the selected value; finally ensure
the computed index is stored/used via finalIndex consistently wherever the
previous index variable was used.

if (index !== -1) {
setActive(index);
Expand All @@ -177,7 +187,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
}

return () => clearTimeout(timeoutId);
}, [open, searchValue]);
}, [open, searchValue, optionFilterProp]);

// ========================== Values ==========================
const onSelectValue = (value: RawValueType) => {
Expand Down
2 changes: 2 additions & 0 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
childrenAsData,
maxCount,
optionRender,
optionFilterProp,
classNames,
styles,
};
Expand All @@ -697,6 +698,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
listItemHeight,
childrenAsData,
optionRender,
optionFilterProp,
classNames,
styles,
]);
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;
optionFilterProp?: string;
}

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