Skip to content

fix: ensure distinct popup ids across menu instances - #888

Open
atharv-sys32 wants to merge 1 commit into
react-component:masterfrom
atharv-sys32:fix/distinct-popup-uuid
Open

fix: ensure distinct popup ids across menu instances#888
atharv-sys32 wants to merge 1 commit into
react-component:masterfrom
atharv-sys32:fix/distinct-popup-uuid

Conversation

@atharv-sys32

@atharv-sys32 atharv-sys32 commented Aug 5, 2026

Copy link
Copy Markdown

Problem

When two Menu instances render submenus, both popups get the same ARIA id. This breaks accessibility (duplicate IDs violate WCAG; screen readers and aria-controls/aria-labelledby references become ambiguous).

This is a follow-up on ant-design/ant-design#53707.

Root cause

In src/Menu.tsx, the menu uuid is:

const uuid = useId(id ? `rc-menu-uuid-${id}` : 'rc-menu-uuid');

rc-util's useId returns the passed id unchanged when it is a stable string (its useSSRContext logic does if (id) return id). So for every menu without an explicit id, uuid collapses to the constant string rc-menu-uuid and the generated popup ids collide:

rc-menu-uuid-admin-popup   (menu 1)
rc-menu-uuid-admin-popup   (menu 2)  ← duplicate

Solution

Derive the fallback uuid from a fresh useId() so every menu instance is unique:

const mergedUuid = useId();
const uuid = useId(id ? `rc-menu-uuid-${id}` : `rc-menu-uuid-${mergedUuid}`);

After the fix the popup ids are distinct per instance (rc-menu-uuid-:r0:-admin-popup vs rc-menu-uuid-:r3:-admin-popup).

Test

Added gives distinct popup ids to separate menu instances in tests/Menu.spec.tsx: renders two vertical menus with the same submenu keys, then asserts the two popup ids differ. It fails on the previous code and passes with the fix.

Snapshots were regenerated (-u) because data-menu-id / aria-controls embed the uuid, which now carries the test-id suffix under NODE_ENV=test.

Summary by CodeRabbit

  • Bug 修复

    • 修复开发环境中多个垂直菜单同时显示子菜单弹层时,弹层 id 可能重复的问题。
    • 现在每个菜单实例都会生成唯一的弹层标识,提升多菜单场景下的稳定性。
  • 测试

    • 新增多菜单实例的弹层标识唯一性验证。

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

@atharv-sys32 is attempting to deploy a commit to the afc163's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

菜单在未提供 id 时使用独立 UUID 生成弹层标识。新增测试验证两个开发环境中的菜单实例生成唯一的 popup id

Changes

菜单弹层标识

Layer / File(s) Summary
UUID 生成与唯一性验证
src/Menu.tsx, tests/Menu.spec.tsx
未提供 id 时,Menu 使用 mergedUuid 生成 UUID。新增测试验证两个菜单实例的 popup id 存在且互不重复。

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: emilyyyliu, afc163

Poem

小兔看菜单,
UUID 跳得欢。
两个弹层齐出现,
各自带着唯一牌。
测试收好环境,
开心蹦回草原。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了修复多个菜单实例生成重复弹层 ID 的主要变更。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Menu.tsx`:
- Around line 262-263: Update the UUID generation in Menu so id-less instances
receive distinct IDs that remain identical between server rendering and initial
client hydration, including on React versions below 18 where the compatibility
useId returns ssr-id. Preserve the caller-provided id behavior, or alternatively
raise the React peer requirement to 18+ and add regression coverage for legacy
React SSR hydration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54307688-14e2-4038-b984-4e762b83b208

📥 Commits

Reviewing files that changed from the base of the PR and between fa0ea16 and 3f50fe3.

⛔ Files ignored due to path filters (6)
  • tests/__snapshots__/Keyboard.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Menu.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/MenuItem.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Options.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/Responsive.spec.tsx.snap is excluded by !**/*.snap
  • tests/__snapshots__/SubMenu.spec.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • src/Menu.tsx
  • tests/Menu.spec.tsx

Comment thread src/Menu.tsx
Comment on lines +262 to +263
const mergedUuid = useId();
const uuid = useId(id ? `rc-menu-uuid-${id}` : `rc-menu-uuid-${mergedUuid}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 检查项目声明的 React peer range。
fd -a -t f -E node_modules -E dist -E build \
  'package.json|package-lock.json|yarn.lock|pnpm-lock.yaml' . \
  | xargs -r rg -n -C 5 '"peerDependencies"|"react"\s*:'

# 检查仓库中是否包含 useId 兼容实现。
fd -a -t f -E node_modules \
  'useId\.(js|ts|tsx)$' . \
  | xargs -r rg -n -C 8 'ssr-id|useCompatId|useOriginId|NODE_ENV.*test'

# 检查现有 SSR 或 hydration 测试。
rg -n -C 3 'renderToString|renderToPipeableStream|hydrateRoot' \
  --glob '*.{js,jsx,ts,tsx}' . || true

