Skip to content

Conversation

@zombieJ
Copy link
Member

@zombieJ zombieJ commented Oct 15, 2025

Summary by CodeRabbit

  • 新功能
    • 可通过组件化入口自定义 Select 的根容器与输入渲染,新增占位符与多选/单选内容渲染组件
    • 支持更灵活的清除配置(allowClear 可传对象并自定义图标);新增 affix/前后缀渲染点位
  • 样式
    • 引入样式补丁,优化多选排版、输入宽度、截断与占位显示
  • 变更
    • suffix 替代旧的 suffixIcon(含兼容提示);按模式自动启用搜索,示例已更新 showSearch
  • 修复
    • 上/左/右方向键不再触发展开,打开/关闭时序更稳定

@vercel
Copy link

vercel bot commented Oct 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
select Ready Ready Preview Comment Oct 16, 2025 3:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

将旧的 Selector 实现替换为可组合的 SelectInput 子系统,重构 BaseSelect 渲染与打开控制,新增/调整多个 hooks 与样式,并同步更新示例与大量测试以匹配新的 DOM 和 API 命名。

Changes

Cohort / File(s) Summary
样式与资源
assets/index.less, assets/patch.less
在入口样式中引入 patch.less;新增 select 相关 LESS(容器、内容、前/后缀、清除、单/多选变体,使用可配置前缀类)。
示例文档
docs/examples/multiple-with-maxCount.tsx, docs/examples/multiple.tsx
示例调整:为 multiple+maxCount 增加 showSearch;示例中将 suffixIcon 改为 suffix
测试配置与环境
jest.config.js, tests/setup.ts
新增 Jest 配置文件;在测试环境中提供 MessageChannel 全局模拟。
新的 SelectInput 子系统
src/SelectInput/*
src/SelectInput/index.tsx, Input.tsx, context.ts, Affix.tsx, Content/*
新增可组合的 SelectInput:上下文、可定制 Input、Affix、Single/Multiple 内容与 Placeholder,暴露 SelectInputRef/Props 并前向 ref,实现新的渲染路径。
BaseSelect 重构
src/BaseSelect/index.tsx
引入 components 配置与组件化渲染路径,合并渲染流与打开/聚焦控制;API 调整(allowClear 接受对象、新增 componentssuffix 替代/兼容 suffixIcon、扩展 semantic 名称等)。
旧 Selector 删除
src/Selector/*
src/Selector/Input.tsx, MultipleSelector.tsx, SingleSelector.tsx, index.tsx
删除旧的 Selector 实现及其导出,相关职责迁移至新的 SelectInput 子系统。
Hooks 与控制流
src/hooks/*
useOpen.ts, useComponents.ts, useAllowClear.tsx, useBaseProps.ts, useSearchConfig.ts, useSelectTriggerControl.ts
新增/调整 hooks:宏任务延迟的 open 控制、组件注入、allowClear 归一化、BaseProps 增加 role、useSearchConfig 接受 mode、触发器全局点击处理支持 SVGElement 等。
触发器与 Select 顶层
src/SelectTrigger.tsx, src/Select.tsx
SelectTrigger 增加 onPopupMouseDownSelect 调整 useSearchConfig 的调用签名(新增 mode 参数)。
工具与注释
src/utils/keyUtil.ts, src/TransBtn.tsx
更新按键开关判定(将上/左/右箭头视作非开键);为 TransBtn 添加说明性 JSDoc 注释。
移除旧工具/钩子
src/hooks/useDelayReset.ts, src/hooks/useLayoutEffect.ts
删除旧的 useDelayReset 与 useLayoutEffect 文件及其导出。
测试与测试工具适配
tests/*, tests/shared/*, tests/utils/common.ts
大量测试更新:类名/选择器替换(如 .rc-select.rc-select-suffix.rc-select-clear.rc-select-placeholder 等)、引入假定计时器与 act 包裹、断言方式调整以匹配新 DOM/API。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Select as Select (外层)
  participant BaseSelect as BaseSelect
  participant SI as SelectInput
  participant Trigger as SelectTrigger
  participant Hook as useOpen

  User->>SI: click / keydown / input
  SI->>Hook: toggleOpen(nextOpen?)
  Hook-->>SI: mergedOpen
  SI->>BaseSelect: onSearch / onSelectorRemove / onClear 等回调
  BaseSelect->>Trigger: 更新弹层 (open=mergedOpen)
  Trigger-->>User: 展示/隐藏下拉
Loading
sequenceDiagram
  autonumber
  actor User
  participant SI as SelectInput
  participant Ctx as SelectInputContext
  participant Content as Single/MultipleContent
  participant Input as Input

  Note over SI,Content: 组件化渲染路径(上下文驱动)
  SI->>Ctx: 提供上下文(prefixCls, mode, handlers)
  SI->>Content: 渲染 Single/MultipleContent
  Content->>Input: 渲染受控输入(事件/宽度同步)
  Input-->>Content: onChange/onKeyDown/onBlur 事件
  Content-->>SI: 搜索值/项移除/提交 事件
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested reviewers

  • afc163

Poem

我是只码田的小兔子,代码行里蹦又跳;
旧的 Selector 轻放下,SelectInput 铺新桥。
前后缀与清除舞,样式与测试都跟好;
宏任务托起弹层梦,提交里藏一把萝卜饼。 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive 该标题“refactor: Semantic structure re-layout”过于宽泛,未明确指出所重构的组件或功能范围,无法让阅读记录的同事快速理解此次变更的主要内容,因此信息量不足且语义模糊。 建议将标题改为更具描述性的形式,例如“refactor: 重构 BaseSelect 和 SelectInput 的语义化结构”或“refactor: 将 Selector 实现迁移至组件驱动渲染路径”,以便更清晰地传达主要改动。
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch r-layout

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @zombieJ, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant internal restructuring of the Select component, moving from a legacy Selector implementation to a more modern and modular SelectInput architecture. The refactoring centralizes input and display logic, enhances state management with new custom hooks, and provides clearer separation of concerns for improved maintainability and future extensibility. It also includes updates to styling, accessibility, and a thorough overhaul of the test suite to accommodate these changes.

Highlights

  • Core Component Refactoring: The entire internal structure of the Select component has been refactored, replacing the monolithic Selector component with a new, modular SelectInput component and its sub-parts (Affix, Content, Input, Placeholder). This change aims to improve maintainability and flexibility.
  • Semantic Prop Changes: The suffixIcon prop has been renamed to suffix for better semantic clarity. Additionally, getInputElement and getRawInputElement are deprecated in favor of a new components prop, allowing for more granular customization of the root and input elements.
  • State Management Enhancements: New custom hooks, useOpen and useComponents, have been introduced to centralize and improve the management of the dropdown's open state (including SSR compatibility and macro-task scheduling for closing) and component customization respectively.
  • Styling and Accessibility Updates: A new patch.less file defines core styles for the refactored components, aligning with the new semantic structure. Accessibility attributes and keyboard navigation logic have been updated, including preventing arrow keys from opening the dropdown during input navigation.
  • Comprehensive Test Updates: Numerous test files have been updated to reflect the new component structure, class names, and event handling. This includes consistent use of jest.useFakeTimers and act for asynchronous operations, ensuring the stability and correctness of the refactored codebase.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and impressive refactoring of the Select component's internal structure. It replaces the monolithic Selector component with a more modular and semantic SelectInput component, which is a great improvement for maintainability and extensibility. The introduction of custom hooks like useOpen and useComponents cleans up the logic for state management and component customization significantly. The API changes, such as replacing suffixIcon with suffix, are also a step towards a more consistent and flexible component API. Overall, this is a high-quality refactor that modernizes the component's architecture. My review includes a few suggestions to remove some leftover debugging code and clarify some implementation details.

display: inline-flex;
align-items: center;
user-select: none;
border: 1px solid blue;

Choose a reason for hiding this comment

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

high

This appears to be a debugging style that was left in the code. It should be removed before merging.

keyDown(container.querySelector('input'), KeyCode.L);
fireEvent.change(container.querySelector('input'), { target: { value: 'l' } });

console.log('clear');

Choose a reason for hiding this comment

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

high

This console.log statement appears to be for debugging and should be removed.

expect(onChange).not.toHaveBeenCalled();

jest.runAllTimers();
console.log('after 200ms');

Choose a reason for hiding this comment

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

high

This console.log statement appears to be for debugging and should be removed.

// This is used for semantic refactoring
@import (reference) url('./index.less');

.@{select-prefix}.@{select-prefix} {

Choose a reason for hiding this comment

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

medium

Using a duplicated class selector (.@{select-prefix}.@{select-prefix}) to increase specificity can be a bit of a hack and might make the CSS harder to maintain. If this is necessary to override existing styles, please add a comment explaining why. A better long-term solution might be to refactor the base styles to avoid needing such high specificity.

onClear?.();

selectorRef.current?.focus();
containerRef.current?.focus();

Choose a reason for hiding this comment

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

medium

Focusing the container ref on clear seems correct, but it's worth noting that this behavior has changed from focusing the selectorRef. This is a positive change as it's more aligned with the new component structure.

style={styles?.clear}
onMouseDown={(e) => {
// Mark to tell not trigger open or focus
(e.nativeEvent as any)._select_lazy = true;

Choose a reason for hiding this comment

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

medium

Modifying the native event object with a custom property (_select_lazy) to pass state between event handlers is generally discouraged as it can be fragile and hard to follow. While it might be a pragmatic solution for complex event interactions, have you considered alternatives like using a React ref to store this transient state? If this is the best approach, please add a comment explaining why this flag is necessary and how it's used in the onInternalMouseDown handler.

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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Multiple.test.tsx (1)

413-426: 移除调试用的 console.log 语句。

测试代码中的 console.log 语句应该在提交前移除,以保持测试输出的整洁。

应用此差异移除调试语句:

-    console.log('clear');
     // Backspace
     keyDown(container.querySelector('input'), KeyCode.BACKSPACE);
     fireEvent.change(container.querySelector('input'), { target: { value: '' } });

     onChange.mockReset();

     keyDown(container.querySelector('input'), KeyCode.BACKSPACE);
     expect(onChange).not.toHaveBeenCalled();

-    console.log('after 200ms');
     act(() => {
       jest.runAllTimers();
     });
🧹 Nitpick comments (19)
src/utils/keyUtil.ts (1)

25-29: 更新箭头键注释
注释当前写“箭头键 – 在输入中导航时不应触发打开”,但实际仅排除了 Up/Left/Right ,保留 Down 键以打开下拉。请将注释改为“除 Down 键外的箭头键 …”。

tests/setup.ts (1)

1-28: 测试环境的 MessageChannel 模拟实现正确。

这个 mock 实现通过 _target 属性实现了双向端口通信,使用 setTimeout(0) 模拟异步消息传递,符合测试需求。

可选优化:考虑为端口对象定义更具体的类型接口,而不是使用 any

+interface MockPort {
+  onmessage: ((event: { data: any }) => void) | null;
+  postMessage: (message: any) => void;
+  _target: MockPort | null;
+}
+
 window.MessageChannel = class {
-  port1: any;
-  port2: any;
+  port1: MockPort;
+  port2: MockPort;

   constructor() {
-    const createPort = () => {
-      const port = {
+    const createPort = (): MockPort => {
+      const port: MockPort = {
         onmessage: null,
         postMessage: (message: any) => {
           setTimeout(() => {
             if (port._target && typeof port._target.onmessage === 'function') {
               port._target.onmessage({ data: message });
             }
           }, 0);
         },
         _target: null,
       };
       return port;
     };
docs/examples/multiple.tsx (1)

102-102: Prop 重命名正确。

suffixIcon 更新为 suffix,与 BaseSelect API 重构保持一致。

可选优化:为保持一致性,考虑同时更新相关的 state 变量名:

   state = {
     useAnim: false,
-    suffixIcon: null,
+    suffix: null,
     loading: false,
     value: ['a10'],
     searchValue: '',
   };

   showArrow = (e) => {
     this.setState({
-      suffixIcon: e.target.checked ? <div>arrow</div> : null,
+      suffix: e.target.checked ? <div>arrow</div> : null,
     });
   };

   render() {
-    const { useAnim, loading, value, suffixIcon } = this.state;
+    const { useAnim, loading, value, suffix } = this.state;
     return (
       <div>
         {/* ... */}
         <input
           id="showArrow"
