Skip to content

Fix:The dropdown list is not following the highlighted item when navigating through the result using up or down arrow after entering keyword #575

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 8 commits into from
Mar 13, 2025

Conversation

jin19980928
Copy link
Contributor

@jin19980928 jin19980928 commented Mar 7, 2025

issuse:ant-design/ant-design#53050

Summary by CodeRabbit

  • 新功能

    • 优化了选项组件的滚动表现。现在当激活项更改时,界面会自动将其滚动至可视区域,提升键盘导航时的用户体验。
  • 测试

    • 增加了验证自动滚动效果的测试用例,确保激活项在导航过程中的显示符合预期。
    • 引入了对 scrollIntoView 方法的模拟实现,以便于测试。

Copy link

vercel bot commented Mar 7, 2025

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

Name Status Preview Comments Updated (UTC)
cascader ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 12, 2025 7:03am

Copy link

coderabbitai bot commented Mar 7, 2025

Walkthrough

本次更新调整了 Cascader 组件的测试逻辑和交互行为。在测试文件中,移除了 getOffesetTopTimes 变量,使 offsetTop 始终返回 0,并新增了一个通过键盘导航验证组件滚动进入视图的测试用例。在 src/OptionList/Column.tsx 中,新增了一个 React.useEffect 钩子,监听 activeValue 的变化,通过构造选择器查找对应的 DOM 元素,并调用 scrollIntoView 使活跃项滚动至可视区域。公共接口未作修改。

Changes

文件路径 变化摘要
tests/index.spec.tsx 移除 getOffesetTopTimes,固定 offsetTop 返回值为 0,并新增键盘导航测试用例,确保调用 scrollIntoView 方法检查菜单项可视性。
src/OptionList/Column.tsx 新增 useEffect 钩子监听 activeValue 变化,构造选择器查找对应 DOM 元素,并调用 scrollIntoView 使活跃项滚动至视图;同时引入了该函数。
tests/setup.js 新增 scrollIntoView 的 Jest 模拟实现,允许在测试中模拟该方法的行为。

Sequence Diagram(s)

sequenceDiagram
    participant C as Column组件
    participant E as useEffect
    participant D as document.querySelector
    participant S as scrollIntoView
    C->>E: activeValue 更新
    E->>D: 构造选择器查询 DOM
    D-->>E: 返回对应元素 (或 null)
    E->>S: 调用 scrollIntoView(若元素存在)
Loading
sequenceDiagram
    participant T as 测试用例
    participant C as Cascader组件
    participant M as 活跃菜单项DOM
    T->>C: 发送键盘导航事件
    C->>M: 激活对应菜单项
    M->>M: 调用 scrollIntoView 方法
Loading

Possibly related PRs

  • feat: optimize search #530: 该 PR 涉及 scrollIntoView 方法的使用,与本次更新的测试用例密切相关。
  • fix: search no scroll #532: 修改了 src/OptionList/Column.tsx,改进了组件滚动进入视图的逻辑,与本次更新在代码层面具有较强关联性。

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/OptionList/Column.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the config "prettier" to extend from. Please check that the name of the config is correct.

The config "prettier" was referenced from the config file in "/.eslintrc.js".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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