Repository: react-component/menu

Length of output: 606


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- package.json peer deps ---\n'
sed -n '80,95p' package.json || true

printf '\n--- locate Menu.tsx and relevant rc util/useId references ---\n'
fd -a 'Menu\.tsx$' .
printf '\n--- inspect Menu.tsx around lines 240-275 ---\n'
sed -n '240,275p' src/Menu.tsx || true

printf '\n--- search useId/useCompatId/ssr-id in tracked files ---\n'
git ls-files \
  | grep -E '(^|/)(package.json|node_modules/.*useId\.(js|ts|tsx)|node_modules/@rc-component/util/.*)$' \
  || true
rg -n -C 5 'useId|ssr-id|useCompatId|useOriginId' package.json src || true

Repository: react-component/menu

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- package.json peer deps ---\n'
sed -n '80,95p' package.json || true

printf '\n--- locate Menu.tsx and relevant rc util/useId references ---\n'
fd -a 'Menu\.tsx$' .
printf '\n--- inspect Menu.tsx around lines 240-275 ---\n'
sed -n '240,275p' src/Menu.tsx || true

printf '\n--- search useId/useCompatId/ssr-id in tracked files ---\n'
git ls-files \
  | grep -E 'useId\.(js|ts|tsx)$|`@rc-component/util`' \
  || true
rg -n -C 5 'useId|ssr-id|useCompatId|useOriginId' package.json src || true

Repository: react-component/menu

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package.json peer deps ---'
sed -n '80,95p' package.json || true

printf '%s\n' ''
printf '%s\n' '--- locate Menu.tsx and relevant rc util/useId references ---'
fd -a 'Menu\.tsx$' .
printf '%s\n' ''
printf '%s\n' '--- inspect Menu.tsx around lines 240-275 ---'
sed -n '240,275p' src/Menu.tsx || true

printf '%s\n' ''
printf '%s\n' '--- search useId/useCompatId/ssr-id in tracked files ---'
git ls-files \
  | grep -E 'useId\.(js|ts|tsx)$|`@rc-component/util`' \
  || true
rg -n -C 5 'useId|ssr-id|useCompatId|useOriginId' package.json src || true

Repository: react-component/menu

Length of output: 2993


🌐 Web query:

@rc-component/util useId ssr-id useCompatId useOriginId

💡 Result:

In the @rc-component/util library, the useId hook is a compatibility wrapper designed to manage ID generation consistently across environments, especially when React's native useId hook might not be available (e.g., in older React versions) or to handle specific synchronization requirements [1]. The implementation logic behind these functions is as follows: 1. useId (The Primary Hook): This is the main exported hook [1]. It acts as a gateway that checks if a native useOriginId is available [1]. If it is, it uses React's native useId [1]. If not, it falls back to useCompatId to provide a compatible ID generation mechanism [1]. It also prioritizes an externally provided id if passed by the developer, treating it as the "single source of truth," and returns a mock ID ('test-id') when running in test environments [1]. 2. useOriginId: This is an internal helper that resolves to React's native useId hook if it exists in the current environment [1]. It is used by useId to detect if the native capability is available [1]. 3. useCompatId: This is the fallback mechanism used when native useId is unavailable [1]. It generates IDs using a simple internal counter (starting with 'ssr-id' and incrementing via rc_unique_{number}) [1]. Because it relies on React.useEffect to initialize, this ID is only generated on the client side, meaning it may not match across server-side and client-side rendering [1]. 4. ssr-id: This is not a function, but a default value ('ssr-id') used by useCompatId during the initial render phase before the effect runs [1]. In practice, when using components from the Ant Design ecosystem (such as Tooltip, Select, or Menu), you import useId directly from @rc-component/util [2][3][4]. The hook handles the underlying logic of whether to use native features or the compatibility fallback automatically [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package.json full ---'
sed -n '1,140p' package.json || true

Repository: react-component/menu

Length of output: 2872


补齐旧版 React 的 SSR 唯一性。

当前 package.json 仍声明 react/react-dom >=16.9.0,但新增的 mergedUuid 在旧版 React 中会落入 @rc-component/util 的兼容 useId。该兼容路径 SSR 首屏返回 ssr-id,到客户端 useEffect 后才生成 rc_unique_*。两个无 idMenu 因此会在 SSR 和 hydration 初始输出中得到相同的 rc-menu-uuid-ssr-id,重复的弹层 ID 会影响 data-menu-idaria-controls 关系。如果这个 peer range 仍有效,请使用 SSR 可水合的实例唯一 ID;否则提升到 React 18+,并增加旧版 React/SSR 回归测试。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Menu.tsx` around lines 262 - 263, Update the UUID generation in Menu so
id-less instances receive distinct IDs that remain identical between server
rendering and initial client hydration, including on React versions below 18
where the compatibility useId returns ssr-id. Preserve the caller-provided id
behavior, or alternatively raise the React peer requirement to 18+ and add
regression coverage for legacy React SSR hydration.

Source: MCP tools

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.

1 participant