Skip to content

fix: Close nested Menus when hovering over other items - #4090

Open
mannycarrera4 wants to merge 16 commits into
Workday:supportfrom
mannycarrera4:mc-fix-nested-menu
Open

fix: Close nested Menus when hovering over other items#4090
mannycarrera4 wants to merge 16 commits into
Workday:supportfrom
mannycarrera4:mc-fix-nested-menu

Conversation

@mannycarrera4

@mannycarrera4 mannycarrera4 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes: #3333, #4119, #4118, #4117

Release Category

Components


Checklist

For the Reviewer

  • PR title is short and descriptive
  • PR summary describes the change (Fixes/Resolves linked correctly)
  • PR Release Notes describes additional information useful to call out in a release message or removed if not applicable
  • Breaking Changes provides useful information to upgrade to this code or removed if not applicable

Where Should the Reviewer Start?

Areas for Feedback? (optional)

  • Code
  • Documentation
  • Testing
  • Codemods

Testing Manually

Screenshots or GIFs (if applicable)

Thank You Gif (optional)

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a nested menu example with sibling submenus, nested selections, and selected-item feedback.
    • Added configurable menu list height with a viewport-friendly default.
  • Bug Fixes

    • Disabled items and submenu targets no longer respond to activation.
    • Submenus close appropriately when navigating away or opening another sibling submenu.
    • Focus resets to the first enabled item when reopening a menu.
  • Style

    • Improved border-radius consistency for nested menu containers.
  • Tests

    • Expanded coverage for keyboard, hover, focus, scrolling, and nested-menu behavior.

@mannycarrera4 mannycarrera4 added the ai author This pull request is primarily or entirely written by an agent, not a human. Review carefully. label Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The menu now closes stale sibling submenus, ignores disabled targets and items, restores focus after reopening, applies default list sizing and nested container styling, and adds nested-menu stories and tests.

Changes

Menu interaction behavior