-          checked={!!suffixIcon}
+          checked={!!suffix}
           type="checkbox"
           onChange={this.showArrow}
         />
         {/* ... */}
-        suffix={suffixIcon}
+        suffix={suffix}
src/SelectInput/Affix.tsx (1)

1-16: LGTM! 简洁且设计良好的辅助组件。

Affix 组件实现简单清晰:

  • 正确扩展了 HTML 属性以支持标准 DOM 属性
  • 使用提前返回模式优化性能
  • 属性展开使用得当
  • 注释说明了组件不读取上下文或逻辑属性的设计意图

可以考虑添加 displayName 以改善开发体验:

 export default function Affix(props: AffixProps) {
   const { children, ...restProps } = props;

   if (!children) {
     return null;
   }

   return <div {...restProps}>{children}</div>;
 }
+
+Affix.displayName = 'Affix';
tests/Tags.test.tsx (1)

516-516: 测试只渲染单个 input,通用选择器安全
当前测试中的组件实例仅包含一个 input,因此 querySelector('input') 不会选错元素。如果未来组件引入多个 input,请改用更具特异性的选择器或 RTL 的 getByRole('textbox') 等查询方法。

src/SelectInput/Content/Placeholder.tsx (1)

19-21: 建议使用 display: none 代替 visibility: hidden

showfalse 时,使用 display: none 可以避免元素占用布局空间,提升渲染性能。

应用此差异来改进样式控制:

-      style={{
-        visibility: show ? 'visible' : 'hidden',
-      }}
+      style={{
+        display: show ? 'block' : 'none',
+      }}
tests/shared/maxTagRenderTest.tsx (1)

17-21: 测试断言仍然依赖快照。

虽然改为只对文本内容进行快照比较比完整 DOM 快照更好,但仍然存在维护负担。建议使用显式的期望值来提高测试的可读性和稳定性。

应用此差异来使用显式断言:

-      expect(
-        Array.from(container.querySelectorAll('.rc-select-selection-item-content')).map(
-          (ele) => ele.textContent,
-        ),
-      ).toMatchSnapshot();
+      const contents = Array.from(
+        container.querySelectorAll('.rc-select-selection-item-content')
+      ).map((ele) => ele.textContent);
+      expect(contents).toEqual(['On', 'Tw']); // maxTagTextLength=2
src/SelectInput/context.ts (1)

6-6: 添加对 useSelectInputContext 的返回值校验
建议在 useSelectInputContext 中加入防御性检查,未被 <SelectInputContext.Provider> 包裹时抛错:

 export function useSelectInputContext() {
-  return React.useContext(SelectInputContext);
+  const context = React.useContext(SelectInputContext);
+  if (!context) {
+    throw new Error('useSelectInputContext must be used within SelectInputContext.Provider');
+  }
+  return context;
 }

当前所有钩子调用均位于 Provider 范围内,此改动为可选重构。

src/SelectInput/Content/SingleContent.tsx (3)

41-55: 基于 value 的 option 匹配可能不稳(引用相等 vs. 值相等)

flattenOptions 使用 opt.value === displayValue.value 做严格相等查找。若 value 允许为对象或存在类型漂移(数字/字符串),可能出现匹配失败,导致 title/className/style 合并缺失。建议改为使用内部的值映射(如 rawValue → option 的 Map),或在上游保证 value 的稳定标识(基本类型且一致化)。


56-63: title 覆盖顺序 OK,但请确认覆盖意图

当前顺序为:option.data → displayValue → rootTitle(rootTitle 若有则最终覆盖)。这会无条件覆盖来自 option 的 title。请确认产品语义是否期望 rootTitle 始终最高优先级;若只在无 title 时兜底,可调整为仅在无 title 时使用 rootTitle。


67-71: combobox 下重置 inputChanged 的副作用

activeValue 变化时在 combobox 模式下重置 inputChanged 为 false 符合旧逻辑。但这会在频繁高亮移动时反复重置。若仅需在打开或选中后重置,可考虑收窄触发条件以减少无效重渲染。

src/SelectInput/Input.tsx (2)

196-206: 合并自定义 Input 的函数型 props 时应限于事件处理器

当前对所有函数型 prop 进行“先现有后共享”的合并,可能误合并非事件类函数(如 render 回调),引入双调用副作用。建议仅合并以 on 开头的事件处理器键。

可按如下方式约束:

