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

feat(a11y): enhance keyboard interaction in search mode #592

Merged
merged 25 commits into from
Nov 11, 2024

Conversation

aojunhao123
Copy link
Contributor

@aojunhao123 aojunhao123 commented Nov 3, 2024

🚀 Feature Description

Enhanced keyboard interaction in search mode to improve accessibility.

Changes

  • Allow keyboard users to select first matched non-disabled node by pressing enter
  • Prevent selecting unrelated active node when search value exists
  • Ignore enter key press when no matches found or all matched nodes are disabled

🎯 Motivation

Improve accessibility for keyboard users by providing a more intuitive way to select nodes when searching. Previously, pressing enter would select the active node regardless of search context, which could lead to unexpected selections.

📝 Implementation Details

Modified the keyboard event handling in OptionList.tsx to:

  1. Check for search value existence when enter key is pressed
  2. Find and select the first non-disabled node that matches the search criteria
  3. Maintain existing keyboard navigation behavior when no search is active

🧪 Testing

Added comprehensive test cases in Select.SearchInput.spec.js covering:

  • Selecting first matched node on enter
  • Handling disabled nodes
  • Edge cases with no matches
  • Interaction with search value

📋 Checklist

  • Added/updated tests
  • All tests passing
  • No breaking changes
  • Follows accessibility best practices

📸 Screenshots/GIFs

N/A

📚 Related Issues

Summary by CodeRabbit

  • 新功能
    • 改进了OptionList组件的搜索功能,新增了根据搜索条件识别首个可选节点的方法。
    • 更新了组件的活动键管理逻辑,确保在打开时优先激活匹配节点。
    • 增强了组件的可访问性,确保在打开下拉菜单时自动激活首个可选项。
  • 错误修复
    • 防止选择禁用节点,确保只有可选节点可以被激活。
  • 测试
    • 增强了组件的键盘事件处理测试,确保在输入有效搜索词后按下Enter键能够正确选择节点。
    • 添加了快照测试,以确保组件在不同状态下的渲染输出一致。
    • 扩展了事件处理测试,确保组件在用户交互时表现正常。
  • 杂项
    • 更新了.gitignore文件,以忽略更多的配置文件和锁文件。

Copy link

vercel bot commented Nov 3, 2024

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

Name Status Preview Comments Updated (UTC)
tree-select ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 9, 2024 11:20am

Copy link

coderabbitai bot commented Nov 3, 2024

Walkthrough

此拉取请求对src/OptionList.tsx中的OptionList组件进行了重要修改,新增了getFirstMatchingNode方法以增强搜索功能。组件在打开时会优先激活第一个匹配节点,更新了onInternalSelect函数以阻止选择禁用节点,并引入了onListMouseDown事件处理程序。搜索功能和展开键的管理逻辑也得到了改善。此外,相关测试文件tests/Select.SearchInput.spec.jstests/Select.spec.tsx进行了增强,以验证组件在不同场景下的行为。.gitignore文件也进行了更新,以忽略更多的配置文件和锁文件。

Changes

文件路径 更改摘要
src/OptionList.tsx 新增getFirstMatchingNode方法,更新onInternalSelect逻辑,优化搜索和状态管理。
tests/Select.SearchInput.spec.js 新增键盘事件处理的测试用例,确保组件在键盘交互下的正确行为。
tests/Select.spec.tsx 增强测试用例以验证节点选择逻辑和默认激活节点,移除不再使用的测试,增加快照测试。
.gitignore 更新以忽略.vscodeyarn.lockpackage-lock.json等文件及目录。

Assessment against linked issues

