Skip to content

feat: add activeOptionFilter prop and update combobox examples #1156

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

chmod777john
Copy link

@chmod777john chmod777john commented Jun 12, 2025

按照原有的逻辑,会默认根据 input 字符串 startsWith 规则自动将匹配的第一项设置为 activate ,且这种行为写死不可配置。

我认为以上行为不对,匹配规则应当能可选地由用户自己配置。

我的修改使得用户可以自行定制匹配规则,详见代码里的两个例子

任意部分匹配(而不仅开头)
图片

强制不匹配(很多时候用户希望按下 Enter 键会直接发起搜索,而不是自动选中第一项)
图片

像B站搜索框就是不会默认 activate 第一个匹配项的
图片

Summary by CodeRabbit

Summary by CodeRabbit

  • 新功能
    • Select 组件新增可选属性 activeOptionFilter,允许自定义搜索时激活选项的过滤逻辑。
    • 文档和示例中增加了 activeOptionFilter 的用法演示,包括自定义匹配和禁用高亮的场景。

Copy link

vercel bot commented Jun 12, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
select ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jun 12, 2025 2:17pm

Copy link
Contributor

coderabbitai bot commented Jun 12, 2025

Walkthrough

本次更改为 Select 组件引入了新的可选属性 activeOptionFilter,允许开发者自定义在搜索时激活选项的过滤逻辑。相关文档、类型声明和上下文传递均已更新,并在示例中展示了该属性的实际用法。测试中也新增了对应的行为验证。

Changes

文件/分组 变更摘要
README.md 在 Select 组件 API 文档中新增 activeOptionFilter 属性说明。
docs/examples/combobox.tsx 新增两个使用 activeOptionFilter 的 Select 组件示例。
src/OptionList.tsx 支持通过 context 获取并应用 activeOptionFilter 进行激活项匹配。
src/Select.tsx 在 SelectProps 和 context 中新增可选属性 activeOptionFilter
src/SelectContext.ts 在 SelectContextProps 接口中新增 activeOptionFilter 属性。
tests/Combobox.test.tsx 新增测试用例验证 activeOptionFilter 在 Combobox 模式下的行为。

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Select
    participant OptionList

    User->>Select: 输入搜索内容
    Select->>OptionList: 传递 activeOptionFilter、searchValue 和 options
    OptionList->>OptionList: 使用 activeOptionFilter 判断激活项
    OptionList-->>Select: 返回激活项索引
    Select-->>User: 高亮/滚动到对应选项
Loading

Possibly related PRs

Suggested reviewers

  • afc163

Poem

🥕
兔子敲键盘,代码添新意,
选项激活,过滤更随心。
搜索中间也能配,
高亮逻辑自定义。
组件升级喜盈盈,
代码花园又添新!

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/Select.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Cannot read config file: /.eslintrc.js
Error: Cannot find module '@umijs/fabric/dist/eslint'
Require stack:

  • /.eslintrc.js
  • /node_modules/.pnpm/@eslint[email protected]/node_modules/@eslint/eslintrc/dist/eslintrc.cjs
  • /node_modules/.pnpm/[email protected]/node_modules/eslint/lib/cli-engine/cli-engine.js
  • /node_modules/.pnpm/[email protected]/node_modules/eslint/lib/eslint/eslint.js
  • /node_modules/.pnpm/[email protected]/node_modules/eslint/lib/eslint/index.js
  • /node_modules/.pnpm/[email protected]/node_modules/eslint/lib/cli.js
  • /node_modules/.pnpm/[email protected]/node_modules/eslint/bin/eslint.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1061:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1066:22)
    at Module._load (node:internal/modules/cjs/loader:1215:37)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Module.require (node:internal/modules/cjs/loader:1491:12)
    at require (node:internal/modules/helpers:135:16)
    at Object. (/.eslintrc.js:1:14)
    at Module._compile (node:internal/modules/cjs/loader:1734:14)

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c023b4b and 4779371.

📒 Files selected for processing (1)
  • src/Select.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Select.tsx
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Commit Unit Tests in branch activeFilterOption
  • Post Copyable Unit Tests in Comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/Select.tsx (1)

217-218: 建议:提醒使用方包裹 activeOptionFilter 以保持引用稳定

虽然这里没有问题,但如果调用者在渲染期间不断创建新函数,会导致 selectContext 频繁变动,进而触发 OptionList 重新渲染。可以在文档中提示用户使用 useCallback
[建议,可选优化]

docs/examples/combobox.tsx (2)

155-163: 示例函数建议用 useCallback 保持稳定引用

示例中 activeOptionFilter 直接内联定义,每次渲染都会创建新函数,不利于性能演示。虽然示例影响有限,但推荐写法为:

const middleMatch = React.useCallback(
  (searchValue, option) => String(option.value).includes(searchValue),
  [],
);
...
activeOptionFilter={middleMatch}

173-180: 同上,禁用高亮示例也可提取回调

保持代码一致性和最佳实践。

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7aa0c21 and 3147942.

📒 Files selected for processing (5)
  • README.md (1 hunks)
  • docs/examples/combobox.tsx (1 hunks)
  • src/OptionList.tsx (2 hunks)
  • src/Select.tsx (4 hunks)
  • src/SelectContext.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/SelectContext.ts (1)