-    Object.keys(existingProps).forEach((key) => {
-      const existingValue = (existingProps as any)[key];
-      if (typeof existingValue === 'function') {
+    Object.keys(existingProps).forEach((key) => {
+      const existingValue = (existingProps as any)[key];
+      if (typeof existingValue === 'function' && /^on[A-Z]/.test(key)) {
         (mergedProps as any)[key] = (...args: any[]) => {
           existingValue(...args);
           (sharedInputProps as any)[key]?.(...args);
         };
       }
     });

161-166: CSS 自定义属性建议带单位,增强兼容性

--select-input-width 直接传 number,某些场景下期望为带单位的字符串。建议在设置时转为 ${scrollWidth}px,并在消费处确保用作长度值。

src/hooks/useOpen.ts (2)

4-20: MessageChannel 不可用时的降级

在少数运行环境(老旧浏览器、极简测试环境)可能缺少 MessageChannel。建议在不可用时回退到 setTimeout(fn, 0),提升健壮性。


36-41: 入参类型建议更宽以兼容非受控用法

propOpenonOpenpostOpen 当前为必填且精确类型。实际调用场景常传 undefined(非受控)。建议将 propOpen?: booleanonOpen?: (nextOpen: boolean) => voidpostOpen?: (nextOpen: boolean) => boolean,并在内部提供默认实现,减少上游断言与兜底代码。

tests/Select.test.tsx (1)

38-45: 计时器模式切换重复

文件顶层 beforeEach/afterEach 已全局启用/还原 fake timers;部分用例内仍显式 useFakeTimers/useRealTimers。这虽不影响正确性,但会增加认知成本。建议统一由顶层控制,单测内仅在确有差异时覆盖。

src/SelectInput/Content/MultipleContent.tsx (1)

93-102: 避免删除按钮重复渲染 “×”

TransBtn 已通过 customizeIcon={removeIcon} 传入图标,同时又在子节点渲染了字符 “×”。当 removeIcon 为默认 “×” 或用户自定义图标时,可能出现重复显示。

建议移除子节点 “×”,仅依赖 customizeIcon:

       {closable && (
         <TransBtn
           className={`${selectionItemPrefixCls}-remove`}
           onMouseDown={onPreventMouseDown}
           onClick={onClose}
           customizeIcon={removeIcon}
         >
-          ×
         </TransBtn>
       )}

注:上方 removeIcon 已提供默认值 '×'(见行 60-66),不会导致图标缺失。

Also applies to: 59-66

src/BaseSelect/index.tsx (2)

607-611: 条件判断中误用函数标识,建议改为检查状态或移除冗余判断

此处 triggerOpen 是函数而非布尔值,&& triggerOpen 恒为真,易混淆:

if (popupElement?.contains(target as HTMLElement) && triggerOpen) {
  triggerOpen(true, true);
}

建议改为基于状态判断或直接去掉冗余条件:

- if (popupElement?.contains(target as HTMLElement) && triggerOpen) {
+ if (popupElement?.contains(target as HTMLElement)) {
    triggerOpen(true, true);
  }

或(若仅在已打开时加锁):

- if (popupElement?.contains(target as HTMLElement) && triggerOpen) {
+ if (popupElement?.contains(target as HTMLElement) && mergedOpen) {
    triggerOpen(true, true);
  }

640-666: 移除 BaseSelectContext 中冗余的 boolean triggerOpen,统一使用 open 与 toggleOpen
全局检索确认所有组件均正确将 triggerOpen 作为布尔值使用,context 中的 setter 已命名为 toggleOpen,未发现误用。建议在下个大版本中废弃 context.triggerOpen,统一使用 open;若需兼容短期保留,请添加 @deprecated 标注并规划后续移除。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2dc268 and 7e2ffc6.

⛔ Files ignored due to path filters (5)
  • tests/__snapshots__/Combobox.test.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Multiple.test.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Select.test.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Tags.test.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/ssr.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (46)
  • assets/index.less (1 hunks)
  • assets/patch.less (1 hunks)
  • docs/examples/multiple-with-maxCount.tsx (1 hunks)
  • docs/examples/multiple.tsx (1 hunks)
  • examples/InputExample.tsx (1 hunks)
  • examples/SelectContentExample.tsx (1 hunks)
  • jest.config.js (1 hunks)
  • src/BaseSelect/index.tsx (24 hunks)
  • src/Select.tsx (1 hunks)
  • src/SelectInput/Affix.tsx (1 hunks)
  • src/SelectInput/Content/MultipleContent.tsx (1 hunks)
  • src/SelectInput/Content/Placeholder.tsx (1 hunks)
  • src/SelectInput/Content/SingleContent.tsx (1 hunks)
  • src/SelectInput/Content/index.tsx (1 hunks)
  • src/SelectInput/Input.tsx (1 hunks)
  • src/SelectInput/context.ts (1 hunks)
  • src/SelectInput/index.tsx (1 hunks)
  • src/SelectTrigger.tsx (3 hunks)
  • src/Selector/Input.tsx (0 hunks)
  • src/Selector/MultipleSelector.tsx (0 hunks)
  • src/Selector/SingleSelector.tsx (0 hunks)
  • src/Selector/index.tsx (0 hunks)
  • src/TransBtn.tsx (1 hunks)
  • src/hooks/useAllowClear.tsx (1 hunks)
  • src/hooks/useBaseProps.ts (1 hunks)
  • src/hooks/useComponents.ts (1 hunks)
  • src/hooks/useOpen.ts (1 hunks)
  • src/hooks/useSearchConfig.ts (2 hunks)
  • src/hooks/useSelectTriggerControl.ts (1 hunks)
  • src/utils/keyUtil.ts (1 hunks)
  • tests/Accessibility.test.tsx (1 hunks)
  • tests/BaseSelect.test.tsx (2 hunks)
  • tests/Combobox.test.tsx (10 hunks)
  • tests/Custom.test.tsx (3 hunks)
  • tests/Multiple.test.tsx (14 hunks)
  • tests/Select.test.tsx (24 hunks)
  • tests/Tags.test.tsx (3 hunks)
  • tests/focus.test.tsx (0 hunks)
  • tests/placeholder.test.tsx (1 hunks)
  • tests/setup.ts (1 hunks)
  • tests/shared/allowClearTest.tsx (1 hunks)
  • tests/shared/blurTest.tsx (3 hunks)
  • tests/shared/inputFilterTest.tsx (2 hunks)
  • tests/shared/maxTagRenderTest.tsx (4 hunks)
  • tests/shared/removeSelectedTest.tsx (1 hunks)
  • tests/utils/common.ts (3 hunks)
💤 Files with no reviewable changes (5)
  • tests/focus.test.tsx
  • src/Selector/MultipleSelector.tsx
  • src/Selector/SingleSelector.tsx
  • src/Selector/Input.tsx
  • src/Selector/index.tsx
🧰 Additional context used
🧬 Code graph analysis (21)
src/SelectInput/context.ts (1)
src/SelectInput/index.tsx (1)
  • SelectInputProps (21-50)
src/SelectInput/Content/index.tsx (1)
src/SelectInput/context.ts (1)
  • useSelectInputContext (8-10)
src/hooks/useSearchConfig.ts (1)
src/Select.tsx (3)
  • SearchConfig (114-121)
  • DefaultOptionType (99-103)
  • SelectProps (122-182)
src/hooks/useComponents.ts (2)
src/SelectInput/index.tsx (2)
  • SelectInputProps (21-50)
  • SelectInputRef (15-19)
src/BaseSelect/index.tsx (1)
  • BaseSelectProps (136-230)
examples/SelectContentExample.tsx (1)
src/BaseSelect/index.tsx (1)
  • DisplayValueType (42-42)
src/SelectInput/Content/MultipleContent.tsx (6)
src/BaseSelect/index.tsx (3)
  • DisplayValueType (42-42)
  • CustomTagProps (69-77)
  • RawValueType (47-47)
src/SelectInput/Content/index.tsx (1)
  • SharedContentProps (8-10)
src/SelectInput/context.ts (1)
  • useSelectInputContext (8-10)
src/utils/commonUtil.ts (1)
  • getTitle (29-40)
src/Select.tsx (1)
  • RawValueType (71-71)
src/SelectInput/Content/Placeholder.tsx (1)
  • Placeholder (8-26)
src/SelectInput/Content/Placeholder.tsx (1)
src/SelectInput/context.ts (1)
  • useSelectInputContext (8-10)
src/Select.tsx (1)
src/hooks/useSearchConfig.ts (1)
  • useSearchConfig (5-49)
tests/Tags.test.tsx (1)
src/index.ts (1)
  • Option (9-9)
tests/Combobox.test.tsx (1)
tests/utils/common.ts (1)
  • expectOpen (4-20)
tests/BaseSelect.test.tsx (3)
src/BaseSelect/index.tsx (1)
  • RefOptionListProps (63-67)
src/OptionList.tsx (1)
  • OptionListProps (20-20)
tests/utils/common.ts (1)
  • waitFakeTimer (138-148)
src/BaseSelect/index.tsx (9)
src/interface.ts (1)
  • RenderNode (22-22)
src/hooks/useComponents.ts (2)
  • ComponentsConfig (5-8)
  • useComponents (19-42)
src/SelectInput/index.tsx (1)
  • SelectInputRef (15-19)
src/SelectTrigger.tsx (1)
  • RefTriggerProps (52-54)
src/OptionList.tsx (1)
  • RefOptionListProps (22-26)
src/hooks/useOpen.ts (1)
  • useOpen (36-93)
src/hooks/useSelectTriggerControl.ts (1)
  • useSelectTriggerControl (4-37)
src/hooks/useBaseProps.ts (1)
  • BaseSelectContext (16-16)
src/BaseSelect/Polite.tsx (1)
  • Polite (9-32)
tests/Multiple.test.tsx (1)
tests/utils/common.ts (1)
  • toggleOpen (22-31)
tests/Custom.test.tsx (1)
tests/utils/common.ts (1)
  • waitFakeTimer (138-148)
src/hooks/useAllowClear.tsx (1)
src/interface.ts (2)
  • DisplayValueType (13-20)
  • Mode (26-26)
tests/Select.test.tsx (1)
tests/utils/common.ts (2)
  • keyDown (113-122)
  • expectOpen (4-20)
src/hooks/useOpen.ts (1)
tests/utils/common.ts (1)
  • toggleOpen (22-31)
src/SelectInput/Input.tsx (3)
src/SelectInput/context.ts (1)
  • useSelectInputContext (8-10)
src/hooks/useBaseProps.ts (1)
  • useBaseProps (18-20)
src/hooks/useLayoutEffect.ts (1)
  • useLayoutEffect (8-16)
tests/placeholder.test.tsx (1)
tests/utils/common.ts (1)
  • toggleOpen (22-31)
src/SelectInput/index.tsx (4)
src/hooks/useComponents.ts (1)
  • ComponentsConfig (5-8)
src/utils/keyUtil.ts (1)
  • isValidateOpenKey (4-45)
tests/utils/common.ts (1)
  • toggleOpen (22-31)
src/SelectInput/Affix.tsx (1)
  • Affix (8-16)
src/SelectInput/Content/SingleContent.tsx (4)
src/SelectInput/Content/index.tsx (1)
  • SharedContentProps (8-10)
src/SelectInput/context.ts (1)
  • useSelectInputContext (8-10)
src/utils/commonUtil.ts (1)
  • getTitle (29-40)
src/SelectInput/Content/Placeholder.tsx (1)
  • Placeholder (8-26)
🪛 GitHub Actions: ✅ test
examples/SelectContentExample.tsx

[error] 22-24: Type '{ prefixCls: string; multiple: boolean; value: DisplayValueType[]; }' is not assignable to type 'IntrinsicAttributes & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & RefAttributes'.


[error] 27-27: Type '{ prefixCls: string; multiple: boolean; value: DisplayValueType[]; }' is not assignable to type 'IntrinsicAttributes & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & RefAttributes'.


[error] 32-32: Type '{ prefixCls: string; multiple: boolean; value: DisplayValueType[]; }' is not assignable to type 'IntrinsicAttributes & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & RefAttributes'.


[error] 37-37: Type '{ prefixCls: string; multiple: boolean; value: DisplayValueType[]; }' is not assignable to type 'IntrinsicAttributes & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & RefAttributes'.

examples/InputExample.tsx

[error] 13-13: Type '{ prefixCls: string; placeholder: string; }' is not assignable to type 'IntrinsicAttributes & InputProps & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & InputProps & RefAttributes'.


[error] 19-19: Type '{ prefixCls: string; value: string; onChange: (e: ChangeEvent) => void; placeholder: string; }' is not assignable to type 'IntrinsicAttributes & InputProps & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & InputProps & RefAttributes'.


[error] 30-30: Type '{ prefixCls: string; disabled: true; placeholder: string; }' is not assignable to type 'IntrinsicAttributes & InputProps & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & InputProps & RefAttributes'.


[error] 39-39: Type '{ prefixCls: string; className: string; style: { border: string; borderRadius: string; }; placeholder: string; }' is not assignable to type 'IntrinsicAttributes & InputProps & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & InputProps & RefAttributes'.

src/SelectInput/index.tsx

[error] 207-207: Argument of type 'readonly ["value", "onChange", "removeIcon", "placeholder", "maxTagCount", "maxTagTextLength", "maxTagPlaceholder", "choiceTransitionName", "onInputKeyDown", "onPopupScroll", "tabIndex", "activeValue", "onSelectorRemove", "focused"]' is not assignable to parameter of type '("hidden" | "color" | "content" | "contextMenu" | "slot" | "title" | "onMouseEnter" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | ... 249 more ... | "focused")[] | readonly ("hidden" | ... 259 more ... | "focused")[]'. Type '"value" | "tabIndex" | "onChange" | "placeholder" | "activeValue" | "choiceTransitionName" | "maxTagTextLength" | "maxTagCount" | "maxTagPlaceholder" | "removeIcon" | "onPopupScroll" | "onInputKeyDown" | "onSelectorRemove" | "focused"' is not assignable to type '"hidden" | "color" | "content" | "contextMenu" | "slot" | "title" | "onMouseEnter" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | ... 249 more ... | "focused"'.

🔇 Additional comments (48)
src/SelectTrigger.tsx (3)

106-106: 解构实现正确。

从 props 中解构 onPopupMouseDown 的实现遵循了现有模式,代码结构清晰。


167-171: 事件处理器正确连接到弹出层。

onMouseDown 事件处理器添加到弹出层包装 div 的实现正确,与现有的 onMouseEnter 处理器保持一致。类型安全性得到保证(React.MouseEventHandler<HTMLDivElement>)。

需要注意的是,包装 div 上的 onMouseDown 可能会影响弹出内容内部的事件传播。请确保这不会干扰弹出内容中现有的 mousedown 处理器。


79-79: 保持 onPopupMouseDown 为必需属性。BaseSelect 组件在所有渲染中都会传入该回调,与 onPopupMouseEnter 保持一致,无需改为可选。

Likely an incorrect or invalid review comment.

src/hooks/useSelectTriggerControl.ts (3)

1-2: LGTM!引入 useEvent 改进了代码质量。

使用 useEvent 替代之前基于 ref 的状态追踪是一个很好的重构,它提供了稳定的回调引用同时能访问最新的闭包变量,避免了不必要的重新渲染。


33-36: LGTM!useEffect 依赖项简化正确。

由于 onGlobalMouseDown 通过 useEvent 包装提供了稳定的引用,将其作为唯一依赖项是正确的。这避免了不必要的监听器重新注册,同时确保回调始终访问最新的闭包值。


4-9: 请运行以下脚本以确认useSelectTriggerControl的所有调用是否与新签名兼容:

#!/bin/bash
# 查找所有调用 useSelectTriggerControl 的地方,检查传入的 elements 函数的兼容性
rg -n -C3 'useSelectTriggerControl\(' -g '*.ts' -g '*.tsx'
src/TransBtn.tsx (1)

15-24: 文档补充到位

注释准确描述了组件作用与交互行为,符合现有实现。

tests/shared/removeSelectedTest.tsx (1)

45-45: 更新选择器以匹配新的类名命名规范

改为 .rc-select-item-remove 看起来与本次语义化重构一致。请同时确认测试工具 removeSelection/其它测试中的旧类名也已统一替换,避免选择器漂移导致的假阳性/假阴性。

jest.config.js (1)

1-3: Jest 环境初始化配置合理

在环境后加载 tests/setup.ts 符合预期,便于全局 mock 与定制断言。

tests/shared/inputFilterTest.tsx (1)

4-4: 基于计时器的异步断言处理正确

引入 act 包裹 runAllTimers,并在用例末尾切回真实计时器,避免测试泄漏,写法稳健。

Also applies to: 8-8, 27-29, 31-31

examples/SelectContentExample.tsx (1)

22-22: 示例不应直接使用 SelectContent 传入 prefixCls/multiple/value
示例中 SelectContent 为内部组件,仅接受 inputPropsref,其余属性需通过 SelectInputContext 或上层 Select 组件提供,请修改示例以正确展示用法。

Likely an incorrect or invalid review comment.

assets/index.less (1)

2-2: 样式模块导入正确。

引入 patch.less 为新的选择器组件提供样式支持,与 PR 中提到的样式重构保持一致。

tests/Custom.test.tsx (1)

17-26: 测试更新符合新的异步测试模式。

将测试函数改为 async 并在交互后使用 await waitFakeTimer() 确保定时器驱动的 UI 更新完成后再进行断言,与 PR 中其他测试的更新模式一致。

tests/shared/allowClearTest.tsx (1)

9-9: 清除按钮选择器更新正确。

类名从 .rc-select-clear-icon 更新为 .rc-select-clear,与组件重构后的新 DOM 结构保持一致。

tests/Accessibility.test.tsx (1)

65-68: 定时器处理正确包裹在 act 中。

jest.runAllTimers() 包裹在 act() 块中确保所有 React 状态更新在断言之前完成,这是 React 测试的最佳实践。

docs/examples/multiple-with-maxCount.tsx (1)

17-17: 启用搜索功能的 prop 添加正确。

为多选示例添加 showSearch 属性,启用搜索 UI 功能。

src/Select.tsx (1)

241-241: 搜索配置钩子调用更新正确。

mode 参数传递给 useSearchConfig,使搜索配置能够根据不同的选择模式(combobox、tags、multiple)进行适配,与 src/hooks/useSearchConfig.ts 中更新的签名一致。

src/hooks/useBaseProps.ts (1)

13-13: LGTM! 增强了可访问性支持。

添加可选的 role 属性为 BaseSelect 组件提供了 ARIA 角色自定义能力,这是一个良好的可访问性改进。属性定义符合 TypeScript 和 React 规范。

tests/BaseSelect.test.tsx (2)

5-5: LGTM! 测试的定时器管理得到改进。

引入 waitFakeTimer 工具函数并在 beforeEach/afterEach 中统一管理假定时器,确保了测试环境的一致性和可靠性。

Also applies to: 12-18


20-51: LGTM! 正确处理了异步测试行为。

将测试转换为异步函数并在用户交互后使用 await waitFakeTimer() 等待定时器驱动的 UI 更新,这确保了断言在正确的时机执行。

tests/Tags.test.tsx (1)

68-68: LGTM! 简化了代码结构。

移除未使用的 option2 常量并将其内联使用,减少了不必要的变量声明。

tests/shared/blurTest.tsx (2)

5-5: LGTM! 正确使用 act() 包装定时器操作。

jest.runAllTimers() 包装在 act() 中确保了 React 状态更新的正确同步,这是 React 测试的最佳实践。

Also applies to: 37-39


68-70: LGTM! 选择器与新的组件结构保持一致。

将选择器从 .rc-select-selector 更新为 .rc-select 反映了 PR 中组件 DOM 结构的变化。

tests/placeholder.test.tsx (2)

28-32: LGTM! 选择器已更新以匹配新的 DOM 结构。

.rc-select-selection-item 更新为 .rc-select-content-value 反映了组件内容渲染结构的变化。


9-15: 手动验证受控 searchValue 时的 placeholder 可见性
自动化搜索未能定位到相关渲染或样式逻辑,请在组件实现中手动确认当 searchValue 受控时 .rc-select-placeholder 的可见性行为是否符合预期。

tests/Combobox.test.tsx (4)

77-80: LGTM! 选择器已更新以匹配新的 CSS 类名。

测试选择器已统一更新:

  • .rc-select-selection-placeholder.rc-select-placeholder
  • .rc-select-clear-icon.rc-select-clear

这些更改与 PR 中的组件结构调整保持一致。

Also applies to: 338-339, 349-350, 361-364


115-115: LGTM! 使用了更符合语义的断言匹配器。

.value 属性访问改为使用 toHaveValue() 匹配器,这是 testing-library 推荐的更具表达性的断言方式。

Also applies to: 322-322


470-487: LGTM! 正确管理了测试中的假定时器。

添加了 jest.useFakeTimers()jest.useRealTimers() 调用以及 jest.runAllTimers() 包装在 act() 中,确保了定时器驱动的测试行为的可预测性。


620-621: LGTM! 选择器与组件结构变化同步。

.rc-select-selector 更新为 .rc-select,反映了组件 DOM 结构的简化。

Also applies to: 637-638

tests/utils/common.ts (4)

6-8: LGTM! 测试工具函数正确处理定时器同步。

expectOpentoggleOpenselectItem 中将定时器操作包装在 act() 中,确保了所有 React 状态更新在断言执行前完成。这是关键的测试可靠性改进。

Also applies to: 25-27, 36-38


24-24: LGTM! 选择器已更新以匹配新的组件结构。

.rc-select-selector 更新为 .rc-select,与 PR 中的组件 DOM 重构保持一致。

Also applies to: 30-30


47-49: LGTM! 增强了选择器的健壮性。

使用可选链操作符并添加 .rc-select-content-value 作为后备选择器,提高了在不同 DOM 结构下查找元素的容错能力。


57-58: LGTM! 非 HTMLElement 分支的选择器保持一致。

更新了 Enzyme 风格测试的选择器以匹配新的类名结构。

tests/shared/maxTagRenderTest.tsx (1)

58-59: LGTM!

显式检查 .rc-select-content-item 的数量和 .rc-select-content-item-rest 的不存在,这比快照测试更加清晰和稳定。

tests/Multiple.test.tsx (4)

36-42: LGTM!

beforeEachafterEach 中添加计时器生命周期管理是良好的测试实践,确保了测试的隔离性和可预测性。


371-383: LGTM!

suffixIcon 重命名为 suffix 并更新相应的选择器(从 .rc-select-arrow.rc-select-suffix)与组件重构保持一致。


437-443: LGTM!

使用 toHaveStyle 断言代替直接属性访问提高了测试的可读性和健壮性。


518-521: LGTM!

验证 tabIndex 正确应用到 input 元素而不是根元素,符合可访问性最佳实践。

assets/patch.less (3)

12-22: LGTM!

内容区域的 flex 布局配置合理,使用 min-width: 0 允许 flex 项收缩,结合 text-overflow: ellipsis 实现了良好的文本截断效果。


32-36: LGTM!

使用 \00a0(不间断空格)作为占位符的 ::after 伪元素是一个巧妙的技巧,可以在占位符为空时保持布局稳定性。


78-81: LGTM!

使用 CSS 自定义属性 --select-input-width 来动态控制输入框宽度是一个灵活的方案,最小宽度 4px 确保了输入框始终可见。

src/SelectInput/Content/index.tsx (1)

12-31: LGTM!

SelectContent 组件的实现清晰且职责单一:

  • 正确使用 forwardRef 传递引用
  • 使用 pickAttrs 提取 ARIA 属性确保可访问性
  • 根据 showSearch 设置 readOnly 属性符合预期
  • 根据 multiple 标志路由到相应的内容组件
src/hooks/useComponents.ts (2)

5-17: LGTM!

接口设计良好:

  • ComponentsConfig 提供灵活的组件替换选项
  • FilledComponentsConfig 使用强类型的 ForwardRefExoticComponent 确保类型安全
  • 类型定义涵盖了输入元素的多种可能类型(input 和 textarea)

19-41: LGTM!

钩子实现正确:

  • 优先使用函数式元素获取器(getRawInputElementgetInputElement)而非直接传入的组件
  • 正确使用 useMemo 进行记忆化优化
  • 依赖数组包含所有相关值,避免闭包陷阱
src/hooks/useSearchConfig.ts (1)

30-38: multiple 模式下默认启用 showSearch 符合预期

经排查,仅测试和示例中使用了 mode="multiple" 且未显式传入 showSearch,与文档保持一致。

tests/Select.test.tsx (1)

1572-1579: loading 图标断言覆盖单选与多选,命名与语义一致

断言基于 .rc-select-suffix-loading.rc-select-suffix-icon 的存在性,与新语义一致,测试用例更新合理。

src/SelectInput/index.tsx (2)

124-155: 键盘打开逻辑与旧行为对齐,防止 TextArea 方向键误开

  • UP/DOWN 在非 TextArea 且已打开时 preventDefault,避免滚动冲突;
  • 通过 isValidateOpenKey(which) 控制开合,过滤系统键;
    此段逻辑清晰,符合预期交互。

171-196: 鼠标交互与惰性标记(_select_lazy)

对 root 的 onMouseDown 中使用 _select_lazy 标记避免清除按钮触发 focus/open,且在单选 +(combobox|showSearch)时阻止关闭,细节处理到位。

display: inline-flex;
align-items: center;
user-select: none;
border: 1px solid blue;
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 | 🟡 Minor

移除或配置化硬编码的蓝色边框。

硬编码的 border: 1px solid blue; 看起来像是用于调试或演示目的。应该将其移除或改为可配置的变量,以便在生产环境中使用。

应用此差异移除硬编码边框:

   display: inline-flex;
   align-items: center;
   user-select: none;
-  border: 1px solid blue;
   position: relative;

或者使用变量:

-  border: 1px solid blue;
+  border: @select-border-width @select-border-style @select-border-color;
🤖 Prompt for AI Agents
In assets/patch.less around line 8 (line 8 only), there is a hard-coded debug
style "border: 1px solid blue;" — remove this hard-coded rule or replace it with
a configurable Less variable (e.g. use a border declaration that references a
variable like @patch-border or @debug-border with a sensible default or none)
and update your variables/settings file to define that variable; ensure any
consumers fallback when the variable is unset and remove any remaining
debug-only styling before production.


<div>
<h3>Basic Input</h3>
<Input prefixCls="rc-select" placeholder="Type something..." />
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 | 🔴 Critical

🧩 Analysis chain

示例使用了 Input 未声明的 prop(prefixCls),导致编译失败

InputProps 未包含 prefixCls,四处用法均触发 TS 报错。请移除 prefixCls 或改用组件实际支持的 props。

可先用以下修复解 unblock:

-        <Input prefixCls="rc-select" placeholder="Type something..." />
+        <Input placeholder="Type something..." />

-        <Input 
-          prefixCls="rc-select" 
+        <Input 
           value={value} 
           onChange={(e) => setValue(e.target.value)}
           placeholder="Controlled input..."
         />

-        <Input 
-          prefixCls="rc-select" 
+        <Input 
           disabled 
           placeholder="Disabled input..."
         />

-        <Input 
-          prefixCls="rc-select" 
+        <Input 
           className="custom-input"
           style={{ border: '2px solid #1890ff', borderRadius: '4px' }}
           placeholder="Styled input..."
         />

如需核对 InputProps 是否确实不含 prefixCls,可运行:

Also applies to: 19-19, 30-30, 39-39


🏁 Script executed:

#!/bin/bash
# 查找 InputProps 定义与是否包含 prefixCls
rg -nP --type=ts -C2 '(interface|type)\s+InputProps\b|prefixCls' src/SelectInput

Length of output: 6027


为 InputProps 接口添加 prefixCls 声明

src/SelectInput/Input.tsxInputProps 缺少 prefixCls,导致示例 <Input prefixCls="rc-select" /> TS 报错。请补充:

 export interface InputProps {
   id?: string;
   readOnly?: boolean;
+  /** CSS 类名前缀 */
+  prefixCls: string;
   // … 其他已有属性
 }
📝 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
<Input prefixCls="rc-select" placeholder="Type something..." />
// In src/SelectInput/Input.tsx
export interface InputProps {
id?: string;
readOnly?: boolean;
/** CSS 类名前缀 */
prefixCls: string;
// … 其他已有属性
}
🧰 Tools
🪛 GitHub Actions: ✅ test

[error] 13-13: Type '{ prefixCls: string; placeholder: string; }' is not assignable to type 'IntrinsicAttributes & InputProps & RefAttributes'. Property 'prefixCls' does not exist on type 'IntrinsicAttributes & InputProps & RefAttributes'.

🤖 Prompt for AI Agents
In examples/InputExample.tsx around line 13 the example uses <Input
prefixCls="rc-select" /> but InputProps in src/SelectInput/Input.tsx lacks a
prefixCls declaration; update src/SelectInput/Input.tsx by adding prefixCls?:
string to the InputProps interface and ensure the component accepts and forwards
that prop to the rendered element (e.g., include prefixCls in the props
destructuring and pass it into the root element or className computation) so the
example type-checks.

Comment on lines +10 to +31
const onGlobalMouseDown = useEvent((event: MouseEvent) => {
// If trigger is customized, Trigger will take control of popupVisible
if (customizedTrigger) {
return;
}

let target = event.target as HTMLElement;
let target = event.target as HTMLElement;

if (target.shadowRoot && event.composed) {
target = (event.composedPath()[0] || target) as HTMLElement;
}
if (target.shadowRoot && event.composed) {
target = (event.composedPath()[0] || target) as HTMLElement;
}

if (
propsRef.current.open &&
elements()
.filter((element) => element)
.every((element) => !element.contains(target) && element !== target)
) {
// Should trigger close
propsRef.current.triggerOpen(false);
}
if (
open &&
elements()
.filter((element) => element)
.every((element) => !element.contains(target) && element !== target)
) {
// Should trigger close
triggerOpen(false);
}
});
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 | 🟡 Minor

类型转换与新的 SVGElement 支持不一致。

useEvent 的重构看起来不错,逻辑正确。但是,第 16 行和第 19 行将 target 强制转换为 HTMLElement,这与新增的 SVGElement 支持不一致。如果用户点击了 SVG 元素,event.target 可能是 SVGElement,类型转换会不准确。

应用以下差异修复类型安全问题:

-    let target = event.target as HTMLElement;
+    let target = event.target as Element;

     if (target.shadowRoot && event.composed) {
-      target = (event.composedPath()[0] || target) as HTMLElement;
+      target = (event.composedPath()[0] || target) as Element;
     }

注意:使用 Element 类型(HTMLElementSVGElement 的公共父类)更准确,且 contains() 方法在 Element 上可用。

🤖 Prompt for AI Agents
In src/hooks/useSelectTriggerControl.ts around lines 10 to 31, the code
currently casts event targets to HTMLElement which breaks type-safety for
SVGElement clicks; change the target typing and casts to the more general
Element type (use Element for target and when reading composedPath entries) so
contains() calls remain valid for both HTMLElement and SVGElement, and preserve
the composedPath handling and early return for customizedTrigger while ensuring
TypeScript uses Element rather than HTMLElement.

Comment on lines +106 to +114
const customizeRenderSelector = (
value: RawValueType,
content: React.ReactNode,
itemDisabled: boolean,
closable?: boolean,
onClose?: React.MouseEventHandler,
isMaxTag?: boolean,
info?: { index: number },
) => {
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 | 🔴 Critical

customizeRenderSelector 的参数类型与调用不一致,编译期错误

renderRest 调用 customizeRenderSelector(undefined, …) 时传入了 undefined,但 value 的类型声明为 RawValueType(string | number,不允许 undefined)。这会导致 TS 报错并阻断编译。建议放宽 value 参数类型以覆盖 “更多标签” 的场景。

可按以下方式修改:

-  const customizeRenderSelector = (
-    value: RawValueType,
+  const customizeRenderSelector = (
+    value?: RawValueType,
     content: React.ReactNode,
     itemDisabled: boolean,
     closable?: boolean,
     onClose?: React.MouseEventHandler,
     isMaxTag?: boolean,
     info?: { index: number },
   ) => {

说明:CustomTagProps 的 value 是 any(参见 BaseSelect 中的定义),放宽为可选或 RawValueType | undefined 均可满足使用方。Based on learnings

Also applies to: 170-182

🤖 Prompt for AI Agents
In src/SelectInput/Content/MultipleContent.tsx around lines 106-114, the
customizeRenderSelector signature declares value as RawValueType (string|number)
but callers (e.g., renderRest) pass undefined for the "more tags" case, causing
TS compile errors; change the value parameter to be optional or accept undefined
(e.g., value?: RawValueType or RawValueType | undefined) so calls with undefined
are valid, and apply the same relaxation to the other occurrence around lines
170-182.

Comment on lines +181 to +185
'aria-owns': `${id}_list`,
'aria-autocomplete': 'list' as const,
'aria-controls': `${id}_list`,
'aria-activedescendant': open ? activeDescendantId : undefined,
};
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 | 🟠 Major

当未提供 id 时避免输出无效 ARIA 关联

aria-owns / aria-controls 在 id 缺失时会变成 undefined_list。建议按有无 id 条件赋值,避免无效可访问性属性。

应用如下修改:

-    'aria-owns': `${id}_list`,
+    'aria-owns': id ? `${id}_list` : undefined,
...
-    'aria-controls': `${id}_list`,
+    'aria-controls': id ? `${id}_list` : undefined,
📝 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
'aria-owns': `${id}_list`,
'aria-autocomplete': 'list' as const,
'aria-controls': `${id}_list`,
'aria-activedescendant': open ? activeDescendantId : undefined,
};
'aria-owns': id ? `${id}_list` : undefined,
'aria-autocomplete': 'list' as const,
'aria-controls': id ? `${id}_list` : undefined,
'aria-activedescendant': open ? activeDescendantId : undefined,
🤖 Prompt for AI Agents
In src/SelectInput/Input.tsx around lines 181 to 185, aria-owns and
aria-controls are currently built using `${id}_list` which produces invalid
values like `undefined_list` when id is not provided; change the logic to only
set 'aria-owns' and 'aria-controls' when id is truthy (e.g., id ? `${id}_list` :
undefined) so those ARIA attributes are omitted if no id exists, keep
'aria-autocomplete' and 'aria-activedescendant' behavior unchanged.

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

♻️ Duplicate comments (1)
src/SelectInput/index.tsx (1)

264-267: 避免在原生事件对象上打补丁 _select_lazy

此前已有类似反馈。建议用 ref 标记或 data- 属性判断点击来源,替代修改 event.nativeEvent。如无法避免,请补充注释说明必要性与作用域。

🧹 Nitpick comments (7)
src/SelectInput/index.tsx (5)

52-67: 修复 omit 的类型宽化方式,避免 any 失去类型约束

当前用 as any 绕过类型系统。建议将常量声明为 string[] 并最小化断言范围,保留更多类型信息。

-const DEFAULT_OMIT_PROPS = [
+const DEFAULT_OMIT_PROPS: string[] = [
   'value',
   'onChange',
   'removeIcon',
   'placeholder',
   'maxTagCount',
   'maxTagTextLength',
   'maxTagPlaceholder',
   'choiceTransitionName',
   'onInputKeyDown',
   'onPopupScroll',
   'tabIndex',
   'activeValue',
   'onSelectorRemove',
   'focused',
-] as const;
+] ;
@@
-  const domProps = omit(restProps, DEFAULT_OMIT_PROPS as any);
+  const domProps = omit(restProps as any, DEFAULT_OMIT_PROPS);

这与先前流水线失败的建议一致,同时尽量减少 any 的扩散。Based on learnings

Also applies to: 207-207


121-121: inputRef 类型过窄,建议支持 TextArea

代码里用到了 instanceof HTMLTextAreaElement 判断,但 inputRef 声明为 HTMLInputElement。建议改为联合类型以匹配实际:

-  const inputRef = React.useRef<HTMLInputElement>(null);
+  const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);

124-155: 键盘处理使用了过时的 which 与位运算技巧,建议现代化与可读性改进

  • 使用 event.key/event.keyCode 替代 which
  • includes() 替代 ~indexOf 提升可读性。

示例改写(要点):

-  const { which } = event;
+  const { key, keyCode } = event;

@@
-  if (!isTextAreaElement && triggerOpen && (which === KeyCode.UP || which === KeyCode.DOWN)) {
+  if (!isTextAreaElement && triggerOpen && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) {
     event.preventDefault();
   }
@@
-  if (
-    isTextAreaElement &&
-    !triggerOpen &&
-    ~[KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT].indexOf(which)
-  ) {
+  if (
+    isTextAreaElement &&
+    !triggerOpen &&
+    [KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT].includes(keyCode)
+  ) {
     return;
   }
@@
-  if (isValidateOpenKey(which)) {
+  if (isValidateOpenKey(keyCode)) {
     toggleOpen(true);
   }

10-10: 统一 clsx 引入方式

本文件使用默认导入 import clsx from 'clsx',而 BaseSelect 使用命名导入 import { clsx } from 'clsx'。为一致性与类型友好(TS/exports map),建议统一为命名导入:

-import clsx from 'clsx';
+import { clsx } from 'clsx';

15-19: SelectInputRef.nativeElement 可为 null,类型建议放宽

useImperativeHandle 初次建立时 rootRef.current 可能为 null。建议将类型放宽为可空,避免潜在的严格空检查报错:

-export interface SelectInputRef {
+export interface SelectInputRef {
   focus: (options?: FocusOptions) => void;
   blur: () => void;
-  nativeElement: HTMLDivElement;
+  nativeElement: HTMLDivElement | null;
 }

BaseSelect 中相应的 BaseSelectRef['nativeElement'] 也应放宽(见对 BaseSelect 的单独评论)。

Also applies to: 167-168

src/BaseSelect/index.tsx (2)

321-326: BaseSelectRef.nativeElement 可能为空;类型与实现建议放宽

getDOM(containerRef.current) 在未挂载或 SSR 场景可能返回 null。当前强制断言为 HTMLElement

  • 建议将 BaseSelectRef['nativeElement'] 放宽为 HTMLElement | null
  • 赋值时不做强断言,保持一致。

示例(接口与实现同步调整):

// interface:
export interface BaseSelectRef {
  focus: (options?: FocusOptions) => void;
  blur: () => void;
  scrollTo: ScrollTo;
  nativeElement: HTMLElement | null;
}

// useImperativeHandle:
nativeElement: getDOM(containerRef.current),

688-699: class 计算良好,但 clsx 使用风格需与其他文件一致

本文件采用命名导入 { clsx },SelectInput 采用默认导入。建议统一(推荐命名导入)。参见对 SelectInput 的建议。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e2ffc6 and d5f7692.

📒 Files selected for processing (2)
  • src/BaseSelect/index.tsx (23 hunks)
  • src/SelectInput/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/SelectInput/index.tsx (3)
src/hooks/useComponents.ts (1)
  • ComponentsConfig (5-8)
src/utils/keyUtil.ts (1)
  • isValidateOpenKey (4-45)
src/SelectInput/Affix.tsx (1)
  • Affix (8-16)
src/BaseSelect/index.tsx (9)
src/interface.ts (2)
  • RenderNode (22-22)
  • DisplayValueType (13-20)
src/hooks/useComponents.ts (2)
  • ComponentsConfig (5-8)
  • useComponents (19-42)
src/SelectInput/index.tsx (1)
  • SelectInputRef (15-19)
src/SelectTrigger.tsx (1)
  • RefTriggerProps (52-54)
src/OptionList.tsx (1)
  • RefOptionListProps (22-26)
src/hooks/useOpen.ts (1)
  • useOpen (36-93)
src/hooks/useSelectTriggerControl.ts (1)
  • useSelectTriggerControl (4-37)
src/hooks/useBaseProps.ts (1)
  • BaseSelectContext (16-16)
src/BaseSelect/Polite.tsx (1)
  • Polite (9-32)
🔇 Additional comments (2)
src/BaseSelect/index.tsx (2)

701-740: 属性透传与事件链路清晰,SelectInput 注入合理(小建议:onInputKeyDown 贯通)

当前通过 Context 包装了 onInternalInputKeyDown,对外部 onInputKeyDown 无需在此处额外传递。若需要让外部 onInputKeyDown 同时作用于输入框,可考虑在 SelectInput 内部 wrapper 中先调用外部回调后再进行内部开合逻辑(已基本符合)。无需改动,仅确认预期。


606-612: 全局点击关闭逻辑合理,定制 root 时按 customizedTrigger 分流

该段使用 useSelectTriggerControl 管理外部点击关闭且考虑了自定义 root 的接管,符合预期。

Comment on lines +215 to +224
if (RootComponent) {
if (React.isValidElement<any>(RootComponent)) {
return React.cloneElement(RootComponent, {
...domProps,
ref: composeRef((RootComponent as any).ref, rootRef),
});
}

return <RootComponent {...domProps} ref={rootRef} />;
}
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 | 🔴 Critical

components.root 导致结构和行为丢失(未渲染 Provider 与内容、事件未绑定)

当传入 components.root 时,当前实现直接返回自定义元素,未包裹 SelectInputContext.Provider,也未渲染 Prefix/Content/Suffix/Clear/children,更未绑定 onMouseDown/onBlur、className/style 等。这会使自定义 root 完全失去选择器的功能与语义,属于功能性缺陷。

建议:始终渲染 Provider;对自定义 root 仅替换“外层容器”而非整体结构;统一注入 props、ref 与事件,并在其 children 渲染原有内部内容。

可按以下方式重构该分支与默认分支合并为一套渲染路径:

@@
-  if (RootComponent) {
-    if (React.isValidElement<any>(RootComponent)) {
-      return React.cloneElement(RootComponent, {
-        ...domProps,
-        ref: composeRef((RootComponent as any).ref, rootRef),
-      });
-    }
-
-    return <RootComponent {...domProps} ref={rootRef} />;
-  }
-
-  return (
-    <SelectInputContext.Provider value={contextValue}>
-      <div
-        {...domProps}
-        // Style
-        ref={rootRef}
-        className={className}
-        style={style}
-        // Mouse Events
-        onMouseDown={onInternalMouseDown}
-        onBlur={onInternalBlur}
-      >
-        {/* Prefix */}
-        <Affix className={clsx(`${prefixCls}-prefix`, classNames?.prefix)} style={styles?.prefix}>
-          {prefix}
-        </Affix>
-
-        {/* Content */}
-        <SelectContent ref={inputRef} />
-
-        {/* Suffix */}
-        <Affix
-          className={clsx(
-            `${prefixCls}-suffix`,
-            {
-              [`${prefixCls}-suffix-loading`]: loading,
-            },
-            classNames?.suffix,
-          )}
-          style={styles?.suffix}
-        >
-          {suffix}
-        </Affix>
-        {/* Clear Icon */}
-        {clearIcon && (
-          <Affix
-            className={clsx(`${prefixCls}-clear`, classNames?.clear)}
-            style={styles?.clear}
-            onMouseDown={(e) => {
-              // Mark to tell not trigger open or focus
-              (e.nativeEvent as any)._select_lazy = true;
-              onClearMouseDown?.(e);
-            }}
-          >
-            {clearIcon}
-          </Affix>
-        )}
-        {children}
-      </div>
-    </SelectInputContext.Provider>
-  );
+  // Build root props used by both default div and custom RootComponent
+  const rootProps = {
+    ...domProps,
+    ref: rootRef,
+    className,
+    style,
+    onMouseDown: onInternalMouseDown,
+    onBlur: onInternalBlur,
+  };
+
+  const inner = (
+    <>
+      {/* Prefix */}
+      <Affix className={clsx(`${prefixCls}-prefix`, classNames?.prefix)} style={styles?.prefix}>
+        {prefix}
+      </Affix>
+      {/* Content */}
+      <SelectContent ref={inputRef} />
+      {/* Suffix */}
+      <Affix
+        className={clsx(
+          `${prefixCls}-suffix`,
+          { [`${prefixCls}-suffix-loading`]: loading },
+          classNames?.suffix,
+        )}
+        style={styles?.suffix}
+      >
+        {suffix}
+      </Affix>
+      {/* Clear Icon */}
+      {clearIcon && (
+        <Affix
+          className={clsx(`${prefixCls}-clear`, classNames?.clear)}
+          style={styles?.clear}
+          onMouseDown={(e) => {
+            (e.nativeEvent as any)._select_lazy = true;
+            onClearMouseDown?.(e);
+          }}
+        >
+          {clearIcon}
+        </Affix>
+      )}
+      {children}
+    </>
+  );
+
+  const rootElement = RootComponent
+    ? React.isValidElement<any>(RootComponent)
+      ? React.cloneElement(RootComponent, {
+          ...rootProps,
+          ref: composeRef((RootComponent as any).ref, rootRef),
+        }, inner)
+      : React.createElement(RootComponent as any, rootProps, inner)
+    : React.createElement('div', rootProps, inner);
+
+  return (
+    <SelectInputContext.Provider value={contextValue}>
+      {rootElement}
+    </SelectInputContext.Provider>
+  );

此改动恢复语义结构与交互,确保自定义 root 仅替换容器实现,且仍获得上下文和功能。

Also applies to: 226-276

🤖 Prompt for AI Agents
In src/SelectInput/index.tsx around lines 215-224 (and similarly lines 226-276),
returning a user-supplied components.root directly causes loss of Provider,
internal content (Prefix/Content/Suffix/Clear/children), and event bindings; fix
by always rendering the SelectInputContext.Provider and the internal structure,
using the supplied RootComponent only as the outer container: render
<RootComponent {...injectedProps} ref={composeRef((RootComponent as any).ref,
rootRef)}> and ensure injectedProps include all domProps, className, style,
onMouseDown, onBlur, other event handlers, aria attributes and merged refs, then
render the original internal elements (Prefix/Content/Suffix/Clear/children)
inside that container so the custom root replaces only the outer wrapper while
preserving context, behavior and events.

import { omit, useEvent } from '@rc-component/util';
import KeyCode from '@rc-component/util/lib/KeyCode';
import { isValidateOpenKey } from '../utils/keyUtil';
import clsx from 'clsx';
Copy link
Member

Choose a reason for hiding this comment

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

这里我统一用具名导入的:

Suggested change
import clsx from 'clsx';
import { clsx } from 'clsx';

@codecov
Copy link

codecov bot commented Oct 16, 2025

Codecov Report

❌ Patch coverage is 99.44598% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.49%. Comparing base (c2dc268) to head (ff308c8).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/BaseSelect/index.tsx 97.50% 1 Missing ⚠️
src/hooks/useSelectTriggerControl.ts 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1166      +/-   ##
==========================================
+ Coverage   98.11%   99.49%   +1.37%     
==========================================
  Files          39       31       -8     
  Lines        1489     1191     -298     
  Branches      452      399      -53     
==========================================
- Hits         1461     1185     -276     
+ Misses         28        6      -22     

☔ 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: 3

♻️ Duplicate comments (2)
src/SelectInput/Input.tsx (1)

181-185: 修复无效 ARIA 关联:id 缺失时不要输出 undefined_list

当未提供 id 时,aria-owns/aria-controls 会变成 undefined_list,应按条件设置以避免无效属性。

-    'aria-owns': `${id}_list`,
+    'aria-owns': id ? `${id}_list` : undefined,
@@
-    'aria-controls': `${id}_list`,
+    'aria-controls': id ? `${id}_list` : undefined,
src/SelectInput/index.tsx (1)

215-224: 修复自定义 Root 渲染丢失 Provider/结构/事件的问题:始终渲染 Provider 与内部结构

当前传入 components.root 时直接返回自定义元素,导致未渲染 SelectInputContext.Provider、Prefix/Content/Suffix/Clear/children,也未绑定必要事件与类名,功能缺失。

-  if (RootComponent) {
-    if (React.isValidElement<any>(RootComponent)) {
-      return React.cloneElement(RootComponent, {
-        ...domProps,
-        ref: composeRef((RootComponent as any).ref, rootRef),
-      });
-    }
-
-    return <RootComponent {...domProps} ref={rootRef} />;
-  }
-
-  return (
-    <SelectInputContext.Provider value={contextValue}>
-      <div
-        {...domProps}
-        // Style
-        ref={rootRef}
-        className={className}
-        style={style}
-        // Mouse Events
-        onMouseDown={onInternalMouseDown}
-        onBlur={onInternalBlur}
-      >
-        {/* Prefix */}
-        <Affix className={clsx(`${prefixCls}-prefix`, classNames?.prefix)} style={styles?.prefix}>
-          {prefix}
-        </Affix>
-
-        {/* Content */}
-        <SelectContent ref={inputRef} />
-
-        {/* Suffix */}
-        <Affix
-          className={clsx(
-            `${prefixCls}-suffix`,
-            {
-              [`${prefixCls}-suffix-loading`]: loading,
-            },
-            classNames?.suffix,
-          )}
-          style={styles?.suffix}
-        >
-          {suffix}
-        </Affix>
-        {/* Clear Icon */}
-        {clearIcon && (
-          <Affix
-            className={clsx(`${prefixCls}-clear`, classNames?.clear)}
-            style={styles?.clear}
-            onMouseDown={(e) => {
-              // Mark to tell not trigger open or focus
-              (e.nativeEvent as any)._select_lazy = true;
-              onClearMouseDown?.(e);
-            }}
-          >
-            {clearIcon}
-          </Affix>
-        )}
-        {children}
-      </div>
-    </SelectInputContext.Provider>
-  );
+  // 统一构建外层容器 props
+  const rootProps = {
+    ...domProps,
+    ref: rootRef,
+    className,
+    style,
+    onMouseDown: onInternalMouseDown,
+    onBlur: onInternalBlur,
+  };
+
+  const inner = (
+    <>
+      {/* Prefix */}
+      <Affix className={clsx(`${prefixCls}-prefix`, classNames?.prefix)} style={styles?.prefix}>
+        {prefix}
+      </Affix>
+      {/* Content */}
+      <SelectContent ref={inputRef} />
+      {/* Suffix */}
+      <Affix
+        className={clsx(
+          `${prefixCls}-suffix`,
+          { [`${prefixCls}-suffix-loading`]: loading },
+          classNames?.suffix,
+        )}
+        style={styles?.suffix}
+      >
+        {suffix}
+      </Affix>
+      {/* Clear Icon */}
+      {clearIcon && (
+        <Affix
+          className={clsx(`${prefixCls}-clear`, classNames?.clear)}
+          style={styles?.clear}
+          onMouseDown={(e) => {
+            (e.nativeEvent as any)._select_lazy = true;
+            onClearMouseDown?.(e);
+          }}
+        >
+          {clearIcon}
+        </Affix>
+      )}
+      {children}
+    </>
+  );
+
+  const rootElement = RootComponent
+    ? React.isValidElement<any>(RootComponent)
+      ? React.cloneElement(RootComponent, {
+          ...rootProps,
+          ref: composeRef((RootComponent as any).ref, rootRef),
+        }, inner)
+      : React.createElement(RootComponent as any, rootProps, inner)
+    : React.createElement('div', rootProps, inner);
+
+  return (
+    <SelectInputContext.Provider value={contextValue}>
+      {rootElement}
+    </SelectInputContext.Provider>
+  );

Also applies to: 226-276

🧹 Nitpick comments (3)
src/SelectInput/Input.tsx (2)

187-213: 合并自定义 Input 元素的 className 与 style,避免覆盖

当前 { ...sharedInputProps, ...existingProps } 会用自定义元素的 className/style 覆盖共享样式与类名,应显式合并。

-    const mergedProps = { ...sharedInputProps, ...existingProps };
+    const mergedProps = { ...sharedInputProps, ...existingProps };
+    // 合并样式与类名(existingProps 优先覆盖冲突样式)
+    mergedProps.className = clsx(sharedInputProps.className, existingProps.className);
+    mergedProps.style = { ...(sharedInputProps.style || {}), ...(existingProps.style || {}) };

4-4: 可选:使用项目内的 useLayoutEffect 包装以统一 SSR 行为

项目已有 src/hooks/useLayoutEffect 封装,建议统一使用以减少包分散与行为差异。

-import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
+import useLayoutEffect from '../hooks/useLayoutEffect';

Based on learnings

src/BaseSelect/index.tsx (1)

616-626: 避免上下文中 triggerOpen 命名歧义(布尔 vs. 函数)

BaseSelectContext 同时暴露 opentriggerOpen(布尔),以及 toggleOpen(函数)。triggerOpen 既作为布尔又在其他处作为函数使用,易引发理解与误用。

  • 保留 open: mergedOpentoggleOpen: triggerOpen
  • 移除或废弃 triggerOpen: mergedOpen,统一改为只读 open
  • 若需兼容,可暂时同时提供 triggerOpen(别名指向 mergedOpen),但标记为内部/待移除。

Also applies to: 631-641

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5f7692 and 1428565.

📒 Files selected for processing (4)
  • src/BaseSelect/index.tsx (23 hunks)
  • src/SelectInput/Content/SingleContent.tsx (1 hunks)
  • src/SelectInput/Input.tsx (1 hunks)
  • src/SelectInput/index.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/SelectInput/Content/SingleContent.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/SelectInput/index.tsx (4)
src/BaseSelect/index.tsx (3)
  • RenderNode (47-47)
  • DisplayValueType (43-43)
  • Mode (44-44)
src/hooks/useComponents.ts (1)
  • ComponentsConfig (5-8)
src/utils/keyUtil.ts (1)
  • isValidateOpenKey (4-45)
src/SelectInput/Affix.tsx (1)
  • Affix (8-16)
src/SelectInput/Input.tsx (3)
src/SelectInput/context.ts (1)
  • useSelectInputContext (8-10)
src/hooks/useBaseProps.ts (1)
  • useBaseProps (18-20)
src/hooks/useLayoutEffect.ts (1)
  • useLayoutEffect (8-16)
src/BaseSelect/index.tsx (8)
src/interface.ts (2)
  • RenderNode (22-22)
  • DisplayValueType (13-20)
src/hooks/useComponents.ts (2)
  • ComponentsConfig (5-8)
  • useComponents (19-42)
src/SelectInput/index.tsx (1)
  • SelectInputRef (15-19)
src/SelectTrigger.tsx (1)
  • RefTriggerProps (52-54)
src/OptionList.tsx (1)
  • RefOptionListProps (22-26)
src/hooks/useOpen.ts (1)
  • useOpen (36-93)
src/hooks/useSelectTriggerControl.ts (1)
  • useSelectTriggerControl (4-37)
src/BaseSelect/Polite.tsx (1)
  • Polite (9-32)

useBaseProps();

const rootRef = React.useRef<HTMLDivElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
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 | 🟠 Major

扩展 inputRef 类型以匹配 TextArea 判定

inputRef 仅声明为 HTMLInputElement,但后续使用了 instanceof HTMLTextAreaElement 判断;应扩大为联合类型以匹配真实节点。

-  const inputRef = React.useRef<HTMLInputElement>(null);
+  const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
@@
-      const isTextAreaElement = inputRef.current instanceof HTMLTextAreaElement;
+      const isTextAreaElement = inputRef.current instanceof HTMLTextAreaElement;

Also applies to: 129-129

🤖 Prompt for AI Agents
In src/SelectInput/index.tsx around lines 121 and 129, inputRef is declared as
React.useRef<HTMLInputElement>(null) but the code later checks for instanceof
HTMLTextAreaElement; change the ref generic to React.useRef<HTMLInputElement |
HTMLTextAreaElement | null>(null) (or the equivalent union) and update any
usages/casts to handle the nullable union (narrow with instanceof or null
checks) so the TypeScript types match the runtime checks.

Comment on lines +26 to +37
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const {
onChange,
onKeyDown,
onBlur,
style,
syncWidth,
value,
className,
autoComplete,
...restProps
} = props;
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 | 🟠 Major

转发缺失的 onFocus 事件

InputProps 定义了 onFocus,但未传递到实际 input,导致外部无法监听 focus。

 const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
   const {
     onChange,
     onKeyDown,
     onBlur,
+    onFocus,
     style,
     syncWidth,
     value,
     className,
     autoComplete,
     ...restProps
   } = props;
@@
     onChange: handleChange,
     onKeyDown: handleKeyDown,
     onBlur: handleBlur,
+    onFocus,
     onPaste: handlePaste,

Also applies to: 156-185

🤖 Prompt for AI Agents
In src/SelectInput/Input.tsx around lines 26 to 37 (and also apply the same fix
at lines 156 to 185), the onFocus prop declared in InputProps is being omitted
from the forwarded props so consumers cannot listen for focus; include onFocus
in the component's destructuring (or ensure it is forwarded via ...restProps)
and pass it down to the underlying <input> element (and to any other input-like
element in the later block) so the external onFocus handler is invoked.

Comment on lines +138 to +166
const [widthCssVar, setWidthCssVar] = React.useState<number | undefined>(undefined);

// When syncWidth is enabled, adjust input width based on content
useLayoutEffect(() => {
const input = inputRef.current;

if (syncWidth && input) {
input.style.width = '0px';
const scrollWidth = input.scrollWidth;
setWidthCssVar(scrollWidth);

// Reset input style
input.style.width = '';
}
}, [syncWidth, value]);

// ============================= Render =============================
// Extract shared input props
const sharedInputProps = {
id,
type: mode === 'combobox' ? 'text' : 'search',
...restProps,
ref: inputRef as React.Ref<HTMLInputElement>,
style: {
...styles?.input,
...style,
'--select-input-width': widthCssVar,
} as React.CSSProperties,
autoFocus,
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 | 🟠 Major

修复宽度同步的单位问题并在关闭时重置

CSS 变量需要带单位的字符串;同时当 syncWidth 关闭或无输入时应清理状态,避免遗留值。

-  const [widthCssVar, setWidthCssVar] = React.useState<number | undefined>(undefined);
+  const [widthCssVar, setWidthCssVar] = React.useState<string | undefined>(undefined);
@@
-  useLayoutEffect(() => {
+  useLayoutEffect(() => {
     const input = inputRef.current;
 
-    if (syncWidth && input) {
+    if (syncWidth && input) {
       input.style.width = '0px';
       const scrollWidth = input.scrollWidth;
-      setWidthCssVar(scrollWidth);
+      setWidthCssVar(`${scrollWidth}px`);
 
       // Reset input style
       input.style.width = '';
-    }
+    } else {
+      setWidthCssVar(undefined);
+    }
   }, [syncWidth, value]);
@@
-      '--select-input-width': widthCssVar,
+      '--select-input-width': widthCssVar,

Also applies to: 144-152

🤖 Prompt for AI Agents
In src/SelectInput/Input.tsx around lines 138 to 166 (also applies to lines
144-152), the width CSS variable is currently stored as a number and not cleared
when syncWidth is disabled; change the state to hold a string including units
(e.g., "123px") and set that string when measuring input.scrollWidth, and ensure
you reset/clear the state to undefined when syncWidth is false or when there is
no content so the CSS var is removed; update the style assignment to pass the
string CSS variable and keep the existing input.style.width reset logic.

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

🧹 Nitpick comments (1)
jest.config.js (1)

3-3: 建议将覆盖率收集设为可配置。

collectCoverage: true 强制在每次测试运行时收集覆盖率,这会显著降低开发时的测试速度。建议改为根据环境变量控制,仅在 CI 或显式请求时启用。

应用此差异使覆盖率收集可配置:

-  collectCoverage: true,
+  collectCoverage: process.env.CI === 'true' || process.env.COVERAGE === 'true',
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1428565 and 41ccbca.

📒 Files selected for processing (3)
  • jest.config.js (1 hunks)
  • src/hooks/useDelayReset.ts (0 hunks)
  • src/hooks/useLayoutEffect.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • src/hooks/useDelayReset.ts
  • src/hooks/useLayoutEffect.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
jest.config.js (1)

1-5: 确认 Jest 环境与转换配置
当前 jest.config.js 仅包含 setupFilesAfterEnv 与覆盖收集,请确认 rc-test 预设是否已包含 React+TypeScript 所需的:

  • testEnvironment: 'jsdom'
  • TypeScript/JSX 转换(如 babel-jest 或 ts-jest)

如未覆盖,可在 jest.config.js 中显式添加:

testEnvironment: 'jsdom',
transform: {
  '^.+\\.(ts|tsx)$': 'babel-jest',
},

执行 npm test 验证测试正常运行。

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/BaseSelect/index.tsx (1)

606-620: 修复:BaseSelectContext 中的 triggerOpen 被错误地赋值为布尔值

当前把 triggerOpen 设成了 mergedOpen(boolean)。下游如按上下文约定调用 triggerOpen(...) 将报错/失效。应传递触发函数本身。

建议修改如下:

   const baseSelectContext = React.useMemo<BaseSelectContextProps>(
     () => ({
       ...props,
       notFoundContent,
       open: mergedOpen,
-      triggerOpen: mergedOpen,
+      triggerOpen: triggerOpen,
       id,
       showSearch,
       multiple,
       toggleOpen: triggerOpen,
       showScrollBar,
       styles,
       classNames,
     }),
     [
       props,
       notFoundContent,
-      triggerOpen,
+      triggerOpen,
       id,
       showSearch,
       multiple,
-      mergedOpen,
+      mergedOpen,
       showScrollBar,
       styles,
       classNames,
     ],
   );

Also applies to: 623-631

🧹 Nitpick comments (8)
tests/BaseSelect.test.tsx (2)

12-19: 全局计时器启用/还原已在 describe 级别统一处理,建议移除用例内重复设置

已经在 beforeEach/afterEach 中统一 useFakeTimers/useRealTimers。单测内(本文件其他用例也有)再次调用会增加心智负担,可能导致计时器状态错乱。可依赖外层统一设置,去掉用例内重复 useFakeTimers/useRealTimers。


20-37: 避免在 JSDOM 中使用带 href 的 触发器

在某些环境下可能产生导航/警告。这里仅需交互触发,建议改为 button 或移除 href,降低测试偶发性。

可以如下调整:

-        getRawInputElement={() => (
-          <a className="trigger" href="#">
-            trigger
-          </a>
-        )}
+        getRawInputElement={() => (
+          <button type="button" className="trigger">trigger</button>
+        )}

Also applies to: 40-51

tests/Select.test.tsx (2)

845-874: 此处无需再次 useFakeTimers/useRealTimers

文件已在 beforeEach/afterEach 统一控制计时器,建议移除本用例内的重复设置,避免潜在的计时器状态干扰。


1860-1885: 点击窗口外关闭的用例:计时器 flush 足够,但可简化

已通过 runAllTimers flush 宏任务,建议依赖外层假定时器设置,移除本用例内的 useFakeTimers/useRealTimers 重复调用,代码更简洁。

Also applies to: 1878-1881

src/BaseSelect/index.tsx (4)

321-327: 避免 findDOMNode:直接使用 SelectInputRef.nativeElement

既然 SelectInputRef 已提供 nativeElement,可直接返回以避免额外依赖和潜在 SSR/严格模式警告。

可改为:

   React.useImperativeHandle(ref, () => ({
     focus: containerRef.current?.focus,
     blur: containerRef.current?.blur,
     scrollTo: (arg) => listRef.current?.scrollTo(arg),
-    nativeElement: getDOM(containerRef.current) as HTMLElement,
+    nativeElement: containerRef.current?.nativeElement as unknown as HTMLElement,
   }));

597-603: useSelectTriggerControl 的元素获取同样可直接用 nativeElement

减少一次 getDOM 调用,更直接也更可靠。

-    () => [getDOM(containerRef.current), triggerRef.current?.getPopupElement()],
+    () => [containerRef.current?.nativeElement, triggerRef.current?.getPopupElement()],

573-577: 条件判断冗余

&& triggerOpen 恒为真(始终存在函数),可移除以简化逻辑。

-    if (popupElement?.contains(target as HTMLElement) && triggerOpen) {
+    if (popupElement?.contains(target as HTMLElement)) {

33-39: 注释与 API 不一致:声明“移除 getInputElement/getRawInputElement”,但类型仍对外暴露并在内部使用

建议:

  • 立即为 getInputElement / getRawInputElement 标注 @deprecated,文档对齐迁移路径(统一到 components.input/components.root)。
  • 明确两者的优先级(当前 useComponents 中这两者会覆盖 components),并在下个大版本移除。

Also applies to: 160-165, 330-331, 344-347

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41ccbca and ff308c8.

📒 Files selected for processing (3)
  • src/BaseSelect/index.tsx (22 hunks)
  • tests/BaseSelect.test.tsx (3 hunks)
  • tests/Select.test.tsx (25 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/Select.test.tsx (2)
tests/shared/allowClearTest.tsx (1)
  • allowClearTest (5-51)
tests/utils/common.ts (4)
  • waitFakeTimer (138-148)
  • toggleOpen (22-31)
  • expectOpen (4-20)
  • keyDown (113-122)
tests/BaseSelect.test.tsx (1)
tests/utils/common.ts (1)
  • waitFakeTimer (138-148)
src/BaseSelect/index.tsx (8)
src/hooks/useComponents.ts (2)
  • ComponentsConfig (5-8)
  • useComponents (19-42)
src/SelectInput/index.tsx (1)
  • SelectInputRef (15-19)
src/SelectTrigger.tsx (1)
  • RefTriggerProps (52-54)
src/OptionList.tsx (1)
  • RefOptionListProps (22-26)
src/hooks/useOpen.ts (1)
  • useOpen (36-93)
src/hooks/useSelectTriggerControl.ts (1)
  • useSelectTriggerControl (4-37)
src/hooks/useBaseProps.ts (1)
  • BaseSelectContext (16-16)
src/BaseSelect/Polite.tsx (1)
  • Polite (9-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (8)
tests/BaseSelect.test.tsx (2)

142-165: 自定义 RootComponent 覆盖默认根节点的用例设计合理

校验了自定义 root 渲染且默认 .rc-select 不再出现,覆盖路径正确,能有效防回归。


167-192: 使用 React 元素作为 RootComponent 的用例也覆盖到位

同时验证了元素形态的 root,保证 clone/渲染路径正确。赞。

tests/Select.test.tsx (4)

38-45: 全局假定时器的设置/还原 OK

在 describe 中统一 useFakeTimers + clearAllTimers/useRealTimers,有助于稳定处理异步 UI。保持此模式。


54-67: 点击清除触发关闭事件的断言更贴近语义

从 clear 按钮交互验证 onPopupVisibleChange(false),与新版 DOM 结构保持一致。


1614-1620: suffix 与 loading 场景的断言覆盖充分

校验了单选与多选在 loading 时的后缀类名变化,匹配 PR 中从 suffixIcon → suffix 的迁移。

Also applies to: 1623-1635


2254-2255: 根容器 onClick 透传测试合理

改为点击 .rc-select 验证 onClick 透传,符合语义化结构调整后的事件委托。

src/BaseSelect/index.tsx (2)

681-693: className 组合符合新结构(focused/allow-clear/show-arrow/show-search 等)

命名与测试断言一致,行为明确。LGTM。


638-653: suffix 合并逻辑合理(支持函数/节点,兼容 suffixIcon)

对外支持函数签名 { searchValue, open, focused, showSearch, loading },满足自定义后缀的复杂场景。LGTM。

@zombieJ zombieJ merged commit 8cc7836 into master Oct 16, 2025
12 checks passed
@zombieJ zombieJ deleted the r-layout branch October 16, 2025 06:20
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.

3 participants