Objective Addressed Explanation
使TreeSelect搜索时更新键盘激活项 (#50852)

Possibly related PRs

Poem

在树下跳跃,寻找匹配的影,
轻轻敲击,键盘发声清脆。
激活节点,搜索不再难,
小兔欢喜,功能全新展现! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • 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 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.

@aojunhao123
Copy link
Contributor Author

@afc163 @zombieJ @yoyo837 三位大佬有空的时候帮忙看看

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

🧹 Outside diff range and nitpick comments (4)
src/OptionList.tsx (3)

162-175: 代码实现清晰,建议加强类型安全性

递归查找第一个匹配且未禁用的节点的实现逻辑正确。建议对 fieldNames.children 的访问添加类型检查,以增强代码的健壮性。

建议如下修改:

-      if (node[fieldNames.children]) {
+      const children = node[fieldNames.children];
+      if (Array.isArray(children) && children.length > 0) {

193-204: 建议提升代码可读性

当前实现正确地解决了搜索模式下的键盘交互问题,但可以通过提取条件判断来提高代码的可维护性。

建议重构如下:

 if (searchValue) {
   const firstMatchNode = getFirstMatchNode(memoTreeData);
-  if (firstMatchNode) {
-    if (firstMatchNode.selectable !== false) {
-      onInternalSelect(null, {
-        node: { key: firstMatchNode[fieldNames.value] },
-        selected: !checkedKeys.includes(firstMatchNode[fieldNames.value]),
-      });
-    }
-  }
+  const canSelectNode = firstMatchNode && firstMatchNode.selectable !== false;
+  if (canSelectNode) {
+    const nodeValue = firstMatchNode[fieldNames.value];
+    onInternalSelect(null, {
+      node: { key: nodeValue },
+      selected: !checkedKeys.includes(nodeValue),
+    });
+  }

226-229: 建议优化 useMemo 的比较逻辑

当前的实现在搜索时正确地禁用了数据加载,但比较函数的逻辑可以更清晰。

建议修改如下:

-    ([preSearchValue], [nextSearchValue, nextExcludeSearchExpandedKeys]) =>
-      preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExcludeSearchExpandedKeys),
+    ([prevSearchValue], [nextSearchValue, expandedKeys]) =>
+      prevSearchValue !== nextSearchValue || (!nextSearchValue && !!expandedKeys),
tests/Select.SearchInput.spec.js (1)

204-230: 测试用例设计合理,建议补充一个场景!

测试用例很好地覆盖了以下场景:

  • 按回车键选择第一个匹配的非禁用节点
  • 验证禁用节点不会被选中

建议补充测试:当存在多个匹配节点时,验证是否始终选择第一个非禁用节点。

// 建议添加以下测试场景
wrapper.search('2');  // 假设存在多个标签包含 '2'
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
expect(onSelect).toHaveBeenCalledWith('第一个非禁用的2', expect.anything());
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between fb1b6bb and e2112e9.

📒 Files selected for processing (2)
  • src/OptionList.tsx (3 hunks)
  • tests/Select.SearchInput.spec.js (2 hunks)
🔇 Additional comments (4)
src/OptionList.tsx (1)

193-204: 请确认测试覆盖率

实现已经很好地解决了键盘交互的问题,建议确认测试用例是否覆盖了以下场景:

  1. 搜索时选择第一个匹配节点
  2. 处理禁用节点的情况
  3. 无匹配结果的情况
✅ Verification successful

Let me gather more information about the test coverage.


测试覆盖情况已完全满足要求

测试用例已经全面覆盖了所有关键场景:

  • 搜索并选择第一个匹配节点的功能
  • 处理禁用节点的情况
  • 无搜索值时的处理
  • 无匹配结果的处理
  • 所有匹配节点都被禁用的情况

测试实现严谨完整,无需额外补充测试用例。

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# 检查测试文件中的相关测试用例
rg -A 5 "describe.*keyboard.*search" tests/
rg -A 5 "it.*press.*enter" tests/

Length of output: 898


Script:

#!/bin/bash
# Check test implementation details
cat tests/Select.SearchInput.spec.js

Length of output: 8752

tests/Select.SearchInput.spec.js (3)

5-5: 导入键码常量模块符合最佳实践!

使用 rc-util/lib/KeyCode 模块来处理键盘事件是一个很好的选择,它提供了统一且可维护的键码常量。


232-274: 测试用例完整覆盖了无选择场景!

这两个测试用例很好地验证了以下关键场景:

  • 无搜索值时按回车键不应选择任何节点
  • 搜索无匹配结果时按回车键不应选择任何节点

测试结构清晰,断言准确。


276-294: 测试用例设计合理,建议验证实现代码!

测试用例很好地覆盖了所有匹配节点都被禁用的场景。为确保实现的完整性,建议验证相关代码。

✅ Verification successful

实现代码已正确处理禁用节点场景,测试用例验证有效!

从源码分析可以看到:

  • getFirstMatchNode 函数在 src/OptionList.tsx 中明确检查了 disabled 状态
  • 搜索匹配时会跳过禁用的节点 (if (filterTreeNode(node) && !node.disabled))
  • 实现逻辑与测试用例的预期行为完全一致
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# 描述:验证 OptionList 组件中处理禁用节点的逻辑实现

# 测试:搜索与禁用节点相关的实现代码
rg -A 5 "disabled.*getFirstMatchNode|getFirstMatchNode.*disabled" 

Length of output: 67


Script:

#!/bin/bash
# 搜索处理选择和禁用节点的相关代码
rg -A 10 "onSelect.*disabled|disabled.*select" src/

# 搜索处理按键事件的相关代码
ast-grep --pattern 'function $FUNC($_) {
  $$$
  disabled
  $$$
}'

# 搜索处理搜索和匹配节点的代码
rg -A 10 "searchValue.*match|getFirstMatch" src/

Length of output: 1688

src/OptionList.tsx Outdated Show resolved Hide resolved
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

🧹 Outside diff range and nitpick comments (2)
tests/Select.SearchInput.spec.js (2)

203-230: 键盘事件测试用例设计合理

测试用例很好地覆盖了以下场景:

  • 按回车键选择第一个匹配的非禁用节点
  • 禁用节点不会被选中

建议添加以下场景的测试:

  • 多层级树结构中的节点选择
  • 异步加载数据时的节点选择

296-317: 搜索时激活首个匹配节点的测试设计合理

测试用例很好地验证了:

  • 搜索时激活第一个匹配的非禁用节点
  • 跳过禁用的节点

建议补充一个测试场景:

  • 验证在搜索过程中,当搜索词变化时,激活状态是否正确更新
// 建议添加的测试用例
it('should update active node when search value changes', () => {
  const wrapper = mount(
    <TreeSelect
      showSearch
      open
      treeData={[
        { value: '1', label: '1' },
        { value: '2', label: '2' },
        { value: '3', label: '3' },
      ]}
    />,
  );

  // 首次搜索
  wrapper.search('1');
  expect(wrapper.find('.rc-tree-select-tree-treenode-active').text()).toBe('1');

  // 更新搜索词
  wrapper.search('2');
  expect(wrapper.find('.rc-tree-select-tree-treenode-active').text()).toBe('2');
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e2112e9 and 0411b7a.

⛔ Files ignored due to path filters (2)
  • tests/__snapshots__/Select.checkable.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Select.spec.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • src/OptionList.tsx (2 hunks)
  • tests/Select.SearchInput.spec.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/OptionList.tsx
🔇 Additional comments (4)
tests/Select.SearchInput.spec.js (4)

5-5: 导入 KeyCode 模块用于键盘事件测试

从 rc-util 库导入 KeyCode 模块来模拟键盘事件,这是一个合理的做法。


232-254: 无搜索值时的回车键处理测试完善

测试用例验证了两个重要场景:

  • 无搜索值时按回车键不选择节点
  • 有搜索值时按回车键选择第一个匹配节点

逻辑严谨,测试覆盖充分。


256-274: 未找到匹配节点时的处理测试完善

测试用例验证了搜索不存在的值时按回车键不会选择任何节点,这对于提升用户体验很重要。


276-294: 所有匹配节点被禁用时的处理测试完善

测试用例验证了当所有匹配的节点都被禁用时,按回车键不会选择任何节点,这种边界情况的处理很重要。

src/OptionList.tsx Outdated Show resolved Hide resolved
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: 2

🧹 Outside diff range and nitpick comments (3)
src/OptionList.tsx (3)

95-109: 实现正确,建议小幅优化

递归查找第一个可选节点的实现逻辑正确。不过可以通过以下方式优化性能:

  1. 使用 Array.prototype.find 来简化代码
  2. 添加类型注解提升代码可维护性

建议如下优化:

-const getFirstSelectableNode = (nodes: EventDataNode<any>): EventDataNode<any> | null => {
+const getFirstSelectableNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
-  for (const node of nodes) {
-    if (node.selectable !== false) {
-      return node;
-    }
-    if (node[fieldNames.children]) {
-      const selectableInChildren = getFirstSelectableNode(node[fieldNames.children]);
-      if (selectableInChildren) {
-        return selectableInChildren;
-      }
-    }
-  }
-  return null;
+  return nodes.find(node => 
+    node.selectable !== false || 
+    (node[fieldNames.children] && getFirstSelectableNode(node[fieldNames.children]))
+  ) || null;
};

179-192: 搜索匹配逻辑正确,建议优化实现

递归查找第一个匹配且未禁用的节点的逻辑正确,但可以参考 getFirstSelectableNode 的优化方案进行改进。

建议如下优化:

-const getFirstMatchNode = (nodes: EventDataNode<any>): EventDataNode<any> | null => {
+const getFirstMatchNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
-  for (const node of nodes) {
-    if (filterTreeNode(node) && !node.disabled) {
-      return node;
-    }
-    if (node[fieldNames.children]) {
-      const matchInChildren = getFirstMatchNode(node[fieldNames.children]);
-      if (matchInChildren) {
-        return matchInChildren;
-      }
-    }
-  }
-  return null;
+  return nodes.find(node => 
+    (filterTreeNode(node) && !node.disabled) || 
+    (node[fieldNames.children] && getFirstMatchNode(node[fieldNames.children]))
+  ) || null;
};

247-250: 建议优化 loadDataFun 的依赖判断逻辑

当前的比较函数逻辑较难理解,建议拆分条件使代码更清晰。

建议如下优化:

const loadDataFun = useMemo(
  () => (searchValue ? null : (loadData as any)),
  [searchValue, treeExpandedKeys || expandedKeys],
-  ([preSearchValue], [nextSearchValue, nextExcludeSearchExpandedKeys]) =>
-    preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExcludeSearchExpandedKeys),
+  ([prevSearchValue], [nextSearchValue, nextExpandedKeys]) => {
+    const hasSearchValueChanged = prevSearchValue !== nextSearchValue;
+    const shouldUpdateOnExpandedKeys = !nextSearchValue && !!nextExpandedKeys;
+    return hasSearchValueChanged || shouldUpdateOnExpandedKeys;
+  },
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 481c983 and a9efbc7.

⛔ Files ignored due to path filters (2)
  • tests/__snapshots__/Select.checkable.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Select.spec.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • src/OptionList.tsx (4 hunks)
  • tests/Select.SearchInput.spec.js (2 hunks)
  • tests/Select.spec.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Select.SearchInput.spec.js
🔇 Additional comments (6)
src/OptionList.tsx (4)

113-124: 下拉框打开时的焦点处理逻辑合理

当下拉框打开时:

  • 单选模式下,正确滚动到已选中项
  • 多选模式下,正确激活第一个可选项
  • 使用 getFirstSelectableNode 确保激活项是可选的

134-148: 选择处理逻辑完善且符合无障碍标准

  • 正确处理了可选和禁用状态
  • 单选模式下自动关闭下拉框
  • 支持多选模式

194-207: 搜索时的状态处理逻辑完善

  • 搜索时自动展开所有节点,提升用户体验
  • 正确设置第一个匹配项为活动项
  • 无匹配项时正确清空活动项

226-227: 键盘交互逻辑符合无障碍要求

回车键处理逻辑正确:

  • 检查节点是否可选
  • 检查节点是否禁用
  • 仅在节点可用时触发选择
tests/Select.spec.tsx (2)

441-447: 代码逻辑正确

在键盘交互测试中,matchValue 函数正确地验证了选中的值,符合预期的选中顺序。


470-475: 验证不可选择的节点不会触发 onChange 事件

测试成功验证了当尝试通过键盘操作选择不可选择的节点时,onChange 事件不会被触发。而当选择可选择的父节点时,onChange 正确地被调用。

Comment on lines 612 to 626
// it('should not add new tag when key enter is pressed if nothing is active', () => {
// const onSelect = jest.fn();

// const wrapper = mount(
// <TreeSelect open treeDefaultExpandAll multiple onSelect={onSelect}>
// <TreeNode value="parent 1-0" title="parent 1-0">
// <TreeNode value="leaf1" title="my leaf" disabled />
// <TreeNode value="leaf2" title="your leaf" disabled />
// </TreeNode>
// </TreeSelect>,
// );

// wrapper.find('input').first().simulate('keydown', { which: KeyCode.ENTER });
// expect(onSelect).not.toHaveBeenCalled();
// });
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

避免注释掉测试用例

发现测试用例 should not add new tag when key enter is pressed if nothing is active 被注释掉了。这可能会导致相关功能缺乏测试覆盖,潜在的问题无法被及时发现。

建议重新启用并完善该测试用例,或者如果该测试不再适用,提供详细的注释说明原因,并考虑安全地删除此冗余代码。

Comment on lines 538 to 555

it('should active first option when dropdown is opened', () => {
const treeData = [
{ key: '0', value: '0', title: '0 label', disabled: true },
{ key: '1', value: '1', title: '1 label' },
{ key: '2', value: '2', title: '2 label' },
];

const wrapper = mount(<TreeSelect treeData={treeData} />);

expect(wrapper.find('.rc-tree-select-tree-treenode-active')).toHaveLength(0);

wrapper.openSelect();

const activeNode = wrapper.find('.rc-tree-select-tree-treenode-active');
expect(activeNode).toHaveLength(1);
expect(activeNode.text()).toBe('0 label');
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

默认激活的节点是禁用的节点

在测试 should active first option when dropdown is opened 中,下拉菜单打开时默认激活了第一个禁用的节点 '0 label'。根据用户体验和可访问性的最佳实践,应该跳过禁用的节点,激活第一个可选择的未禁用节点 '1 label'

建议修改测试用例,使其在打开下拉菜单时,默认激活第一个未禁用的节点。可以修改如下代码:

- expect(activeNode.text()).toBe('0 label');
+ expect(activeNode.text()).toBe('1 label');

同时,需要检查组件逻辑,确保在实际使用中,下拉菜单打开时不会默认聚焦到禁用的节点。

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

src/OptionList.tsx Outdated Show resolved Hide resolved
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

🧹 Outside diff range and nitpick comments (4)
src/OptionList.tsx (4)

244-247: 优化 loadDataFun 的依赖项

useMemo 的依赖数组中使用 treeExpandedKeys || expandedKeys 可能会导致不必要的重新计算。建议将条件判断移到回调函数中。

  const loadDataFun = useMemo(
    () => (searchValue ? null : (loadData as any)),
-   [searchValue, treeExpandedKeys || expandedKeys],
+   [searchValue, treeExpandedKeys, expandedKeys],
    ([preSearchValue], [nextSearchValue, nextExpandedKeys]) =>
      preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExpandedKeys),
  );

184-204: 优化 Active Key Effect 的性能

当前实现在每次 opensearchValue 变化时都会重新计算 firstSelectableNode。建议将 getFirstSelectableNode 的结果进行缓存。

+ const firstSelectableNode = React.useMemo(
+   () => getFirstSelectableNode(memoTreeData),
+   [memoTreeData]
+ );

  React.useEffect(() => {
    if (searchValue) {
      const firstMatchNode = getFirstMatchNode(memoTreeData);
      setActiveKey(firstMatchNode ? firstMatchNode[fieldNames.value] : null);
      return;
    }

    if (open) {
      if (!multiple && checkedKeys.length) {
        setActiveKey(checkedKeys[0]);
      } else {
-       const firstSelectableNode = getFirstSelectableNode(memoTreeData);
        if (firstSelectableNode) {
          setActiveKey(firstSelectableNode[fieldNames.value]);
        }
      }
      return;
    }
    setActiveKey(null);
  }, [open, searchValue]);

223-224: 优化 ENTER 键处理逻辑的可读性

建议将选择逻辑抽取为一个独立的函数,使代码更易理解和维护。

+ const isNodeSelectable = (node: EventDataNode<any>) => 
+   node?.selectable !== false && !node?.disabled;

  case KeyCode.ENTER: {
    if (activeEntity) {
-     const { selectable, value, disabled } = activeEntity?.node || {};
-     if (selectable !== false && !disabled) {
+     if (isNodeSelectable(activeEntity.node)) {
        onInternalSelect(null, {
          node: { key: activeKey },
-         selected: !checkedKeys.includes(value),
+         selected: !checkedKeys.includes(activeEntity.node.value),
        });
      }
    }
    break;
  }

152-157: 优化搜索时的展开逻辑

当前实现在搜索时展开所有节点,这对于大型树结构可能会影响性能。建议只展开包含匹配节点的路径。

  React.useEffect(() => {
    if (searchValue) {
-     setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
+     // 建议实现一个函数,只返回包含匹配节点的路径的键
+     setSearchExpandedKeys(getMatchedNodePaths(treeData, fieldNames, searchValue));
    }
  }, [searchValue]);

您需要我帮助实现 getMatchedNodePaths 函数吗?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a9efbc7 and 644bf9d.

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

Comment on lines 159 to 183
// ========================== Get First Selectable Node ==========================
const getFirstMatchingNode = (
nodes: EventDataNode<any>,
predicate: (node: EventDataNode<any>) => boolean,
): EventDataNode<any> | null => {
for (const node of nodes) {
if (predicate(node)) {
return node;
}
if (node[fieldNames.children]) {
const matchInChildren = getFirstMatchingNode(node[fieldNames.children], predicate);
if (matchInChildren) {
return matchInChildren;
}
}
}
return null;
};

const onInternalSelect = (__: Key[], info: TreeEventInfo) => {
const { node } = info;
const getFirstSelectableNode = (nodes: EventDataNode<any>): EventDataNode<any> | null =>
getFirstMatchingNode(nodes, node => node.selectable !== false && !node.disabled);

if (checkable && isCheckDisabled(node)) {
const getFirstMatchNode = (nodes: EventDataNode<any>): EventDataNode<any> | null =>
getFirstMatchingNode(nodes, node => filterTreeNode(node) && !node.disabled);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

修复 getFirstMatchingNode 的类型定义

nodes 参数应该是数组类型,但当前的类型定义是单个节点。

  const getFirstMatchingNode = (
-   nodes: EventDataNode<any>,
+   nodes: EventDataNode<any>[],
    predicate: (node: EventDataNode<any>) => boolean,
  ): EventDataNode<any> | null => {
📝 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
// ========================== Get First Selectable Node ==========================
const getFirstMatchingNode = (
nodes: EventDataNode<any>,
predicate: (node: EventDataNode<any>) => boolean,
): EventDataNode<any> | null => {
for (const node of nodes) {
if (predicate(node)) {
return node;
}
if (node[fieldNames.children]) {
const matchInChildren = getFirstMatchingNode(node[fieldNames.children], predicate);
if (matchInChildren) {
return matchInChildren;
}
}
}
return null;
};
const onInternalSelect = (__: Key[], info: TreeEventInfo) => {
const { node } = info;
const getFirstSelectableNode = (nodes: EventDataNode<any>): EventDataNode<any> | null =>
getFirstMatchingNode(nodes, node => node.selectable !== false && !node.disabled);
if (checkable && isCheckDisabled(node)) {
const getFirstMatchNode = (nodes: EventDataNode<any>): EventDataNode<any> | null =>
getFirstMatchingNode(nodes, node => filterTreeNode(node) && !node.disabled);
// ========================== Get First Selectable Node ==========================
const getFirstMatchingNode = (
nodes: EventDataNode<any>[],
predicate: (node: EventDataNode<any>) => boolean,
): EventDataNode<any> | null => {
for (const node of nodes) {
if (predicate(node)) {
return node;
}
if (node[fieldNames.children]) {
const matchInChildren = getFirstMatchingNode(node[fieldNames.children], predicate);
if (matchInChildren) {
return matchInChildren;
}
}
}
return null;
};
const getFirstSelectableNode = (nodes: EventDataNode<any>): EventDataNode<any> | null =>
getFirstMatchingNode(nodes, node => node.selectable !== false && !node.disabled);
const getFirstMatchNode = (nodes: EventDataNode<any>): EventDataNode<any> | null =>
getFirstMatchingNode(nodes, node => filterTreeNode(node) && !node.disabled);

// ========================== Active Key Effect ==========================
React.useEffect(() => {
if (searchValue) {
const firstMatchNode = getFirstMatchNode(memoTreeData);
Copy link
Member

Choose a reason for hiding this comment

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

有没有 searchValue 你的 matchNode 逻辑应该都是一样的。要么都是 selectable 的,要么都是非 disabled 的。可以考虑直接从 virtualList 里获取,这样也不需要递归,并且给出来的 TreeNode 本来就是过滤完的。

src/OptionList.tsx Outdated Show resolved Hide resolved
src/OptionList.tsx Outdated Show resolved Hide resolved
src/OptionList.tsx Outdated Show resolved Hide resolved
src/OptionList.tsx Outdated Show resolved Hide resolved
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: 2

🧹 Outside diff range and nitpick comments (2)
src/OptionList.tsx (2)

105-117: 建议优化选择逻辑的实现

当前实现正确但可以更简洁。建议合并条件判断,减少代码嵌套。

-  const onInternalSelect = (__: Key[], info: TreeEventInfo) => {
-    const { node } = info;
-
-    if (checkable && isCheckDisabled(node)) {
-      return;
-    }
-
-    onSelect(node.key, {
-      selected: !checkedKeys.includes(node.key),
-    });
-
-    if (!multiple) {
-      toggleOpen(false);
-    }
-  };
+  const onInternalSelect = (__: Key[], { node }: TreeEventInfo) => {
+    if (checkable && isCheckDisabled(node)) return;
+    
+    onSelect(node.key, { selected: !checkedKeys.includes(node.key) });
+    !multiple && toggleOpen(false);
+  };

239-242: 建议简化 useMemo 的条件判断

当前的条件判断逻辑较复杂,可以简化为更直观的形式。

  const loadDataFun = useMemo(
    () => (searchValue ? null : (loadData as any)),
    [searchValue, treeExpandedKeys || expandedKeys],
-   ([preSearchValue], [nextSearchValue, nextExcludeSearchExpandedKeys]) =>
-     preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExcludeSearchExpandedKeys),
+   ([prevSearch], [nextSearch, nextKeys]) => prevSearch !== nextSearch || !!nextKeys,
  );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 5d29cc0 and 4640c4d.

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

101-103: 事件处理实现正确!

preventDefault 的使用可以防止失去焦点,这对于键盘交互很重要。


Line range hint 218-223: 键盘交互逻辑完善!

新增的选择条件检查(selectable !== false && !disabled)确保了更好的可访问性体验。


158-172: 🛠️ Refactor suggestion

建议优化节点匹配逻辑

根据之前的讨论,OptionList 接收的 treeData 已经是过滤后的节点,不需要重复过滤。建议简化实现:

- const getFirstMatchingNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
-   for (const node of nodes) {
-     if (!node.disabled && node.selectable !== false) {
-       return node;
-     }
-
-     if (node[fieldNames.children]) {
-       const matchInChildren = getFirstMatchingNode(node[fieldNames.children]);
-       if (matchInChildren) {
-         return matchInChildren;
-       }
-     }
-   }
-   return null;
- };
+ const getFirstMatchingNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => 
+   nodes.find(node => !node.disabled && node.selectable !== false) || null;

Likely invalid or redundant comment.

Comment on lines +150 to +155
React.useEffect(() => {
if (searchValue) {
setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

需要补充 useEffect 的依赖项

当前的依赖数组缺少 treeDatafieldNames,这可能导致搜索结果不准确。

  React.useEffect(() => {
    if (searchValue) {
      setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
    }
-  }, [searchValue]);
+  }, [searchValue, treeData, fieldNames]);
📝 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 (searchValue) {
setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);
React.useEffect(() => {
if (searchValue) {
setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue, treeData, fieldNames]);

Comment on lines 178 to 199
React.useEffect(() => {
if (!open) {
return;
}

onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
});
// // Prioritize activating the searched node
if (searchValue) {
const firstNode = getFirstMatchingNode(memoTreeData);
setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
return;
}

if (!multiple) {
toggleOpen(false);
// If no search value, activate the first checked node
if (!multiple && checkedKeys.length) {
setActiveKey(checkedKeys[0]);
return;
}
};

// If no search value and no checked nodes, activate the first node
const firstNode = getFirstMatchingNode(memoTreeData);
setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
}, [open, searchValue]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

需要完善 useEffect 依赖项

当前实现存在以下问题:

  1. 缺少重要的依赖项:multiplecheckedKeysmemoTreeDatafieldNames
  2. 根据之前的讨论,关闭时不需要设置 activeKeynull,这会影响动画效果
  React.useEffect(() => {
-   if (!open) {
-     return;
-   }
+   if (!open) return;

    if (searchValue) {
      const firstNode = getFirstMatchingNode(memoTreeData);
      setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
      return;
    }

    if (!multiple && checkedKeys.length) {
      setActiveKey(checkedKeys[0]);
      return;
    }

    const firstNode = getFirstMatchingNode(memoTreeData);
    setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
-  }, [open, searchValue]);
+  }, [open, searchValue, multiple, checkedKeys, memoTreeData, fieldNames]);
📝 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 (!open) {
return;
}
onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
});
// // Prioritize activating the searched node
if (searchValue) {
const firstNode = getFirstMatchingNode(memoTreeData);
setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
return;
}
if (!multiple) {
toggleOpen(false);
// If no search value, activate the first checked node
if (!multiple && checkedKeys.length) {
setActiveKey(checkedKeys[0]);
return;
}
};
// If no search value and no checked nodes, activate the first node
const firstNode = getFirstMatchingNode(memoTreeData);
setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
}, [open, searchValue]);
React.useEffect(() => {
if (!open) return;
// // Prioritize activating the searched node
if (searchValue) {
const firstNode = getFirstMatchingNode(memoTreeData);
setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
return;
}
// If no search value, activate the first checked node
if (!multiple && checkedKeys.length) {
setActiveKey(checkedKeys[0]);
return;
}
// If no search value and no checked nodes, activate the first node
const firstNode = getFirstMatchingNode(memoTreeData);
setActiveKey(firstNode ? firstNode[fieldNames.value] : null);
}, [open, searchValue, multiple, checkedKeys, memoTreeData, fieldNames]);

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

🧹 Outside diff range and nitpick comments (1)
src/OptionList.tsx (1)

105-117: 选择逻辑处理合理,建议小幅优化

选择逻辑的实现考虑了禁用状态和多选模式,处理得当。不过建议将选择和关闭下拉框的逻辑分开处理,以提高代码的可维护性。

 const onInternalSelect = (__: Key[], info: TreeEventInfo) => {
   const { node } = info;

   if (checkable && isCheckDisabled(node)) {
     return;
   }

   onSelect(node.key, {
     selected: !checkedKeys.includes(node.key),
   });

-  if (!multiple) {
-    toggleOpen(false);
-  }
+  // 将关闭逻辑提取到单独的副作用中
+  React.useEffect(() => {
+    if (!multiple && checkedKeys.length) {
+      toggleOpen(false);
+    }
+  }, [checkedKeys, multiple]);
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4640c4d and 229f4f7.

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

101-103: 事件处理实现正确!

preventDefault的使用可以防止不必要的文本选择,这对于下拉列表的用户体验很重要。


222-223: 键盘交互处理得当!

选择逻辑正确考虑了节点的可选状态和禁用状态,增强了键盘访问的可用性。


243-246: 数据加载逻辑优化合理!

loadDataFun的记忆化处理考虑了搜索状态的变化,比较函数的实现准确。


141-155: ⚠️ Potential issue

搜索功能需要完善依赖项

搜索功能的实现基本正确,但useEffect的依赖数组缺少了重要的依赖项:

 React.useEffect(() => {
   if (searchValue) {
     setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
   }
-}, [searchValue]);
+}, [searchValue, treeData, fieldNames]);

Likely invalid or redundant comment.


157-172: 🛠️ Refactor suggestion

建议优化节点查找逻辑

当前的递归实现虽然正确,但考虑到性能和已有的virtualList功能,建议重构此方法:

-const getFirstMatchingNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
-  for (const node of nodes) {
-    if (!node.disabled && node.selectable !== false) {
-      return node;
-    }
-
-    if (node[fieldNames.children]) {
-      const matchInChildren = getFirstMatchingNode(node[fieldNames.children]);
-      if (matchInChildren) {
-        return matchInChildren;
-      }
-    }
-  }
-  return null;
-};
+const getFirstMatchingNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
+  // 直接从virtualList中获取第一个可用节点
+  return nodes.find(node => !node.disabled && node.selectable !== false) || null;
+};

Likely invalid or redundant comment.

Comment on lines 174 to 203
// ========================== Active ==========================
const [activeKey, setActiveKey] = React.useState<Key>(null);
const activeEntity = keyEntities[activeKey as SafeKey];

React.useEffect(() => {
if (!open) {
return;
}
let nextActiveKey = null;

onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
});
const getFirstNode = () => {
const firstNode = getFirstMatchingNode(memoTreeData);
return firstNode ? firstNode[fieldNames.value] : null;
};

if (!multiple) {
toggleOpen(false);
// search mode active first node
if (searchValue) {
nextActiveKey = getFirstNode();
}
};
// single mode active first checked node
else if (!multiple && checkedKeys.length) {
nextActiveKey = checkedKeys[0];
}
// default active first node
else {
nextActiveKey = getFirstNode();
}

setActiveKey(nextActiveKey);
}, [open, searchValue]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

活动键管理需要补充依赖项

活动键管理的逻辑符合需求,但useEffect缺少了关键依赖:

 React.useEffect(() => {
   if (!open) {
     return;
   }
   // ...
   setActiveKey(nextActiveKey);
-}, [open, searchValue]);
+}, [open, searchValue, multiple, checkedKeys, memoTreeData, fieldNames]);
📝 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
// ========================== Active ==========================
const [activeKey, setActiveKey] = React.useState<Key>(null);
const activeEntity = keyEntities[activeKey as SafeKey];
React.useEffect(() => {
if (!open) {
return;
}
let nextActiveKey = null;
onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
});
const getFirstNode = () => {
const firstNode = getFirstMatchingNode(memoTreeData);
return firstNode ? firstNode[fieldNames.value] : null;
};
if (!multiple) {
toggleOpen(false);
// search mode active first node
if (searchValue) {
nextActiveKey = getFirstNode();
}
};
// single mode active first checked node
else if (!multiple && checkedKeys.length) {
nextActiveKey = checkedKeys[0];
}
// default active first node
else {
nextActiveKey = getFirstNode();
}
setActiveKey(nextActiveKey);
}, [open, searchValue]);
// ========================== Active ==========================
const [activeKey, setActiveKey] = React.useState<Key>(null);
const activeEntity = keyEntities[activeKey as SafeKey];
React.useEffect(() => {
if (!open) {
return;
}
let nextActiveKey = null;
const getFirstNode = () => {
const firstNode = getFirstMatchingNode(memoTreeData);
return firstNode ? firstNode[fieldNames.value] : null;
};
// search mode active first node
if (searchValue) {
nextActiveKey = getFirstNode();
}
// single mode active first checked node
else if (!multiple && checkedKeys.length) {
nextActiveKey = checkedKeys[0];
}
// default active first node
else {
nextActiveKey = getFirstNode();
}
setActiveKey(nextActiveKey);
}, [open, searchValue, multiple, checkedKeys, memoTreeData, fieldNames]);

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