src/Select.tsx (1)
  • SelectProps (113-167)
🔇 Additional comments (3)
src/SelectContext.ts (1)

40-41: activeOptionFilter 字段加入上下文 👍

上下文定义同步新增了 activeOptionFilter,类型引用 SelectProps['activeOptionFilter'],保持了单一来源,非常可读。

src/Select.tsx (1)

154-155: Props 透传完整,依赖数组同步更新已到位

activeOptionFilter 已正确暴露到 SelectProps 并随 selectContext 一并 memo 化,符合预期。

README.md (1)

135-136: 文档更新同步,可读性良好

activeOptionFilter 参数已补充到 API 表格,没有遗漏默认值说明,清晰易懂。

Comment on lines 160 to 169
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;
});

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.

Copy link

codecov bot commented Jun 12, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.23%. Comparing base (7aa0c21) to head (4779371).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1156   +/-   ##
=======================================
  Coverage   98.23%   98.23%           
=======================================
  Files          38       38           
  Lines        1471     1473    +2     
  Branches      446      440    -6     
=======================================
+ Hits         1445     1447    +2     
  Misses         26       26           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/Combobox.test.tsx (2)

135-149: 断言过于依赖固定选项顺序,建议改为断言“首个满足条件的选项”

当前测试假设匹配字符串 'an' 时激活的一定是 'banana',而这取决于渲染顺序与内部遍历实现;
如若以后更改为“按字母排序后再匹配”或“返回所有匹配中的首条可见项”,测试就会误失败。
可考虑改为:

  1. 先拿到所有 .rc-select-item-option-active 元素,断言其数量为 1。
  2. 断言其 textContentinput 当前值存在包含关系(或调用同一 activeOptionFilter 判断为真)。

这样能保证测试关注“过滤逻辑正确”而非“具体哪条被激活”。


151-165: 缺少对「无匹配时亦不激活」场景的负面验证

第二个用例只在输入 'an' 时断言无 active 选项;若 activeOptionFilter 总返回 false,理应在任意输入下都不激活。
可追加一次输入 'ora' 的断言,增强覆盖率:

 fireEvent.change(input, { target: { value: 'an' } });
 await delay();
 expect(container.querySelector('.rc-select-item-option-active')).toBeNull();
+
+fireEvent.change(input, { target: { value: 'ora' } });
+await delay();
+expect(container.querySelector('.rc-select-item-option-active')).toBeNull();

这样能防止实现不一致导致 “第一次正常、第二次错误” 却漏检。

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3147942 and c023b4b.

📒 Files selected for processing (1)
  • tests/Combobox.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/Combobox.test.tsx (1)
docs/examples/combobox.tsx (1)
  • render (68-190)

Comment on lines +120 to +134
it('activeOptionFilter should work', async () => {
const { container } = render(
<Select
mode="combobox"
showSearch
activeOptionFilter={(searchValue, option) => {
return String(option.value).includes(searchValue);
}}
>
<Option value="apple">apple</Option>
<Option value="banana">banana</Option>
<Option value="orange">orange</Option>
</Select>,
);

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

建议使用 waitForfindBy... 替换自定义 delay 以消除非必要的 setTimeout

delay() 只是为了等下一个事件循环,但这会引入真实 setTimeout,在 CI 上容易出现偶发现象。
采用 Testing-Library 自带的 waitFor(或直接使用 findByRole / findByText)能在内部自动处理异步刷新,更符合该库的最佳实践,也无需显式 await delay()

- fireEvent.change(input, { target: { value: 'an' } });
- await delay();
+ fireEvent.change(input, { target: { value: 'an' } });
+ await waitFor(() =>
+   expect(
+     container.querySelector('.rc-select-item-option-active .rc-select-item-option-content'),
+   ).toHaveTextContent('banana'),
+ );

同理可删除自定义 delay 函数。
这样做能减少对实现细节的耦合,并提升测试稳定性。

📝 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
it('activeOptionFilter should work', async () => {
const { container } = render(
<Select
mode="combobox"
showSearch
activeOptionFilter={(searchValue, option) => {
return String(option.value).includes(searchValue);
}}
>
<Option value="apple">apple</Option>
<Option value="banana">banana</Option>
<Option value="orange">orange</Option>
</Select>,
);
it('activeOptionFilter should work', async () => {
const { container } = render(
<Select
mode="combobox"
showSearch
activeOptionFilter={(searchValue, option) => {
return String(option.value).includes(searchValue);
}}
>
<Option value="apple">apple</Option>
<Option value="banana">banana</Option>
<Option value="orange">orange</Option>
</Select>,
);
const input = container.querySelector('input')!;
fireEvent.change(input, { target: { value: 'an' } });
await waitFor(() =>
expect(
container.querySelector('.rc-select-item-option-active .rc-select-item-option-content'),
).toHaveTextContent('banana'),
);
});
🤖 Prompt for AI Agents
In tests/Combobox.test.tsx around lines 120 to 134, replace any custom delay or
setTimeout calls used to wait for asynchronous updates with Testing Library's
built-in waitFor or findBy... queries. Remove the custom delay function
entirely. This change will make the tests more stable and aligned with best
practices by relying on Testing Library's internal async handling instead of
manual timing.

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 的逻辑

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants