Skip to content

feat: Support custom rendering #792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Conversation

Meet-student
Copy link

@Meet-student Meet-student commented Aug 10, 2025

中文版模板 / Chinese template

🤔 This is a ...

  • 🆕 New feature
  • 🐞 Bug fix
  • 📝 Site / documentation improvement
  • 📽️ Demo improvement
  • 💄 Component style improvement
  • 🤖 TypeScript definition improvement
  • 📦 Bundle size optimization
  • ⚡️ Performance optimization
  • ⭐️ Feature enhancement
  • 🌐 Internationalization
  • 🛠 Refactoring
  • 🎨 Code style optimization
  • ✅ Test Case
  • 🔀 Branch merge
  • ⏩ Workflow
  • ⌨️ Accessibility improvement
  • ❓ Other (about what?)

🔗 Related Issues

💡 Background and Solution

  • The specific problem to be addressed.
  • List the final API implementation and usage if needed.
  • If there are UI/interaction changes, consider providing screenshots or GIFs.

📝 Change Log

Language Changelog
🇺🇸 English Support custom rendering
🇨🇳 Chinese Support custom rendering

Summary by CodeRabbit

  • 新功能
    • 增加 Menu 的 itemRender 属性,支持自定义菜单项、子菜单、分组与分割线的渲染;示例中新增演示项并展示将菜单项包装为外链的用法。
  • 文档
    • 在 API 文档中补充 itemRender 的说明与默认行为。
  • 测试
    • 新增测试覆盖自定义渲染场景(验证菜单项被包装为外链)。
  • 样式
    • 若干代码格式调整与内部重构,不影响外部行为。

Copy link

coderabbitai bot commented Aug 10, 2025

Walkthrough

为 Menu 增加通用的 itemRender 自定义渲染能力,并将该能力贯穿 Menu、SubMenu、MenuItem、MenuItemGroup、Divider、类型定义、示例、文档与测试,支持通过回调包裹或替换原始渲染节点。

Changes