🧹 Outside diff range and nitpick comments (2)
src/OptionList.tsx (2)

189-193: 建议简化活动键设置逻辑

当前的条件判断结构可以更简洁。根据之前的讨论,可以先检查单选模式下的选中项,其他情况统一处理。

建议修改如下:

- if (!multiple && checkedKeys.length && !searchValue) {
-   nextActiveKey = checkedKeys[0];
- } else {
-   nextActiveKey = getFirstNode();
- }
+ nextActiveKey = (!multiple && checkedKeys.length && !searchValue)
+   ? checkedKeys[0]
+   : getFirstNode();

237-240: 建议优化 memoization 逻辑的可读性

当前的实现逻辑较为复杂,可以通过拆分条件提高可读性。

建议修改如下:

  useMemo(
    () => (searchValue ? null : (loadData as any)),
    [searchValue, treeExpandedKeys || expandedKeys],
-   ([preSearchValue], [nextSearchValue, nextExcludeSearchExpandedKeys]) =>
-     preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExcludeSearchExpandedKeys),
+   ([preSearchValue], [nextSearchValue, nextExpandedKeys]) => {
+     const hasSearchValueChanged = preSearchValue !== nextSearchValue;
+     const hasValidNextState = !!(nextSearchValue || nextExpandedKeys);
+     return hasSearchValueChanged && hasValidNextState;
+   },
  );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 229f4f7 and 9c7ad27.

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