Layer / File(s) Summary
Manage submenu visibility and disabled targets
modules/react/menu/lib/Submenu.tsx, modules/react/collection/lib/useListItemSelect.tsx, modules/react/menu/lib/MenuItem.tsx
Submenus close when their parent target changes. Disabled targets and items do not open, toggle, or select. Reopened single-select menus move focus away from disabled items.
Apply menu sizing and container styling
modules/react/menu/lib/MenuList.tsx, modules/react/menu/lib/MenuCard.tsx
MenuList forwards a default maxHeight of 60vh to ListBox. Nested list-box containers inherit the menu card border radius.
Add nested menu examples and interaction coverage
modules/react/menu/stories/examples/NestedSiblings.tsx, modules/react/menu/stories/Menu.stories.ts, modules/react/menu/spec/MenuDisabled.spec.tsx, cypress/component/Menu.spec.tsx
The new story demonstrates sibling submenu selection. Tests cover disabled items, list scrolling, sibling switching, keyboard focus transfer, focus recovery, and preserved dynamic nesting chains.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: sheelah, williamjstanton

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes disabled-item behavior, focus restoration, scrolling, card styling, and selection handling beyond the linked hover issue [#3333]. Separate unrelated fixes into focused pull requests, or link and document the additional issue requirements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes closing nested menus when hovering over other menu items, which matches the primary change.
Linked Issues check ✅ Passed The implementation closes an open submenu when hovering another target and adds sibling-menu regression coverage for issue [#3333].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 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.

@aek5ba aek5ba added this to Canvas Kit Aug 3, 2026
@aek5ba aek5ba moved this to 🆕 New in Canvas Kit Aug 3, 2026
@mannycarrera4
mannycarrera4 changed the base branch from master to support August 3, 2026 20:14
@mannycarrera4
mannycarrera4 marked this pull request as ready for review August 3, 2026 20:14
@mannycarrera4
mannycarrera4 requested a review from a team as a code owner August 3, 2026 20:14

@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 `@modules/react/menu/lib/Submenu.tsx`:
- Around line 94-98: Update the target ID resolution in Submenu’s
sibling-closing logic to fall back to the ID captured from the target event when
model.state.targetRef.current is unavailable. Keep the existing parentCursorId
comparison and model.events.hide() behavior, and add coverage using a target
component that does not forward its ref.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d15f24a6-71bf-4f9b-b890-ad9aca92fc1a

📥 Commits

Reviewing files that changed from the base of the PR and between 23b7a01 and 34a699d.

📒 Files selected for processing (4)
  • cypress/component/Menu.spec.tsx
  • modules/react/menu/lib/Submenu.tsx
  • modules/react/menu/stories/Menu.stories.ts
  • modules/react/menu/stories/examples/NestedSiblings.tsx

Comment on lines +94 to +98
const targetId = (model.state.targetRef.current as HTMLElement | null)?.getAttribute(
'data-id'
);
if (targetId && parentCursorId && parentCursorId !== targetId) {
model.events.hide();

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 | ⚡ Quick win

Do not require a forwarded ref to close a sibling submenu.

Line 94 derives targetId only from targetRef. A target component without ref forwarding can open on hover, but its targetRef remains unset. When the parent cursor moves to a sibling, this condition does not call hide().

Use the ID captured from the target event as a fallback. Add coverage with a non-ref-forwarding target component.

Proposed fix
-      const targetId = (model.state.targetRef.current as HTMLElement | null)?.getAttribute(
-        'data-id'
-      );
+      const targetId =
+        currentTargetIdRef.current ||
+        (model.state.targetRef.current as HTMLElement | null)?.getAttribute('data-id');
📝 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
const targetId = (model.state.targetRef.current as HTMLElement | null)?.getAttribute(
'data-id'
);
if (targetId && parentCursorId && parentCursorId !== targetId) {
model.events.hide();
const targetId =
currentTargetIdRef.current ||
(model.state.targetRef.current as HTMLElement | null)?.getAttribute('data-id');
if (targetId && parentCursorId && parentCursorId !== targetId) {
model.events.hide();
🤖 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 `@modules/react/menu/lib/Submenu.tsx` around lines 94 - 98, Update the target
ID resolution in Submenu’s sibling-closing logic to fall back to the ID captured
from the target event when model.state.targetRef.current is unavailable. Keep
the existing parentCursorId comparison and model.events.hide() behavior, and add
coverage using a target component that does not forward its ref.

@mannycarrera4 mannycarrera4 added the ready for review Code is ready for review label Aug 3, 2026

@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 `@modules/react/menu/spec/MenuDisabled.spec.tsx`:
- Around line 102-126: Extend the sibling submenu test around NestedSiblings to
cover opening the second submenu via hover, using the project’s established
hover event helper or event sequence instead of only fireEvent.click(thirdItem).
Assert that the previously opened Second submenu closes, Second Item becomes
aria-expanded="false", Third Item’s submenu appears, and the menu count remains
unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f89c24cc-b4db-42a6-921a-6b19f42c5dec

📥 Commits

Reviewing files that changed from the base of the PR and between 34a699d and 95b7599.

⛔ Files ignored due to path filters (1)
  • .cursor/debug-dea6ae.log is excluded by !**/*.log
📒 Files selected for processing (5)
  • modules/react/collection/lib/useListItemSelect.tsx
  • modules/react/menu/lib/MenuItem.tsx
  • modules/react/menu/lib/MenuList.tsx
  • modules/react/menu/lib/Submenu.tsx
  • modules/react/menu/spec/MenuDisabled.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/react/menu/lib/Submenu.tsx

Comment on lines +102 to +126
it('should close sibling submenu when another is opened by click', async () => {
render(<NestedSiblings />);

fireEvent.click(screen.getByRole('button', {name: 'Open Menu'}));
await screen.findByRole('menu');

const secondItem = screen.getByRole('menuitem', {name: 'Second Item'});
const thirdItem = screen.getByRole('menuitem', {name: 'Third Item'});

fireEvent.click(secondItem);
await waitFor(() => {
expect(screen.getByRole('menuitem', {name: 'Second: First Sub Item'})).toBeInTheDocument();
});
expect(secondItem).toHaveAttribute('aria-expanded', 'true');

fireEvent.click(thirdItem);
await waitFor(() => {
expect(
screen.queryByRole('menuitem', {name: 'Second: First Sub Item'})
).not.toBeInTheDocument();
});
expect(secondItem).toHaveAttribute('aria-expanded', 'false');
expect(screen.getByRole('menuitem', {name: 'Third: First Sub Item'})).toBeInTheDocument();
expect(screen.getAllByRole('menu')).toHaveLength(2);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Cover the hover interaction.

This test opens the sibling submenu with fireEvent.click(thirdItem). The stated regression occurs when the user hovers over another submenu target. A click-only test can pass while the hover handler leaves the first submenu open.

Proposed test update
-  it('should close sibling submenu when another is opened by click', async () => {
+  it('should close sibling submenu when another is opened by hover', async () => {
...
-    fireEvent.click(thirdItem);
+    fireEvent.mouseEnter(thirdItem);
📝 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
it('should close sibling submenu when another is opened by click', async () => {
render(<NestedSiblings />);
fireEvent.click(screen.getByRole('button', {name: 'Open Menu'}));
await screen.findByRole('menu');
const secondItem = screen.getByRole('menuitem', {name: 'Second Item'});
const thirdItem = screen.getByRole('menuitem', {name: 'Third Item'});
fireEvent.click(secondItem);
await waitFor(() => {
expect(screen.getByRole('menuitem', {name: 'Second: First Sub Item'})).toBeInTheDocument();
});
expect(secondItem).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(thirdItem);
await waitFor(() => {
expect(
screen.queryByRole('menuitem', {name: 'Second: First Sub Item'})
).not.toBeInTheDocument();
});
expect(secondItem).toHaveAttribute('aria-expanded', 'false');
expect(screen.getByRole('menuitem', {name: 'Third: First Sub Item'})).toBeInTheDocument();
expect(screen.getAllByRole('menu')).toHaveLength(2);
});
it('should close sibling submenu when another is opened by hover', async () => {
render(<NestedSiblings />);
fireEvent.click(screen.getByRole('button', {name: 'Open Menu'}));
await screen.findByRole('menu');
const secondItem = screen.getByRole('menuitem', {name: 'Second Item'});
const thirdItem = screen.getByRole('menuitem', {name: 'Third Item'});
fireEvent.click(secondItem);
await waitFor(() => {
expect(screen.getByRole('menuitem', {name: 'Second: First Sub Item'})).toBeInTheDocument();
});
expect(secondItem).toHaveAttribute('aria-expanded', 'true');
fireEvent.mouseEnter(thirdItem);
await waitFor(() => {
expect(
screen.queryByRole('menuitem', {name: 'Second: First Sub Item'})
).not.toBeInTheDocument();
});
expect(secondItem).toHaveAttribute('aria-expanded', 'false');
expect(screen.getByRole('menuitem', {name: 'Third: First Sub Item'})).toBeInTheDocument();
expect(screen.getAllByRole('menu')).toHaveLength(2);
});
🤖 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 `@modules/react/menu/spec/MenuDisabled.spec.tsx` around lines 102 - 126, Extend
the sibling submenu test around NestedSiblings to cover opening the second
submenu via hover, using the project’s established hover event helper or event
sequence instead of only fireEvent.click(thirdItem). Assert that the previously
opened Second submenu closes, Second Item becomes aria-expanded="false", Third
Item’s submenu appears, and the menu count remains unchanged.

@cypress

cypress Bot commented Aug 5, 2026

Copy link
Copy Markdown

Workday/canvas-kit    Run #11485

Run Properties:  status check passed Passed #11485  •  git commit 951039c661 ℹ️: Merge 32d5f34136668001736d36472399b185d678d900 into 48fb856670ed68ca18a457fa091c...
Project Workday/canvas-kit
Branch Review mc-fix-nested-menu
Run status status check passed Passed #11485
Run duration 02m 31s
Commit git commit 951039c661 ℹ️: Merge 32d5f34136668001736d36472399b185d678d900 into 48fb856670ed68ca18a457fa091c...
Committer Manuel Carrera
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 17
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 824
View all changes introduced in this branch ↗︎
UI Coverage  19.34%
  Untested elements 1570  
  Tested elements 374  
Accessibility  99.57%
  Failed rules  5 critical   5 serious   0 moderate   2 minor
  Failed elements 66  

manuel.carrera and others added 6 commits August 5, 2026 13:42
A prior commit accidentally left in fetch() calls to a local debug
ingest server on every menu click/render, plus a stray debug log file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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

Caution

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

⚠️ Outside diff range comments (1)
cypress/component/Menu.spec.tsx (1)

235-240: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add accessibility checks for each mounted story.

Both new story contexts mount an example without cy.checkA11y(). Add an accessibility test in each context.

  • cypress/component/Menu.spec.tsx#L235-L240: Add cy.checkA11y() for NestedSiblings.
  • cypress/component/Menu.spec.tsx#L328-L333: Add cy.checkA11y() for NestedDynamic.

As per coding guidelines, Cypress component tests must “include cy.checkA11y() for every mounted example.”

🤖 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 `@cypress/component/Menu.spec.tsx` around lines 235 - 240, Add cy.checkA11y()
to both mounted story contexts: cypress/component/Menu.spec.tsx lines 235-240
for NestedSiblings and lines 328-333 for NestedDynamic, ensuring each context’s
beforeEach accessibility-checks its mounted example.

Source: Coding guidelines

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

Outside diff comments:
In `@cypress/component/Menu.spec.tsx`:
- Around line 235-240: Add cy.checkA11y() to both mounted story contexts:
cypress/component/Menu.spec.tsx lines 235-240 for NestedSiblings and lines
328-333 for NestedDynamic, ensuring each context’s beforeEach
accessibility-checks its mounted example.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7951fd41-250f-43f8-a3cf-df70151812bb

📥 Commits

Reviewing files that changed from the base of the PR and between b316321 and 4de6bf4.

📒 Files selected for processing (6)
  • cypress/component/Menu.spec.tsx
  • modules/react/collection/lib/useListItemSelect.tsx
  • modules/react/menu/lib/MenuItem.tsx
  • modules/react/menu/lib/MenuList.tsx
  • modules/react/menu/lib/Submenu.tsx
  • modules/react/menu/spec/MenuDisabled.spec.tsx
💤 Files with no reviewable changes (3)
  • modules/react/collection/lib/useListItemSelect.tsx
  • modules/react/menu/lib/MenuList.tsx
  • modules/react/menu/lib/Submenu.tsx

@mannycarrera4 mannycarrera4 removed the ready for review Code is ready for review label Aug 10, 2026
@mannycarrera4 mannycarrera4 added the ready for review Code is ready for review label Aug 10, 2026
boxShadow: system.depth[3],
minWidth,
maxHeight,
maxHeight: cssVar(maxHeight, '60vh'),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

add a default value here

Comment on lines +12 to +18
if (!element) {
return false;
}
return (
element.getAttribute('aria-disabled') === 'true' ||
(element as HTMLButtonElement | HTMLInputElement).disabled === true
);

@RayRedGoose RayRedGoose Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (!element) {
return false;
}
return (
element.getAttribute('aria-disabled') === 'true' ||
(element as HTMLButtonElement | HTMLInputElement).disabled === true
);
return (
Boolean(element?.getAttribute('aria-disabled')) ||
(element as HTMLButtonElement | HTMLInputElement)?.disabled
);

@RayRedGoose RayRedGoose Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But honestly, I would change that function to be isElementActive with the next return:

return (
    element &&
    !Boolean(element?.getAttribute('aria-disabled')) &&
    !(element as HTMLButtonElement | HTMLInputElement)?.disabled
  );

it would be more precise checking and you will not need to have many returns in other functions

Comment on lines +27 to +31
if (isElementDisabled(event.currentTarget) || state.nonInteractiveIds.includes(name)) {
return null;
}
events.select({id: name});
return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you need anything to be returned here?

Comment on lines +119 to 123
if (isElementDisabled(event.currentTarget)) {
return;
}
currentTargetIdRef.current = event.currentTarget.getAttribute('data-id')!;
mouseEnterTimer.start();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do not check "If element is not disabled" and do not have return at all?

@aek5ba aek5ba moved this from 🆕 New to 🔖 Ready in Canvas Kit Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai author This pull request is primarily or entirely written by an agent, not a human. Review carefully. ready for review Code is ready for review v16-Fast-Follow

Projects

Status: 🔖 Ready

Development

Successfully merging this pull request may close these issues.

Nested menus remain open after hovering

4 participants