Cohort / File(s) Summary
核心:Menu 与上下文
src/Menu.tsx, src/context/MenuContext.tsx, src/interface.ts
新增并暴露 itemRender 类型与 MenuProps.itemRender;将 itemRender 注入 MenuContext 并在 Menu 中解构转发;类型 ItemRenderType 与 ItemSharedProps.itemRender 添加。
菜单项渲染:Item / Group / SubMenu / Divider
src/MenuItem.tsx, src/MenuItemGroup.tsx, src/SubMenu/index.tsx, src/Divider.tsx
各组件从 props 与 MenuContext 合并 itemRender;在非 measure 分支调用 itemRender(originNode, { item, keys }),并使用返回节点替换原始渲染结果;引入 ItemType 用于 metadata typing。
工具与小改动
src/utils/nodeUtil.tsx, src/utils/commonUtil.ts
nodeUtil 中对额外内容包裹表达式简化为单行条件表达式(格式化);commonUtil 对象字面量换行,行为不变。
示例与文档
docs/examples/items.tsx, README.md
示例中新增 itemRender 用例(将 type 为 'item' 的节点包裹为外链)并在 items 中新增两个示例项;README 增加 itemRender API 文档。
测试
tests/MenuItem.spec.tsx
新增两项测试以验证 itemRender 能将 item 的 originNode 包裹为外链(注意测试存在重复插入)。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as 开发者
  participant Menu as Menu
  participant Context as MenuContext
  participant Parser as 解析/渲染管线
  participant Cmp as 组件(Item/SubMenu/Group/Divider)
  participant Fn as itemRender 函数

  Dev->>Menu: 提供 itemRender 与 items/children
  Menu->>Context: 将 itemRender 注入上下文
  Menu->>Parser: 构建节点树
  Parser->>Cmp: 生成 originNode
  alt mergedItemRender 存在
    Cmp->>Fn: itemRender(originNode, { item, keys })
    Fn-->>Cmp: customNode
    Cmp-->>Parser: 返回 customNode
  else 无 itemRender
    Cmp-->>Parser: 返回 originNode
  end
  Parser-->>Menu: 节点树完成
  Menu-->>Dev: 渲染完成的菜单
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
为 Menu.Item 提供可包裹的 render 能力(支持外层自定义节点)(#54289)
render 函数签名可接收 originNode 与 item(#54289)
在 items 配置中为单个 item 指定 render(逐项自定义)(#54289) items 层并未直接新增 per-item render 字段,当前实现通过 Menu/MenuContext 或组件 props 提供 itemRender;未见每个 item 对应的 render 字段解析与优先级示例。
文档补充新 API(#54289)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
SubMenu / MenuItemGroup / Divider 增加 itemRender 使用(src/SubMenu/index.tsx, src/MenuItemGroup.tsx, src/Divider.tsx 原 issue 主要聚焦 Menu.Item 的根节点可包裹需求;对子组件的泛化超出原描述,但属于一致性扩展。
重复新增同名测试用例(tests/MenuItem.spec.tsx 出现重复测试插入,属于测试文件编辑问题且与需求实现无直接新增价值。

Possibly related PRs

Suggested reviewers

  • zombieJ

Poem

小兔绕树寻渲染,原生 li 也能穿;
外层轻覆成链接,菜单自此任你玩。
回调穿针细又软,子树合唱也不散。 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

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

Copy link

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

🧹 Nitpick comments (2)
src/utils/nodeUtil.tsx (2)

55-56: prefixCls 可能为空时生成异常类名

prefixCls 未传入时,当前会生成 class="undefined-item-extra"。建议在渲染 extra 时判断 prefixCls 是否存在再拼接类名(见上方 diff 示例)。

请确认是否存在调用 parseItems 时未传入 prefixCls 的路径;必要时可为 prefixCls 提供默认值或在入口处断言。


27-29: 命名一致性:originNode vs originalNode

接口使用 originalNode,实现中变量命名为 originNode。建议统一命名,降低心智负担与搜索成本。

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c1b586d and 18cb788.

📒 Files selected for processing (4)
  • docs/examples/items.tsx (1 hunks)
  • src/interface.ts (1 hunks)
  • src/utils/nodeUtil.tsx (1 hunks)
  • tests/MenuItem.spec.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
tests/MenuItem.spec.tsx (1)
docs/examples/items.tsx (1)
  • Menu (7-83)
src/utils/nodeUtil.tsx (1)
src/MenuItem.tsx (1)
  • render (40-63)

Copy link

codecov bot commented Aug 11, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.72%. Comparing base (c1b586d) to head (02bad65).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #792   +/-   ##
=======================================
  Coverage   99.72%   99.72%           
=======================================
  Files          27       27           
  Lines         734      740    +6     
  Branches      198      201    +3     
=======================================
+ Hits          732      738    +6     
  Misses          2        2           

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

{originNode}
</a>
),
},
Copy link
Member

Choose a reason for hiding this comment

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

render 放 item 里意义不大,和直接写 label 没区别。用户期望的是可以在顶层统一配置 itemRender

Copy link
Member

Choose a reason for hiding this comment

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

<Menu itemRender={...} />

Copy link
Author

Choose a reason for hiding this comment

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

done

@Meet-student Meet-student requested review from afc163 and zombieJ August 17, 2025 14:27
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

🧹 Nitpick comments (1)
src/Menu.tsx (1)

65-66: 命名一致性与语义约束建议:originalNode -> originNode,并在实现上确保不替换

    • 命名建议:其他模块(如 utils/nodeUtil.tsx)普遍使用 originNode。为避免文档/类型注释不一致,建议统一为 originNode。功能无差异。
    • 语义约束:Overflow 会“原样”渲染子节点为 <ul> 的直接子元素。请确保 itemsRender 的应用点在 <li> 内部(例如包裹标题/内容区域),而不是替换 <li> 本身;否则会造成无效 DOM(<ul> 子元素必须是 <li>)并破坏可访问性与样式选择器。这一点需在 convertItemsToNodes/parseItems 的实现中得到保证。

    可以按如下方式微调类型签名中的参数名(仅注释层面的统一,不影响运行时):

    -  itemsRender?: (originalNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode;
    +  itemsRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode;
  • 📜 Review details

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

    💡 Knowledge Base configuration:

    • MCP integration is disabled by default for public repositories
    • Jira integration is disabled by default for public repositories
    • Linear integration is disabled by default for public repositories

    You can enable these sources in your CodeRabbit configuration.

    📥 Commits

    Reviewing files that changed from the base of the PR and between 02bad65 and d45697e.

    📒 Files selected for processing (4)
    • docs/examples/items.tsx (1 hunks)
    • src/Menu.tsx (4 hunks)
    • src/utils/nodeUtil.tsx (4 hunks)
    • tests/MenuItem.spec.tsx (1 hunks)
    🚧 Files skipped from review as they are similar to previous changes (3)
    • src/utils/nodeUtil.tsx
    • docs/examples/items.tsx
    • tests/MenuItem.spec.tsx
    🧰 Additional context used
    🧬 Code Graph Analysis (1)
    src/Menu.tsx (2)
    src/interface.ts (1)
    • ItemType (77-77)
    src/utils/nodeUtil.tsx (1)
    • parseItems (73-96)
    🔇 Additional comments (2)
    src/Menu.tsx (2)

    248-251: 从 props 解构 itemsRender ✅

    按需解构后向解析链路透传,符合预期使用方式。


    33-35: 验证通过:./SubMenu 中存在 SemanticName 类型导出及默认导出 SubMenu

    • src/SubMenu/index.tsx 第 25 行已声明 export type SemanticName = 'list' | 'listTitle'
    • 在同文件第 428 行已声明 export default SubMenu

    将 type-only 引入与默认导入分离是安全的,无需额外更改。

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

    ♻️ Duplicate comments (3)
    tests/MenuItem.spec.tsx (1)

    232-265: 补充对 DOM 语义与交互不回归的断言(与先前建议一致)

    当前仅断言 <a> 存在。为防回归,建议同时校验:

    • ul 的直接子节点均为 li
    • 自定义渲染后键盘/点击交互仍可触发;
    • 禁用态不受影响。

    示例补充(可直接追加在本测试用例末尾):

       const link = container.querySelector('a');
       expect(link).toHaveAttribute('href', 'https://ant.design');
    +
    +  // 结构语义:ul 的直接子节点应为 li
    +  const ul = container.querySelector('ul.rc-menu-root') || container.querySelector('ul');
    +  const directChildren = Array.from(ul!.children);
    +  directChildren.forEach(node => expect(node.tagName.toLowerCase()).toBe('li'));
    +
    +  // 可选:验证包装后键盘交互不回归(需要为某项传入 onClick)
    +  // const secondLi = container.querySelectorAll('li.rc-menu-item')[1];
    +  // fireEvent.keyDown(secondLi, { which: KeyCode.ENTER, keyCode: KeyCode.ENTER });
    +  // expect(......).toHaveBeenCalled();
    src/utils/nodeUtil.tsx (2)

    62-66: 避免在所有分支统一后置包装 originNode

    分支外层统一 return itemRender(originNode, opt) 会让 group/submenu/divider 也被外层包裹,风险更大。建议移除该段,仅在 MenuItem 分支内应用(见上方修改):

    -        if (typeof itemRender === 'function') {
    -          return itemRender(originNode, opt);
    -        }
    -        return originNode;
    +        return originNode;

    52-59: 将 itemRender 限定在“内容层”,保持

  • 为最外层,避免破坏语义

    当前对 originNode(实际是 <li>)整体做 itemRender 后置包装,允许外层包裹/替换,可能导致 ul > a > liul > div。建议仅对 <li> 内部内容进行定制:

    -        else {
    -          originNode = (
    -            <MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
    -              {label}
    -              {(!!extra || extra === 0) && (
    -                <span className={`${prefixCls}-item-extra`}>{extra}</span>
    -              )}
    -            </MergedMenuItem>
    -          );
    -        }
    +        else {
    +          // 仅对 li 的内容层应用 itemRender,确保最外层仍为 <li>
    +          const content = (
    +            <>
    +              {label}
    +              {(!!extra || extra === 0) && prefixCls && (
    +                <span className={`${prefixCls}-item-extra`}>{extra}</span>
    +              )}
    +            </>
    +          );
    +          originNode = (
    +            <MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
    +              {typeof itemRender === 'function' ? itemRender(content, opt as any) : content}
    +            </MergedMenuItem>
    +          );
    +        }

    要点:

    • 仅在 MenuItem 分支应用 itemRender
    • prefixCls 加保护,避免渲染出 undefined-item-extra
  • 🧹 Nitpick comments (3)
    tests/Responsive.spec.tsx (2)

    33-35: 用 Map 重置全局状态不错;补充类型声明以避免 TS 报错

    当前对 global.resizeProps 的读写在 TS 下可能触发 “Property 'resizeProps' does not exist on type 'Global'” 的类型错误。建议补充全局声明,或将变量挂在 globalThis 上。

    可在测试入口或此文件顶部追加类型声明(二选一):

    // 方案 A:全局声明
    declare global {
      // 注意与 jest 声明合并
      // eslint-disable-next-line no-var
      var resizeProps: Map<number, any> | undefined;
    }
    export {};

    或修改为:

    - global.resizeProps = new Map<number, any>();
    + (globalThis as any).resizeProps = new Map<number, any>();

    125-127: 更明确地触发首个 ResizeObserver 回调

    使用可选链和下标访问是安全的,但 [0] 语义略隐晦。建议解构以提升可读性并避免魔法数字。

    - getResizeProps()?.[0]?.onResize?.({}, document.createElement('div'));
    + const [containerRO] = getResizeProps() || [];
    + containerRO?.onResize?.({}, document.createElement('div'));
    docs/examples/items.tsx (1)

    31-34: 示例命名易混淆:label 为 “SubMenu” 但该项并非 SubMenu

    该项没有 children 且未声明 type: 'group'/'divider',按实现会被当作普通 item。为避免读者误解,建议更改为普通项的描述。

    -        label: 'SubMenu',
    +        label: 'Navigation Three',
    📜 Review details

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

    💡 Knowledge Base configuration:

    • MCP integration is disabled by default for public repositories
    • Jira integration is disabled by default for public repositories
    • Linear integration is disabled by default for public repositories

    You can enable these sources in your CodeRabbit configuration.

    📥 Commits

    Reviewing files that changed from the base of the PR and between b0829a4 and e9f46a0.

    📒 Files selected for processing (6)
    • docs/examples/items.tsx (1 hunks)
    • src/Menu.tsx (4 hunks)
    • src/utils/commonUtil.ts (2 hunks)
    • src/utils/nodeUtil.tsx (4 hunks)
    • tests/MenuItem.spec.tsx (1 hunks)
    • tests/Responsive.spec.tsx (2 hunks)
    🚧 Files skipped from review as they are similar to previous changes (1)
    • src/Menu.tsx
    🧰 Additional context used
    🧬 Code Graph Analysis (1)
    tests/MenuItem.spec.tsx (1)
    docs/examples/items.tsx (1)
    • Menu (7-88)

    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

    ♻️ Duplicate comments (4)
    src/utils/commonUtil.ts (2)

    4-8: 统一 itemRender 的类型签名以匹配 parseItems

    当前签名为 (originNode: React.ReactNode) => React.ReactElement,而 parseItems 透传的是 (originNode, item?: NonNullable<ItemType>) => React.ReactElement。为避免类型不一致导致的困惑或 TS 报错,建议在此处引入 ItemType 并统一签名(第二参数可选)。

    在选定行内修改签名:

     export function parseChildren(
       children: React.ReactNode | undefined,
       keyPath: string[],
    -  itemRender?: (originNode: React.ReactNode) => React.ReactElement,
    +  itemRender?: (originNode: React.ReactNode, item?: NonNullable<ItemType>) => React.ReactElement,
     ) {

    并在文件顶部补充类型引入(需在选定行之外修改):

    import type { ItemType } from '../interface';

    25-27: 避免在 children 路径直接应用 itemRender,防止破坏 ul/li 语义

    parseChildren 中直接对克隆后的子节点应用 itemRender,会让最终 DOM 结构出现 ul > <Wrapper> > li 甚至 ul > <Wrapper> 的情况,破坏列表语义、样式选择器与可访问性。建议不要在此层进行外层包装或替换。

    短期修复(移除该层的 itemRender 调用,仅返回克隆节点):

    -      if (typeof itemRender === 'function') {
    -        return itemRender(React.cloneElement(child, cloneProps));
    -      }
           return React.cloneElement(child, cloneProps);

    后续可通过在 MenuItem 内部内容层应用 itemRender(参考对 nodeUtil.tsx 的建议)。

    src/utils/nodeUtil.tsx (1)

    79-80: 与 parseChildren 的签名保持一致;避免在 children 路径透传 itemRender

    parseItemsitemRender 继续传入 parseChildren,而 children 路径的外层包装存在语义风险(见 commonUtil.ts 建议)。建议:即便保留 parseItems 的可选 itemRender 参数,也不要在 parseChildren 路径透传。

    在选定行内修改调用:

    -  itemRender?: (originNode: React.ReactNode, item?: NonNullable<ItemType>) => React.ReactElement,
    +  itemRender?: (originNode: React.ReactNode, item?: NonNullable<ItemType>) => React.ReactElement,
    @@
    -  return parseChildren(childNodes, keyPath, itemRender);
    +  return parseChildren(childNodes, keyPath);

    (配合 commonUtil.ts 的变更,children 路径不再应用 itemRender,结构更安全。)

    Also applies to: 95-96

    src/Menu.tsx (1)

    258-262: 修复 useMemo 依赖缺失 + 移除测量树 itemRender,避免双挂载与副作用

    问题:

    • 当前依赖数组未包含 itemRender,当 itemRender 发生变化时 childList 不会重算,导致 UI 过期。[高风险]
    • 测量树(隐藏节点)也应用了 itemRender,会造成用户自定义 wrapper 被挂载两次,带来副作用与性能开销。

    一并修改为:

       ] = React.useMemo(
         () => [
    -      parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemRender),
    -      parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemRender),
    +      parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemRender),
    +      // 测量树不应用 itemRender,避免双渲染与副作用
    +      parseItems(children, items, EMPTY_LIST, {}, prefixCls),
         ],
    -    [children, items, _internalComponents, prefixCls],
    +    // 可见列表依赖 itemRender,确保变更时重算
    +    [children, items, _internalComponents, prefixCls, itemRender],
       );

    效果:

    • 可见树随 itemRender 变更而正确重算;
    • 隐藏测量树不再执行 itemRender,避免双挂载与副作用。
    🧹 Nitpick comments (1)
    src/utils/nodeUtil.tsx (1)

    55-57: 避免生成 undefined-item-extra 类名

    prefixCls 为空或未传入时,${prefixCls}-item-extra 会变成 undefined-item-extra。建议加防护。

    可结合上一个修复一起改为:

    - {(!!extra || extra === 0) && (
    -   <span className={`${prefixCls}-item-extra`}>{extra}</span>
    - )}
    + {(!!extra || extra === 0) && prefixCls && (
    +   <span className={`${prefixCls}-item-extra`}>{extra}</span>
    + )}
    📜 Review details

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

    💡 Knowledge Base configuration:

    • MCP integration is disabled by default for public repositories
    • Jira integration is disabled by default for public repositories
    • Linear integration is disabled by default for public repositories

    You can enable these sources in your CodeRabbit configuration.

    📥 Commits

    Reviewing files that changed from the base of the PR and between e9f46a0 and 743803f.

    📒 Files selected for processing (3)
    • src/Menu.tsx (4 hunks)
    • src/utils/commonUtil.ts (2 hunks)
    • src/utils/nodeUtil.tsx (4 hunks)
    🧰 Additional context used
    🧬 Code Graph Analysis (2)
    src/Menu.tsx (2)
    src/interface.ts (1)
    • ItemType (77-77)
    src/utils/nodeUtil.tsx (1)
    • parseItems (73-96)
    src/utils/nodeUtil.tsx (2)
    src/interface.ts (1)
    • ItemType (77-77)
    src/utils/commonUtil.ts (1)
    • parseChildren (4-33)

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

    Caution

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

    ⚠️ Outside diff range comments (1)
    src/utils/nodeUtil.tsx (1)

    24-71: renderNodeWrapper 隐式 any 且闭包捕获 opt 不必要,建议直接下传 itemRender 保持类型与语义一致

    当前 renderNodeWrapper = node => ... 会产生隐式 any(在严格模式会导致构建失败),且通过闭包捕获 opt,屏蔽了子组件传入的第二个参数 item。直接把 itemRender 透传给子组件更简单、也更符合类型约定。

    应用如下精简与类型友好修改:

    -      const renderNodeWrapper = node => {
    -        return typeof itemRender === 'function' ? itemRender(node, opt as any) : node;
    -      };
    +      // 直接透传 itemRender,避免隐式 any 与不必要的闭包
                   <MergedMenuItemGroup
                     key={mergedKey}
                     {...restProps}
    -                itemRender={renderNodeWrapper}
    +                itemRender={itemRender}
                     title={label}
                   >
               return (
                 <MergedSubMenu
                   key={mergedKey}
                   {...restProps}
    -              itemRender={renderNodeWrapper}
    +              itemRender={itemRender}
                   title={label}
                 >
    -          return <MergedDivider key={mergedKey} {...restProps} itemRender={renderNodeWrapper} />;
    +          return <MergedDivider key={mergedKey} {...restProps} itemRender={itemRender} />;
               <MergedMenuItem
                 key={mergedKey}
                 {...restProps}
                 extra={extra}
    -            itemRender={renderNodeWrapper}
    +            itemRender={itemRender}
               >

    备注:若希望进一步收紧为“仅内容层渲染”,可参考另一个可选建议。

    ♻️ Duplicate comments (2)
    src/interface.ts (1)

    9-9: API 契约不一致:itemRender 缺少 item 参数,且与 PR 其他文件/文档不一致

    当前仅定义了单参数 (originNode) => ReactNode,而 PR 目标与文档示例均为 (originNode, item) => ReactNode。这会导致类型与实现、用法和文档三者不一致,增加使用方困惑,也无法把条目元数据传递给自定义渲染逻辑。

    此外,建议补充 JSDoc 约束“必须保留可访问性与菜单交互语义(focus、键盘、ARIA 等)”,并明确应包含 originNode 以避免破坏导航与选择逻辑。

    建议修改如下(并收窄第二参数类型,见下 diff):

     interface ItemSharedProps {
       ref?: React.Ref<HTMLLIElement | null>;
       style?: React.CSSProperties;
       className?: string;
    -  itemRender?: (originNode: React.ReactNode) => React.ReactNode;
    +  /**
    +   * 自定义渲染菜单条目的原始节点。
    +   * 必须保留可访问性与交互语义:不要移除可聚焦元素、键盘交互与 ARIA role;
    +   * 返回值应包含传入的 originNode,否则可能破坏键盘导航与选择逻辑。
    +   */
    +  itemRender?: (
    +    originNode: React.ReactNode,
    +    item: NonNullable<ItemType>
    +  ) => React.ReactNode;
     }

    为确保全局一致性,请同步更新所有调用点(如 MenuItem、Divider、SubMenu、MenuItemGroup 等)传递第二参数。

    请运行以下脚本检查仓库内调用签名是否一致(是否存在两参/单参混用):

    #!/bin/bash
    set -euo pipefail
    
    echo "== 定位 itemRender 定义与调用 =="
    rg -n -C2 --type ts --type tsx '\bitemRender\s*\(' || true
    
    echo "== 统计两参/一参调用(仅作参考,人工复核) =="
    rg -n -P --type ts --type tsx 'itemRender\s*\(\s*[^,()]+\s*,\s*[^)]+\)' || true
    rg -n -P --type ts --type tsx 'itemRender\s*\(\s*[^,()]+\s*\)' || true
    src/utils/nodeUtil.tsx (1)

    60-71: 可选:将 itemRender 限定在 MenuItem 的“内容层”,避免外层 li 被替换/包裹破坏语义与可访问性

    该建议与既有评论一致:把 render 能力限制在内容区,保持最外层仍为 <li>,避免破坏 ul > li 语义、ARIA、键盘导航与样式选择器。

    参考最小改动示例(只针对 MenuItem 分支,Group/SubMenu/Divider 不透传 itemRender):

    -        return (
    -          <MergedMenuItem
    -            key={mergedKey}
    -            {...restProps}
    -            extra={extra}
    -            itemRender={itemRender}
    -          >
    -            {label}
    -            {(!!extra || extra === 0) && prefixCls && (
    -              <span className={`${prefixCls}-item-extra`}>{extra}</span>
    -            )}
    -          </MergedMenuItem>
    -        );
    +        // 构造内容节点,供 itemRender 定制
    +        const content = (
    +          <>
    +            {label}
    +            {(!!extra || extra === 0) && prefixCls && (
    +              <span className={`${prefixCls}-item-extra`}>{extra}</span>
    +            )}
    +          </>
    +        );
    +
    +        return (
    +          <MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
    +            {typeof itemRender === 'function' ? itemRender(content, opt as any) : content}
    +          </MergedMenuItem>
    +        );

    若团队最终决定允许外层包裹/替换,建议至少在开发环境对非 <li> 顶层返回进行 warning,并在文档中明确风险与规避手段。

    🧹 Nitpick comments (7)
    src/SubMenu/index.tsx (2)

    410-411: 在 SubMenu 中对 “子列表” 进行变换可能遗漏条目元信息与可访问性约束

    若继续保留该能力,建议也传入第二参数(当前 SubMenu 的条目元数据 props 或其子集),并在文档中明确“不得移除/破坏子项的可聚焦元素与 ARIA 语义”。

    -  const childListNode = typeof itemRender === 'function' ? itemRender(childList) : childList;
    +  const childListNode =
    +    typeof itemRender === 'function' ? itemRender(childList, props as any) : childList;

    同时,建议在 InternalSubMenu 构造 listNode 后增加条目级 itemRender 钩子(见下条评审),统一行为面。


    334-369: (设计建议)在 listNode 级别支持与 MenuItem 一致的 itemRender 注入

    为达成“包裹整个 SubMenu 条目”的一致体验,建议在生成 listNode 后、_internalRenderSubMenuItem 之前插入与 MenuItem 同构的注入逻辑,并采用与 MenuItem 相同的“外层替换/内部注入”安全策略。示例(非精确 diff,仅思路):

    if (typeof props.itemRender === 'function') {
      const custom = props.itemRender(listNode, props as any);
      if (React.isValidElement(custom) && (custom.type === Overflow.Item || custom.type === 'li')) {
        listNode = custom;
      } else {
        listNode = React.cloneElement(listNode as React.ReactElement, undefined, custom);
      }
    }

    请同步在类型与文档中明确 SubMenu 的 itemRender 语义与 MenuItem/Divider 对齐。

    src/MenuItemGroup.tsx (1)

    66-67: 为回调补齐第二参数,并在文档中强调不可破坏子项可访问性

    建议最小改动如下:

    -      {typeof itemRender === 'function' ? itemRender(childList) : childList}
    +      {typeof itemRender === 'function' ? itemRender(childList, props as any) : childList}

    并在 README/TS 类型注释中补充“不得移除/破坏子项的 focus、键盘与 ARIA 语义”的契约说明。

    src/utils/nodeUtil.tsx (4)

    73-73: 为 extra 的类名添加 prefixCls 保护,避免渲染出 undefined-item-extra

    当未传递 prefixCls 时会得到不期望的类名前缀。

    建议:

    -            {(!!extra || extra === 0) && <span className={`${prefixCls}-item-extra`}>{extra}</span>}
    +            {(!!extra || extra === 0) && prefixCls && (
    +              <span className={`${prefixCls}-item-extra`}>{extra}</span>
    +            )}

    89-90: parseItems 与 convertItemsToNodes 的 itemRender 签名不一致

    这里把第二个参数标为可选,而上游 convertItemsToNodes 使用的是必填 NonNullable<ItemType>。为保持一致性与类型安全,建议统一为非可选。

    建议:

    -export function parseItems(
    +export function parseItems(
       children: React.ReactNode | undefined,
       items: ItemType[] | undefined,
       keyPath: string[],
       components: Components,
       prefixCls?: string,
    -  itemRender?: (originNode: React.ReactNode, item?: NonNullable<ItemType>) => React.ReactElement,
    +  itemRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactElement,
     ) {

    80-80: 使用 .filter(Boolean) 简化可读性

    当前 .filter(opt => opt) 可用更语义化写法替代,行为等价。

    -    .filter(opt => opt);
    +    .filter(Boolean);

    13-14: 请统一 itemRender 签名,保持接口与实现一致

    • tsconfig.json 中未启用 strictnoImplicitAnystrictFunctionTypes,当前默认配置下不存在隐式 any 或函数兼容性错误
    • src/interface.ts(9 行)中,itemRender?: (originNode: React.ReactNode) => React.ReactNode
    • src/Menu.tsx(162 行)及 src/utils/nodeUtil.tsx(13-14、89-90 行)中,均定义为
      (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactElement
    • parseChildren 定义在 src/utils/commonUtil.ts,无需在此额外透传 itemRender

    建议将接口声明改为双参数回调,或将实现调整为单参数,以消除潜在类型不一致风险。

    📜 Review details

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

    💡 Knowledge Base configuration:

    • MCP integration is disabled by default for public repositories
    • Jira integration is disabled by default for public repositories
    • Linear integration is disabled by default for public repositories

    You can enable these sources in your CodeRabbit configuration.

    📥 Commits

    Reviewing files that changed from the base of the PR and between 743803f and 281afbb.

    📒 Files selected for processing (8)
    • src/Divider.tsx (1 hunks)
    • src/Menu.tsx (4 hunks)
    • src/MenuItem.tsx (2 hunks)
    • src/MenuItemGroup.tsx (2 hunks)
    • src/SubMenu/index.tsx (2 hunks)
    • src/interface.ts (1 hunks)
    • src/utils/commonUtil.ts (1 hunks)
    • src/utils/nodeUtil.tsx (5 hunks)
    ✅ Files skipped from review due to trivial changes (1)
    • src/utils/commonUtil.ts
    🚧 Files skipped from review as they are similar to previous changes (1)
    • src/Menu.tsx
    🧰 Additional context used
    🧬 Code Graph Analysis (2)
    src/Divider.tsx (2)
    src/context/MenuContext.tsx (1)
    • MenuContext (66-66)
    src/context/PathContext.tsx (1)
    • useMeasure (15-17)
    src/utils/nodeUtil.tsx (1)
    src/interface.ts (1)
    • ItemType (78-78)
    🔇 Additional comments (1)
    src/utils/nodeUtil.tsx (1)

    61-63: 确认 Divider 是否需要支持 itemRender

    对 Divider 开放外层包装的意义有限,还可能引入无效交互与语义问题。建议评估是否移除,或仅支持内容层渲染。

    请确认产品设计是否要求 Divider 也支持自定义渲染;如不要求,建议去除以降低复杂度与风险。

    src/Divider.tsx Outdated
    Comment on lines 25 to 27
    if (typeof itemRender === 'function') {
    return itemRender(renderNode);
    }
    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    ⚠️ Potential issue

    安全地注入自定义渲染结果,避免破坏 UL/LI 结构

    将外层替换改为“仅当返回仍为 LI 时替换,否则作为 children 注入”。同时传入第二参数:

    -  if (typeof itemRender === 'function') {
    -    return itemRender(renderNode);
    -  }
    +  if (typeof itemRender === 'function') {
    +    const custom = itemRender(renderNode, { type: 'divider' } as any);
    +    if (React.isValidElement(custom) && custom.type === 'li') {
    +      return custom;
    +    }
    +    return React.cloneElement(renderNode as React.ReactElement, undefined, custom);
    +  }

    额外建议:在开发环境添加告警(warning),当返回根节点不是 <li> 时提示用户可能破坏语义。

    📝 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 (typeof itemRender === 'function') {
    return itemRender(renderNode);
    }
    if (typeof itemRender === 'function') {
    const custom = itemRender(renderNode, { type: 'divider' } as any);
    if (React.isValidElement(custom) && custom.type === 'li') {
    return custom;
    }
    return React.cloneElement(renderNode as React.ReactElement, undefined, custom);
    }

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

    ♻️ Duplicate comments (3)
    src/utils/nodeUtil.tsx (3)

    36-41: 谨慎:对 Group 最外层应用 itemRender 可能破坏 DOM 语义/可访问性(重复提醒)

    在 Group 最外层传递 itemRender,若内部实现允许替换/包裹外层节点,可能导致 <ul> 的直接子节点不再是 <li>,从而破坏语义、样式选择器与键盘导航等。这与以 <li> 为交互根节点的约束冲突。

    建议在 Group 层不暴露外层 render,保持结构稳定,仅对内容层或标题提供更细粒度的 render(例如 titleRender)。最小化修改如下:

                   <MergedMenuItemGroup
                     key={mergedKey}
                     {...restProps}
    -                itemRender={renderNodeWrapper}
                     title={label}
                   >

    如果团队坚持允许外包裹/替换,建议在开发环境对顶层非 <li> 或未包含原节点时发出 warning,并在文档中明确风险与规避手段。


    49-54: 谨慎:对 SubMenu 最外层应用 itemRender 存在同样语义风险(重复提醒)

    与 Group 相同,若 SubMenu 外层支持被替换/包裹,可能破坏 <li> 作为直接子节点的语义与交互。

    建议移除最外层的 itemRender 传递,或改为仅对标题/内容层提供专用 render:

                 <MergedSubMenu
                   key={mergedKey}
                   {...restProps}
    -              itemRender={renderNodeWrapper}
                   title={label}
                 >

    66-75: 将 itemRender 限定在 MenuItem“内容层”,避免替换/包裹最外层

  • (重复提醒)

    当前将 itemRender 作为 prop 传入 MergedMenuItem,若其实现允许替换外层,会破坏 <ul>/<li> 语义与交互。建议仅对内容进行定制,外层 <li> 固定由 MergedMenuItem 承载。

    最小改法(仅在内容层应用 render,且为 extra 的 className 加保护):

    -          <MergedMenuItem
    -            key={mergedKey}
    -            {...restProps}
    -            extra={extra}
    -            itemRender={renderNodeWrapper}
    -          >
    -            {label}
    -            {(!!extra || extra === 0) && <span className={`${prefixCls}-item-extra`}>{extra}</span>}
    -          </MergedMenuItem>
    +          // 构造内容节点并仅在内容层应用 render,保持 <li> 作为最外层
    +          <MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
    +            {renderNodeWrapper(
    +              <>
    +                {label}
    +                {(!!extra || extra === 0) && prefixCls && (
    +                  <span className={`${prefixCls}-item-extra`}>{extra}</span>
    +                )}
    +              </>,
    +            )}
    +          </MergedMenuItem>
  • 🧹 Nitpick comments (3)
    src/utils/nodeUtil.tsx (3)

    24-26: 为 renderNodeWrapper 增加显式类型,避免隐式 any 并提高可读性

    目前参数与返回值均为隐式 any/unknown。建议标注为 ReactNode -> ReactNode。

    -      const renderNodeWrapper = node => {
    -        return typeof itemRender === 'function' ? itemRender(node, opt as any) : node;
    -      };
    +      const renderNodeWrapper = (node: React.ReactNode): React.ReactNode => {
    +        return typeof itemRender === 'function' ? itemRender(node, opt as any) : node;
    +      };

    62-62: Divider 暴露 itemRender 的价值存疑

    分割线通常不承载交互与内容,开放 itemRender 可能引入非预期的结构替换与样式问题。建议先不暴露,保持最简化。

    -          return <MergedDivider key={mergedKey} {...restProps} itemRender={renderNodeWrapper} />;
    +          return <MergedDivider key={mergedKey} {...restProps} />;

    73-73: prefixCls 为空时会产生 undefined-item-extra 类名

    当未传入 prefixCls 时,${prefixCls}-item-extra 会变为 undefined-item-extra。已在上条建议的重构中加了保护;如暂不调整结构,至少先加保护。

    -            {(!!extra || extra === 0) && <span className={`${prefixCls}-item-extra`}>{extra}</span>}
    +            {(!!extra || extra === 0) && prefixCls && (
    +              <span className={`${prefixCls}-item-extra`}>{extra}</span>
    +            )}
    📜 Review details

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

    💡 Knowledge Base configuration:

    • MCP integration is disabled by default for public repositories
    • Jira integration is disabled by default for public repositories
    • Linear integration is disabled by default for public repositories

    You can enable these sources in your CodeRabbit configuration.

    📥 Commits

    Reviewing files that changed from the base of the PR and between 281afbb and a438a2e.

    📒 Files selected for processing (1)
    • src/utils/nodeUtil.tsx (5 hunks)
    🧰 Additional context used
    🧬 Code Graph Analysis (1)
    src/utils/nodeUtil.tsx (1)
    src/interface.ts (1)
    • ItemType (78-78)
    🔇 Additional comments (2)
    src/utils/nodeUtil.tsx (2)

    42-43: 递归向子项传递 itemRender 👍

    对子树递归传递 itemRender 的修正到位,保证自定义渲染在子节点生效。


    55-55: 递归传递 itemRender 到 SubMenu 子项 👍

    对子树的继续传递是正确的。

    src/Menu.tsx Outdated
    @@ -157,6 +158,8 @@ export interface MenuProps
    _internalComponents?: Components;

    popupRender?: PopupRender;

    itemRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode;
    Copy link
    Member

    @zombieJ zombieJ Aug 20, 2025

    Choose a reason for hiding this comment

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

    新的规范第二个参数是 info:

    type XXXX = (
      origin: ReactElement,
      info: {
        item: ItemType,
        keys: string[],
        components: {}, // 如果需要透出内部组件的时候用
      }
    ) => ReactNode;

    参考一下这个:
    https://ant-design-next.pages.dev/components/menu-cn#menu-demo-custom-popup-render

    Copy link
    Author

    Choose a reason for hiding this comment

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

    done

    @Meet-student Meet-student requested a review from zombieJ August 20, 2025 14:31
    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

    ♻️ Duplicate comments (3)
    src/interface.ts (1)

    148-151: info.item 收窄为非空并收敛返回类型到 ReactNode

    与调用方约定保持一致:info.item 实际不会为 null,返回类型使用 React.ReactNode 即可覆盖 ReactElement。这有助于提示正确用法,减少不必要的联合类型。

     export type ItemRenderType = (
       node: React.ReactElement | React.ReactElement<any, string | React.JSXElementConstructor<any>>[],
    -  info: { item: ItemType; keys: string[] },
    -) => React.ReactNode | React.ReactElement;
    +  info: { item: NonNullable<ItemType>; keys: string[] },
    +) => React.ReactNode;
    src/Menu.tsx (1)

    254-263: 不在 parseItems 中传入 itemRender,避免测量树双渲染与副作用,赞

    测量树和可见树均不传 itemRender,把自定义渲染下沉到具体条目组件(Item/Group/Divider/SubMenu)层面,既避免了重复挂载与副作用问题,也保持了渲染路径的一致性。依赖数组移除 itemRender 也合理。

    src/MenuItem.tsx (1)

    207-256: 严重:直接用 itemRender 替换根节点会破坏 ul>li 语义、rc-overflow 测量与可访问性;且 meta.item 构造不准确

    问题简述:

    • DOM 语义与布局:当前将 renderNode = mergedItemRender(renderNode, ...),若用户返回 <Link>{originNode}</Link> 等外层包裹,会产生 <ul><Link><li/></Link></ul> 的非法结构,并破坏 rc-overflow 对直接子项的测量/收拢。
    • 可访问性/交互流失:根 <li>(由 LegacyMenuItem/Overflow.Item 提供的 role、tabIndex、onKeyDown/onClick 等)可能被替换导致键盘与选择逻辑失效。
    • 类型不符:meta.item 通过 { type: 'item', ...props } as ItemType 伪造了 type 字段;按常规类型定义,MenuItem 并不携带 type: 'item'

    建议:

    • 将自定义渲染“下沉”为 LI 内部内容:把 itemRender 的输入改为“内部内容节点”(children + Icon),输出作为 <LegacyMenuItem> 的 children 注入,保留 LI/Overflow.Item 作为根,确保语义与测量不变。
    • meta.item 直接传递 props as MenuItemType,不再构造 type: 'item'。如需区分类型,可在 meta 增加自定义的 kind: 'item' 字段(不与类型系统的 discriminant 冲突)。

    按此思路可在本段直接改造如下:

    -  let renderNode: React.ReactElement = (
    -    <LegacyMenuItem
    -      ref={legacyMenuItemRef}
    -      elementRef={mergedEleRef}
    -      role={role === null ? 'none' : role || 'menuitem'}
    -      tabIndex={disabled ? null : -1}
    -      data-menu-id={overflowDisabled && domDataId ? null : domDataId}
    -      {...omit(restProps, ['extra'])}
    -      {...activeProps}
    -      {...optionRoleProps}
    -      component="li"
    -      aria-disabled={disabled}
    -      style={{
    -        ...directionStyle,
    -        ...style,
    -      }}
    -      className={classNames(
    -        itemCls,
    -        {
    -          [`${itemCls}-active`]: active,
    -          [`${itemCls}-selected`]: selected,
    -          [`${itemCls}-disabled`]: mergedDisabled,
    -        },
    -        className,
    -      )}
    -      onClick={onInternalClick}
    -      onKeyDown={onInternalKeyDown}
    -      onFocus={onInternalFocus}
    -    >
    -      {children}
    -      <Icon
    -        props={{
    -          ...props,
    -          isSelected: selected,
    -        }}
    -        icon={mergedItemIcon}
    -      />
    -    </LegacyMenuItem>
    -  );
    -
    -  if (typeof mergedItemRender === 'function') {
    -    renderNode = mergedItemRender(renderNode, {
    -      item: {
    -        type: 'item',
    -        ...props,
    -      } as ItemType,
    -      keys: connectedKeys,
    -    }) as React.ReactElement;
    -  }
    +  // 1) 先构造“内部内容”的原始节点
    +  const originInnerNode: React.ReactNode = (
    +    <>
    +      {children}
    +      <Icon
    +        props={{
    +          ...props,
    +          isSelected: selected,
    +        }}
    +        icon={mergedItemIcon}
    +      />
    +    </>
    +  );
    +
    +  // 2) 允许 itemRender 定制“内部内容”,而非替换根节点
    +  const contentNode: React.ReactNode =
    +    typeof mergedItemRender === 'function'
    +      ? mergedItemRender(originInnerNode, {
    +          item: props as MenuItemType,
    +          keys: connectedKeys,
    +        })
    +      : originInnerNode;
    +
    +  // 3) 保持 <li>/Overflow.Item 为根,确保 ul>li 语义、测量与可达性不变
    +  let renderNode: React.ReactElement = (
    +    <LegacyMenuItem
    +      ref={legacyMenuItemRef}
    +      elementRef={mergedEleRef}
    +      role={role === null ? 'none' : role || 'menuitem'}
    +      tabIndex={disabled ? null : -1}
    +      data-menu-id={overflowDisabled && domDataId ? null : domDataId}
    +      {...omit(restProps, ['extra'])}
    +      {...activeProps}
    +      {...optionRoleProps}
    +      component="li"
    +      aria-disabled={disabled}
    +      style={{
    +        ...directionStyle,
    +        ...style,
    +      }}
    +      className={classNames(
    +        itemCls,
    +        {
    +          [`${itemCls}-active`]: active,
    +          [`${itemCls}-selected`]: selected,
    +          [`${itemCls}-disabled`]: mergedDisabled,
    +        },
    +        className,
    +      )}
    +      onClick={onInternalClick}
    +      onKeyDown={onInternalKeyDown}
    +      onFocus={onInternalFocus}
    +    >
    +      {contentNode}
    +    </LegacyMenuItem>
    +  );

    补充验证建议:

    • 用例覆盖:
      • 断言 DOM 结构满足 ul > li,无 ul > *:not(li)
      • 包裹 <Link>/<Upload> 后,回车键仍触发 onItemClick,鼠标点击整行(将 Link 样式设为 display:block)仍工作。
      • 水平/垂直溢出场景中 rc-overflow 的测量、折叠行为不受影响。
    🧹 Nitpick comments (7)
    src/context/MenuContext.tsx (1)

    57-58: 为 itemRender 增补契约说明,防止误用破坏可访问性与结构

    建议在上下文属性处补充 JSDoc,明确约束与语义,降低误用风险(如返回非 <li> 根导致 <ul>/<li> 结构失效,或移除可聚焦元素影响键盘导航)。

    可在当前位置追加注释:

    -  itemRender?: ItemRenderType;
    +  /**
    +   * 自定义渲染单个条目或其子列表。
    +   * 注意:
    +   * - 请保留 <li> 的语义结构与可访问性(role、tabIndex、键盘交互等)。
    +   * - Divider / Item / SubMenu 等应返回以 <li> 为根的节点;Group 的入参为“子列表数组”。
    +   * - 尽量避免带副作用的渲染(测量树不会调用 itemRender)。
    +   */
    +  itemRender?: ItemRenderType;
    src/interface.ts (1)

    12-13: 在公共类型处补充 JSDoc,统一语义且提示使用约束

    作为公开 API 的一部分,建议在 ItemSharedProps.itemRender 上补充契约说明,和上下文中的注释保持一致,提醒用户保留 <li> 语义,避免破坏可访问性。

     interface ItemSharedProps {
       ref?: React.Ref<HTMLLIElement | null>;
       style?: React.CSSProperties;
       className?: string;
    -  itemRender?: ItemRenderType;
    +  /** 
    +   * 自定义渲染单个条目或其子列表。
    +   * - Item/Divider/SubMenu:应返回以 <li> 为根的节点;
    +   * - Group:第一个参数为组内子项的数组;
    +   * - 请保留可访问性与交互语义(role、键盘操作等)。
    +   */
    +  itemRender?: ItemRenderType;
     }
    src/Menu.tsx (1)

    161-164: 公开 itemRender API 与 Context 透传对齐,但建议补充使用约束注释

    Props 与 Context 一致传递 itemRender,设计清晰。建议在 Props 处补充与 interface/Context 同步的 JSDoc,强调保持 <li> 语义,避免用户返回非 <li> 根节点:

       popupRender?: PopupRender;
    -  itemRender?: ItemRenderType;
    +  /**
    +   * 自定义渲染 originNode 与条目信息。
    +   * - Divider / Item / SubMenu:请返回以 <li> 为根的节点;
    +   * - Group:第一个参数为子项数组;
    +   * - 建议避免副作用渲染(测量树不会调用该函数)。
    +   */
    +  itemRender?: ItemRenderType;

    Also applies to: 664-665

    src/MenuItemGroup.tsx (2)

    55-59: Group 级别的 itemRender 用法与其他项存在语义差异,建议在文档与类型注释中明确

    这里将第一个参数作为“子项列表(ReactElement[])”传入,而 Item/Divider 等传入的是整个 <li> 节点。虽然 ItemRenderType 通过联合类型技术上兼容,但对用户心智不统一。

    建议:

    • 在接口与文档明确:Group 的 itemRender 第一个参数是“子项数组”,用于对子项进行变换;
    • 或引入更明确的命名(如 childrenRender/itemsRender)以区分用途(如不考虑短期 API 变更,可先通过 JSDoc 强化说明)。

    65-76: 为 info.item 透出完整分组元数据的做法合理,但可避免不必要的类型断言

    as ItemType 的断言可通过将字面量完善为 MenuItemGroupType 来规避,从而减少断言:

    示例(仅表达意图):

    • 使用明确的局部对象 const meta: MenuItemGroupType = { type: 'group', ...omit(props, ['itemRender']) }
    • 再传入 meta 而非断言为 ItemType

    非阻断,按需优化。

    src/MenuItem.tsx (2)

    15-15: 类型导入 OK,但后续不建议强行构造 ItemType

    后文你通过人为添加 type: 'item' 来断言为 ItemType(见本评审中对 247–256 行的评论)。按 rc-menu/antd 既有类型约定,MenuItem 项通常没有 type: 'item' 的区分字段。建议将 meta.item 明确为 MenuItemType,不再伪造 type 字段。若采纳该调整,本行可去掉对 ItemType 的导入。


    118-119: 建议合并策略支持“先 context 后 props”的函数组合,而不是二选一

    当前 propItemRender || contextItemRender 会忽略其一。更通用的做法是将二者串联:context 先包一层,props 再二次包裹,便于应用级别与组件级别同时生效。

    可在原地替换为下面的实现:

    -  const mergedItemRender = propItemRender || contextItemRender;
    +  const mergedItemRender = React.useMemo(() => {
    +    if (propItemRender && contextItemRender) {
    +      return (node: React.ReactNode, meta: { item: MenuItemType | null; keys: React.Key[] }) =>
    +        propItemRender(contextItemRender(node, meta), meta);
    +    }
    +    return propItemRender || contextItemRender;
    +  }, [propItemRender, contextItemRender]);
    📜 Review details

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

    💡 Knowledge Base configuration:

    • MCP integration is disabled by default for public repositories
    • Jira integration is disabled by default for public repositories
    • Linear integration is disabled by default for public repositories

    You can enable these sources in your CodeRabbit configuration.

    📥 Commits

    Reviewing files that changed from the base of the PR and between a438a2e and 8993061.

    📒 Files selected for processing (10)
    • docs/examples/items.tsx (1 hunks)
    • src/Divider.tsx (1 hunks)
    • src/Menu.tsx (5 hunks)
    • src/MenuItem.tsx (5 hunks)
    • src/MenuItemGroup.tsx (2 hunks)
    • src/SubMenu/index.tsx (3 hunks)
    • src/context/MenuContext.tsx (2 hunks)
    • src/interface.ts (2 hunks)
    • src/utils/nodeUtil.tsx (1 hunks)
    • tests/MenuItem.spec.tsx (1 hunks)
    🚧 Files skipped from review as they are similar to previous changes (4)
    • src/utils/nodeUtil.tsx
    • docs/examples/items.tsx
    • src/SubMenu/index.tsx
    • tests/MenuItem.spec.tsx
    🧰 Additional context used
    🧬 Code Graph Analysis (5)
    src/context/MenuContext.tsx (1)
    src/interface.ts (1)
    • ItemRenderType (148-151)
    src/MenuItemGroup.tsx (4)
    src/context/PathContext.tsx (2)
    • useFullPath (22-29)
    • useMeasure (15-17)
    src/utils/commonUtil.ts (1)
    • parseChildren (4-30)
    src/context/MenuContext.tsx (1)
    • MenuContext (69-69)
    src/interface.ts (1)
    • ItemType (81-81)
    src/Divider.tsx (3)
    src/interface.ts (1)
    • MenuDividerType (77-79)
    src/context/MenuContext.tsx (1)
    • MenuContext (69-69)
    src/context/PathContext.tsx (2)
    • useMeasure (15-17)
    • useFullPath (22-29)
    src/MenuItem.tsx (2)
    src/context/MenuContext.tsx (1)
    • MenuContext (69-69)
    src/interface.ts (1)
    • ItemType (81-81)
    src/Menu.tsx (1)
    src/interface.ts (1)
    • ItemRenderType (148-151)
    🔇 Additional comments (7)
    src/context/MenuContext.tsx (1)

    12-13: 类型引入合理,和接口对齐

    ItemRenderType 作为类型导入,避免运行时依赖,符合最佳实践。

    src/Menu.tsx (1)

    31-32: 类型导入与拆分引入方式合理

    • ItemRenderType 作为类型导入,避免运行时负担。
    • SubMenu 的类型与默认导入拆分,利于 tree-shaking 与类型检查。

    Also applies to: 34-35

    src/Divider.tsx (1)

    6-6: 引入 useFullPath 获取 keyPath 的做法正确

    keys 能用于自定义渲染的上下文信息(如埋点、定位),与整体方案一致。

    src/MenuItemGroup.tsx (1)

    6-6: 类型导入补充 ItemType,便于为 info.item 正确定型

    info.item 提供显式类型有助于避免 any 逃逸。

    src/MenuItem.tsx (3)

    92-93: props 侧 itemRender 命名为 propItemRender 👍

    从 props 解构并重命名,便于与 context 中的同名项合并使用,方向正确。


    115-116: 上下文注入 itemRender 👍

    从 MenuContext 读取 itemRender 以支持全局/父级定制,设计符合预期。


    118-119: 确认 itemRender 二参签名一致

    已验证:

    • src/interface.tsItemRenderType 定义为 (node, info) 两参签名
    • src/context/MenuContext.tsxitemRender?: ItemRenderType
    • 所有调用点均传入两个参数:
      • src/SubMenu/index.tsxitemRender(childList, { item, keys })
      • src/MenuItem.tsxmergedItemRender(renderNode, { item, keys })

    无隐式 any 或类型断言导致的签名漂移问题。

    Comment on lines +30 to +37
    if (typeof mergedItemRender === 'function') {
    return mergedItemRender(renderNode, {
    item: { type: 'divider', ...props },
    keys: connectedKeyPath,
    });
    }

    return renderNode;
    Copy link

    Choose a reason for hiding this comment

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

    🛠️ Refactor suggestion

    防止用户返回非

  • 根节点导致 UL/LI 结构失效,建议安全注入策略

    当前直接返回 mergedItemRender 的结果,若用户返回 <div> 或 Fragment,将导致 <ul> 子元素不是 <li> 的非法结构,破坏语义与可访问性。

    建议:若返回值是 <li> 则直接使用;否则作为 children 注入到现有 <li> 中,并在开发环境给出告警。

    -  if (typeof mergedItemRender === 'function') {
    -    return mergedItemRender(renderNode, {
    -      item: { type: 'divider', ...props },
    -      keys: connectedKeyPath,
    -    });
    -  }
    +  if (typeof mergedItemRender === 'function') {
    +    const custom = mergedItemRender(renderNode, {
    +      item: { type: 'divider', ...props },
    +      keys: connectedKeyPath,
    +    });
    +    if (React.isValidElement(custom) && (custom as any).type === 'li') {
    +      return custom as React.ReactElement;
    +    }
    +    if (process.env.NODE_ENV !== 'production') {
    +      // 返回的根节点不是 <li>,可能破坏 UL/LI 结构
    +      // eslint-disable-next-line no-console
    +      console.warn('[rc-menu] itemRender for Divider should return <li> root. Fallback to inject as children.');
    +    }
    +    return React.cloneElement(renderNode as React.ReactElement, undefined, custom);
    +  }
    📝 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 (typeof mergedItemRender === 'function') {
    return mergedItemRender(renderNode, {
    item: { type: 'divider', ...props },
    keys: connectedKeyPath,
    });
    }
    return renderNode;
    if (typeof mergedItemRender === 'function') {
    const custom = mergedItemRender(renderNode, {
    item: { type: 'divider', ...props },
    keys: connectedKeyPath,
    });
    if (React.isValidElement(custom) && (custom as any).type === 'li') {
    return custom as React.ReactElement;
    }
    if (process.env.NODE_ENV !== 'production') {
    // 返回的根节点不是 <li>,可能破坏 UL/LI 结构
    // eslint-disable-next-line no-console
    console.warn(
    '[rc-menu] itemRender for Divider should return <li> root. ' +
    'Fallback to inject as children.'
    );
    }
    return React.cloneElement(
    renderNode as React.ReactElement,
    undefined,
    custom
    );
    }
    return renderNode;
    🤖 Prompt for AI Agents
    In src/Divider.tsx around lines 30 to 37, the code currently returns
    mergedItemRender output directly which can break UL/LI semantics if the user
    returns a non-<li> element; change the flow so you inspect the returned React
    element: if it's an <li> (or a ReactElement with type 'li') return it as-is;
    otherwise render a safe <li> wrapper and inject the returned value as its
    children (preserving props, keys and connectedKeyPath) and in development mode
    emit a console.warn explaining that non-<li> output was wrapped to preserve list
    semantics and accessibility.
    
  • 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.

    Menu 组件 item 希望支持类似 table column 的 render 方法
    3 participants