101-103: 事件处理符合预期

preventDefault的使用正确,可以防止失去焦点,有助于维持键盘导航状态。


105-117: 选择逻辑处理完善

选择处理逻辑考虑周全:

  • 正确处理了禁用状态的检查
  • 适当处理了单选模式下的下拉框关闭
  • 维护了选中状态的更新

216-217: 键盘交互处理得当

Enter键的处理逻辑完善:

  • 正确检查了可选择状态
  • 适当处理了禁用状态
  • 符合无障碍访问要求

150-155: ⚠️ Potential issue

需要补充 useEffect 依赖项

当前的依赖数组缺少了 treeDatafieldNames,这可能导致在这些值发生变化时不会触发预期的更新。

建议修改如下:

  React.useEffect(() => {
    if (searchValue) {
      setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
    }
-  }, [searchValue]);
+  }, [searchValue, treeData, fieldNames]);

Likely invalid or redundant comment.

Comment on lines +164 to +165
if (node[fieldNames.children]) {
const matchInChildren = getFirstMatchingNode(node[fieldNames.children]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

需要增加空值检查

访问 node[fieldNames.children] 时缺少必要的空值检查,可能导致运行时错误。

建议修改如下:

- if (node[fieldNames.children]) {
+ if (node?.[fieldNames.children]?.length) {
📝 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 (node[fieldNames.children]) {
const matchInChildren = getFirstMatchingNode(node[fieldNames.children]);
if (node?.[fieldNames.children]?.length) {
const matchInChildren = getFirstMatchingNode(node[fieldNames.children]);

describe('keyboard events', () => {
it('should select first matched node when press enter', () => {
const onSelect = jest.fn();
const wrapper = mount(
Copy link
Member

Choose a reason for hiding this comment

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

用 testing lib

Copy link

codecov bot commented Nov 7, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (fb1b6bb) to head (97bde6c).
Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master      #592   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           16        16           
  Lines          520       545   +25     
  Branches       151       163   +12     
=========================================
+ Hits           520       545   +25     

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

@afc163
Copy link
Member

afc163 commented Nov 7, 2024

覆盖率掉了

@aojunhao123
Copy link
Contributor Author

覆盖率掉了

我改一下

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6f5a84c and 97bde6c.

📒 Files selected for processing (2)
  • src/OptionList.tsx (4 hunks)
  • tests/Select.SearchInput.spec.js (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Select.SearchInput.spec.js
🔇 Additional comments (6)
src/OptionList.tsx (6)

100-103: 事件处理实现正确!

onListMouseDown 的实现简洁且有效,正确阻止了默认行为。


105-117: 选择处理逻辑完善!

选择处理器正确实现了:

  • 禁用节点的检查
  • 单选模式下的下拉框关闭

224-225: 键盘事件处理完善!

Enter 键处理逻辑正确实现了:

  • 可选状态检查
  • 禁用状态检查

245-248: 数据加载函数优化得当!

loadDataFun 的记忆化逻辑正确处理了:

  • 搜索状态变化
  • 展开键变化

172-173: ⚠️ Potential issue

需要添加空值检查!

访问 node[fieldNames.children] 时没有进行必要的空值检查,可能导致运行时错误。建议修改如下:

- if (node[fieldNames.children]) {
+ if (node?.[fieldNames.children]?.length) {

Likely invalid or redundant comment.


150-155: ⚠️ Potential issue

需要补充 useEffect 依赖项!

当前实现缺少了 treeDatafieldNames 作为依赖项,这可能导致搜索结果不准确。建议修改如下:

  React.useEffect(() => {
    if (searchValue) {
      setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
    }
-  }, [searchValue]);
+  }, [searchValue, treeData, fieldNames]);

Likely invalid or redundant comment.

Comment on lines +186 to +205
React.useEffect(() => {
if (!open) {
return;
}
let nextActiveKey = null;

onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
});
const getFirstNode = () => {
const firstNode = getFirstMatchingNode(memoTreeData);
return firstNode ? firstNode[fieldNames.value] : null;
};

if (!multiple) {
toggleOpen(false);
// single mode active first checked node
if (!multiple && checkedKeys.length && !searchValue) {
nextActiveKey = checkedKeys[0];
} else {
nextActiveKey = getFirstNode();
}
};

setActiveKey(nextActiveKey);
}, [open, searchValue]);
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. 逻辑可以简化:先检查单选模式下的选中键,其他情况都使用 getFirstNode
  2. 需要补充依赖项
  React.useEffect(() => {
    if (!open) {
      return;
    }

    let nextActiveKey = null;
-    if (!multiple && checkedKeys.length && !searchValue) {
+    if (!multiple && checkedKeys.length) {
      nextActiveKey = checkedKeys[0];
    } else {
      nextActiveKey = getFirstNode();
    }

    setActiveKey(nextActiveKey);
-  }, [open, searchValue]);
+  }, [open, searchValue, multiple, checkedKeys, memoTreeData, fieldNames]);
📝 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 (!open) {
return;
}
let nextActiveKey = null;
onSelect(node.key, {
selected: !checkedKeys.includes(node.key),
});
const getFirstNode = () => {
const firstNode = getFirstMatchingNode(memoTreeData);
return firstNode ? firstNode[fieldNames.value] : null;
};
if (!multiple) {
toggleOpen(false);
// single mode active first checked node
if (!multiple && checkedKeys.length && !searchValue) {
nextActiveKey = checkedKeys[0];
} else {
nextActiveKey = getFirstNode();
}
};
setActiveKey(nextActiveKey);
}, [open, searchValue]);
React.useEffect(() => {
if (!open) {
return;
}
let nextActiveKey = null;
const getFirstNode = () => {
const firstNode = getFirstMatchingNode(memoTreeData);
return firstNode ? firstNode[fieldNames.value] : null;
};
// single mode active first checked node
if (!multiple && checkedKeys.length) {
nextActiveKey = checkedKeys[0];
} else {
nextActiveKey = getFirstNode();
}
setActiveKey(nextActiveKey);
}, [open, searchValue, multiple, checkedKeys, memoTreeData, fieldNames]);

@zombieJ zombieJ merged commit a16f0f0 into react-component:master Nov 11, 2024
9 checks passed
@aojunhao123 aojunhao123 deleted the feat-a11y branch November 11, 2024 12:06
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.

TreeSelect search doesn't update keyboard-active item
3 participants