@@ -186,9 +186,6 @@ const RawOptionList = React.forwardRef<RefOptionListProps, RawOptionListProps>((

// >>>>> Active Scroll
React.useEffect(() => {
if (searchValue) {
Copy link
Member

Choose a reason for hiding this comment

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

这个以前有一定是有什么原因的

Copy link
Contributor Author

Choose a reason for hiding this comment

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

看了下之前的PR:#532
如果searchValue 有值,会阻止滚动,这个行为是不合理的,不应该阻止滚动。

Copy link

codecov bot commented Mar 9, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.83%. Comparing base (ee97a7d) to head (4537525).
Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #575   +/-   ##
=======================================
  Coverage   99.83%   99.83%           
=======================================
  Files          23       23           
  Lines         610      610           
  Branches      186      186           
=======================================
  Hits          609      609           
  Misses          1        1           

☔ 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

@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: 0

♻️ Duplicate comments (1)
src/OptionList/Column.tsx (1)

103-114: ⚠️ Potential issue

良好的滚动实现,但存在安全问题

这段代码实现了当活跃选项改变时自动滚动到视图中的功能,这对于键盘导航体验来说是一个很好的改进。

但是,我注意到第104行中的字符串转义可能不完整:

const escapedValue = String(activeValue).replace(/"/g, '\\"');

仅替换双引号是不够的,如果activeValue包含其他特殊字符(如\'()等),可能会导致CSS选择器注入风险。

建议使用更全面的转义方法:

-const escapedValue = String(activeValue).replace(/"/g, '\\"');
+const escapedValue = CSS.escape(String(activeValue));

CSS.escape() 方法可以正确转义所有CSS选择器中的特殊字符,提供更好的安全保障。

🧹 Nitpick comments (3)
src/OptionList/Column.tsx (3)

103-114: 考虑添加性能优化

当前实现在每次 activeValue 变化时都会查询整个文档,这在选项很多时可能会影响性能。

建议通过以下方式优化:

  1. 使用组件内的引用来限制查询范围
+ const columnRef = React.useRef<HTMLUListElement>(null);

  React.useEffect(() => {
    const escapedValue = String(activeValue).replace(/"/g, '\\"');
    const selector = `[data-path-key="${escapedValue}"]`;
-   const activeElement = document.querySelector<HTMLElement>(selector);
+   const activeElement = columnRef.current?.querySelector<HTMLElement>(selector);

    if (activeElement) {
      activeElement.scrollIntoView({
        block: 'nearest',
        inline: 'nearest',
      });
    }
  }, [activeValue]);
  
  // 在返回的JSX中添加ref
  return (
-   <ul className={menuPrefixCls} role="menu">
+   <ul className={menuPrefixCls} role="menu" ref={columnRef}>
  1. 考虑添加防抖或节流,特别是在快速导航时

103-114: 添加注释说明修复的问题

这段代码解决了PR描述中提到的问题,但代码中没有解释这个实现的目的。

建议添加简短注释:

  React.useEffect(() => {
+   // 确保当使用键盘导航时,活跃项会滚动到视图中
    const escapedValue = String(activeValue).replace(/"/g, '\\"');
    const selector = `[data-path-key="${escapedValue}"]`;
    const activeElement = document.querySelector<HTMLElement>(selector);

103-114: 考虑添加错误处理

当前实现没有处理可能的异常情况,如无效的activeValue值或DOM操作失败。

建议添加简单的错误处理:

  React.useEffect(() => {
+   if (activeValue === undefined || activeValue === null) return;
+   
    const escapedValue = String(activeValue).replace(/"/g, '\\"');
    const selector = `[data-path-key="${escapedValue}"]`;
-   const activeElement = document.querySelector<HTMLElement>(selector);
+   try {
+     const activeElement = document.querySelector<HTMLElement>(selector);
+     
+     if (activeElement) {
+       activeElement.scrollIntoView({
+         block: 'nearest',
+         inline: 'nearest',
+       });
+     }
+   } catch (error) {
+     console.error('Failed to scroll active item into view:', error);
+   }
-   if (activeElement) {
-     activeElement.scrollIntoView({
-       block: 'nearest',
-       inline: 'nearest',
-     });
-   }
  }, [activeValue]);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 387b00e and a55564b.

📒 Files selected for processing (2)
  • src/OptionList/Column.tsx (1 hunks)
  • tests/index.spec.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/index.spec.tsx

@@ -100,6 +100,22 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
[options, checkedSet, fieldNames, halfCheckedSet, loadingKeys, prevValuePath],
);

React.useEffect(() => {
const escapedValue = String(activeValue)
.replace(/\\/g, '\\\\')
Copy link
Member

Choose a reason for hiding this comment

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

有没有更好的方式来搜索对应的 ele?感觉这个 escape 有点太黑了

Copy link

@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: 0

🧹 Nitpick comments (1)
src/OptionList/Column.tsx (1)

103-112: 建议优化查找活动元素的方法

当前实现直接使用document.querySelector查找活动元素可能存在以下问题:

  1. 绕过了React的虚拟DOM流程进行直接DOM操作
  2. 没有处理menuItemPrefixCls中可能包含的特殊字符
  3. 如果页面上有多个Cascader实例,可能会选中错误的元素
  4. 在整个文档范围内查询可能影响性能

以下是改进建议:

- React.useEffect(() => {
-   const activeElement = document.querySelector<HTMLElement>(`.${menuItemPrefixCls}-active`);
- 
-   if (activeElement) {
-     activeElement.scrollIntoView({
-       block: 'nearest',
-       inline: 'nearest',
-     });
-   }
- }, [activeValue]);
+ // 使用ref引用列表容器元素
+ const containerRef = React.useRef<HTMLUListElement>(null);
+ 
+ React.useEffect(() => {
+   if (!containerRef.current || !activeValue) return;
+   
+   // 在容器范围内查找活动元素,避免全局查询
+   const activeElement = containerRef.current.querySelector<HTMLElement>(
+     `.${menuItemPrefixCls}-active`
+   );
+   
+   if (activeElement) {
+     activeElement.scrollIntoView({
+       block: 'nearest',
+       inline: 'nearest',
+     });
+   }
+ }, [activeValue, menuItemPrefixCls]);

然后在返回的JSX中添加ref:

- return (
-   <ul className={menuPrefixCls} role="menu">
+ return (
+   <ul ref={containerRef} className={menuPrefixCls} role="menu">

这种方法可以:

  • 限制查询范围在当前组件内
  • 避免与其他Cascader实例冲突
  • 提高性能
  • 确保使用正确的menuItemPrefixCls
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7c2c2e0 and 1be5519.

📒 Files selected for processing (1)
  • src/OptionList/Column.tsx (1 hunks)

Copy link

@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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d803482 and 4537525.

📒 Files selected for processing (1)
  • src/OptionList/Column.tsx (2 hunks)
🔇 Additional comments (2)
src/OptionList/Column.tsx (2)

46-46: 新增 menuRef 用于获取 DOM 元素,逻辑清晰

添加对 DOM 节点的引用将有助于实现滚动功能,是实现需求的必要步骤。


120-120: 正确添加了 ref 属性到 ul 元素

将 menuRef 正确绑定到 ul 元素上,使得 DOM 操作逻辑能够正常工作。

Comment on lines +104 to +116
React.useEffect(() => {
if (menuRef.current) {
const selector = `.${menuItemPrefixCls}-active`;
const activeElement = menuRef.current.querySelector<HTMLElement>(selector);

if (activeElement) {
activeElement.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
}
}
}, [activeValue, menuItemPrefixCls]);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

实现了滚动高亮项到视图功能,但存在潜在安全隐患

通过监听 activeValue 变更并调用 scrollIntoView 的方式实现了下拉列表跟随高亮项的功能,解决了原有的键盘导航问题。

然而,构建选择器时未对 menuItemPrefixCls 进行转义处理,若其中包含 CSS 选择器的特殊字符(如 .#:等),可能导致选择器失效或存在潜在的注入风险。

建议按照下面的方式进行修改,使用 data 属性替代类选择器,避免潜在问题:

 React.useEffect(() => {
   if (menuRef.current) {
-    const selector = `.${menuItemPrefixCls}-active`;
-    const activeElement = menuRef.current.querySelector<HTMLElement>(selector);
+    const activeElement = menuRef.current.querySelector<HTMLElement>('[data-active="true"]');

     if (activeElement) {
       activeElement.scrollIntoView({
         block: 'nearest',
         inline: 'nearest',
       });
     }
   }
 }, [activeValue, menuItemPrefixCls]);

同时,需要调整 <li> 元素的属性:

 <li
   key={fullPathKey}
   className={classNames(menuItemPrefixCls, {
     [`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
     [`${menuItemPrefixCls}-active`]:
       activeValue === value || activeValue === fullPathKey,
     [`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
     [`${menuItemPrefixCls}-loading`]: isLoading,
   })}
+  data-active={activeValue === value || activeValue === fullPathKey ? 'true' : undefined}
   style={dropdownMenuColumnStyle}
   role="menuitemcheckbox"
   ...

关键安全隐患:建议使用 data-active 属性替换基于 menuItemPrefixCls 构造的动态类名选择器

  • 当前 useEffect 中使用 `.${menuItemPrefixCls}-active` 构造 CSS 选择器直接查找高亮项,这在菜单项的前缀值含有特殊字符(如 .#: 等)时可能导致选择器失效,甚至引发潜在的注入风险。
  • 建议改用 data 属性来标识高亮状态,这样既能避免字符串拼接带来的安全隐患,也能提高代码的可维护性和健壮性。

具体建议修改如下:

 React.useEffect(() => {
   if (menuRef.current) {
-    const selector = `.${menuItemPrefixCls}-active`;
-    const activeElement = menuRef.current.querySelector<HTMLElement>(selector);
+    const activeElement = menuRef.current.querySelector<HTMLElement>('[data-active="true"]');
 
     if (activeElement) {
       activeElement.scrollIntoView({
         block: 'nearest',
         inline: 'nearest',
       });
     }
   }
 }, [activeValue, menuItemPrefixCls]);

同时,需要在 <li> 元素上添加 data-active 属性以标识高亮项:

 <li
   key={fullPathKey}
   className={classNames(menuItemPrefixCls, {
     [`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
     [`${menuItemPrefixCls}-active`]:
       activeValue === value || activeValue === fullPathKey,
     [`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
     [`${menuItemPrefixCls}-loading`]: isLoading,
   })}
+  data-active={activeValue === value || activeValue === fullPathKey ? 'true' : undefined}
   style={dropdownMenuColumnStyle}
   role="menuitemcheckbox"
   ...

请尽快按照上述建议进行修正,以确保下拉列表在键盘导航时能够安全准确地滚动显示高亮项。

📝 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
React.useEffect(() => {
if (menuRef.current) {
const selector = `.${menuItemPrefixCls}-active`;
const activeElement = menuRef.current.querySelector<HTMLElement>(selector);
if (activeElement) {
activeElement.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
}
}
}, [activeValue, menuItemPrefixCls]);
// In src/OptionList/Column.tsx
// Updated useEffect for scrolling active element into view:
React.useEffect(() => {
if (menuRef.current) {
- const selector = `.${menuItemPrefixCls}-active`;
- const activeElement = menuRef.current.querySelector<HTMLElement>(selector);
+ const activeElement = menuRef.current.querySelector<HTMLElement>('[data-active="true"]');
if (activeElement) {
activeElement.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
}
}
}, [activeValue, menuItemPrefixCls]);
// Updated <li> element with data-active attribute:
<li
key={fullPathKey}
className={classNames(menuItemPrefixCls, {
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
[`${menuItemPrefixCls}-active`]:
activeValue === value || activeValue === fullPathKey,
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
[`${menuItemPrefixCls}-loading`]: isLoading,
})}
+ data-active={activeValue === value || activeValue === fullPathKey ? 'true' : undefined}
style={dropdownMenuColumnStyle}
role="menuitemcheckbox"
...
>
{/* ... rest of the <li> content ... */}
</li>

@jin19980928 jin19980928 requested a review from zombieJ March 13, 2025 08:10
@zombieJ zombieJ merged commit 90f10d1 into react-component:master Mar 13, 2025
11 checks passed
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