Skip to content

fix(core): a mouse slide across NavHeadingMenu or TabMenu items selects no text - #8802

Merged
cixzhang merged 2 commits into
mainfrom
fix/nav-menus-no-text-selection
Oct 28, 2026
Merged

cixzhang merged 2 commits into
mainfrom
fix/nav-menus-no-text-selection

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

User impact

Someone sliding a mouse from one NavHeadingMenu or TabMenu item to the next, which these menus track as a held pointer since #8791, selected the item text while holding. In an expanded SideNavHeading, which stays open after a pick, the selection stayed on the item the press began on. With this change no item text is selected. Caller content beside NavHeadingMenu's items stays selectable.

Change

  • userSelect: 'none' on NavHeadingMenuItem's row and on TabMenu's dropdown. DropdownMenu's and BreadcrumbItem's menus already do this. NavHeadingMenu's root is left alone, so caller content keeps the browser's selection (component:NavHeadingMenu FR8).
  • A real-Chromium spec, NavMenu/__tests__/NavMenusSlideSelection.a11y.chromium.spec.ts. A mouse presses the first item's text, slides to the second and lifts. The spec asserts that nothing is selected while held or after the lift, in TopNavHeading's and an expanded SideNavHeading's NavHeadingMenu, in TopNavMenu, and in TabMenu.
  • A patch changeset.

Evidence

The spec runs in Chrome 156 against the built Storybook at main (820769d) and at this head:

Menu main this PR
NavHeadingMenu in TopNavHeading held "Analytics", lifted "" held "", lifted ""
NavHeadingMenu in an expanded SideNavHeading held "Personal Account", lifted "Personal Account" held "", lifted ""
TopNavMenu held "", lifted "" (already none, kept as a guard) held "", lifted ""
TabMenu held "Analytics", lifted "" held "", lifted ""

Authority

family:action-groups FR1–FR2 (the slide is the gesture), component:NavHeadingMenu FR8, component:TabMenu FR7, and the DropdownMenu precedent. CodeBunny noted this on #8791 at a515c5f.

Testing

The new Chromium spec fails 3 of 4 on main and passes 4 of 4 here. NavMenu, TopNav and TabList unit tests: 288 pass. tsc, lint and the changeset check are clean.

…text

NavHeadingMenu, TopNavMenu and TabMenu take userSelect none on their menu,
as DropdownMenu's and BreadcrumbItem's menus do, so a held pointer sliding
across the items (family:action-groups) starts no text selection; in an
expanded SideNavHeading, which stays open after a pick, the selection used
to stay on the item the press began on. Follow-up to #8791.
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 28, 2026 12:14pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 28, 2026
@astracat-bot

astracat-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 28, 2026, 12:39 PM UTC

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/NavHeadingMenu 13.0KB 13.0KB 0B 3.3KB 3.3KB 0B
core/Tab 9.7KB 9.7KB 0B 4.1KB 4.1KB 0B
core/TabList 40.6KB 40.6KB 0B 7.3KB 7.3KB 0B
core/TabMenu 55.9KB 55.9KB +10B 10.2KB 10.2KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 35.4KB 35.4KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

Status: No visual change across 6 compared shot(s).


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

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

Changes requested: the fix stops a mouse slide from selecting the rows' text in all three menus, but it needs two adjustments first.

  • userSelect: 'none' on NavHeadingMenu's root also makes caller content inside the menu unselectable, which component:NavHeadingMenu FR8 leaves to the browser; putting it on NavHeadingMenuItem's row keeps the fix and the caller's text.
  • The new test only checks that the style is declared, so please add real-browser slide tests (like MenuPress.a11y.chromium.spec.ts) for NavHeadingMenu, TopNavMenu and TabMenu that assert no selection remains, as architecture:component-test-sufficiency INV3 asks.

[Automated review]

const styles = stylex.create({
root: {
// A held pointer slides across the items; it selects no text.
userSelect: 'none',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This root also holds caller content that is not an item, which component:NavHeadingMenu FR8 leaves to "that content's or the browser's own". user-select: none here resolves onto that content, so text a caller puts in the menu can no longer be selected or copied. Moving the declaration to NavHeadingMenuItem's root style keeps the slide free of selection (a press has to start on an item to be tracked) and leaves caller content alone.

<NavHeadingMenuItem label="Dashboard" onClick={() => {}} />
</NavHeadingMenu>,
);
expect(screen.getByRole('menu')).toHaveStyle({userSelect: 'none'});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This asserts the new StyleX declaration rather than the outcome; architecture:component-test-sufficiency lists that kind of test as not establishing sufficiency, and INV3 asks for evidence at a layer that reproduces the selection. A real-Chromium slide test (as in MenuPress.a11y.chromium.spec.ts) that asserts window.getSelection().isCollapsed after a mouse slide, for NavHeadingMenu, TopNavMenu and TabMenu, would fail on main and pass here.

… real-Chromium slide tests

- userSelect none moves from NavHeadingMenu's root to NavHeadingMenuItem,
  so caller content beside the items stays selectable (NavHeadingMenu FR8).
- TopNavMenu's change is dropped: its slide already selects no text.
- The jsdom style assertion is replaced by a real-Chromium spec: a mouse
  slide from the first item's text to the second selects nothing while held
  and after the lift, in TopNavHeading's and an expanded SideNavHeading's
  NavHeadingMenu, TopNavMenu and TabMenu. On main three of the four fail
  (the held selection, which an expanded SideNavHeading keeps after the
  lift); all pass here.
@cixzhang cixzhang changed the title fix(core): a mouse slide across a nav or tab menu's items selects no text fix(core): a mouse slide across NavHeadingMenu or TabMenu items selects no text Oct 28, 2026

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

This is ready to approve: putting user-select: none on NavHeadingMenuItem's row and TabMenu's dropdown stops a mouse slide from selecting item text while caller content beside NavHeadingMenu's items stays selectable, and the new Chromium spec fails on main and passes here. One optional follow-up: TopNavMenu items with onClick and no href still select text during a slide, which the spec's TopNavMenu case cannot catch because its story only has link rows, so consider restoring TopNavMenu's declaration with an action-row case or narrowing that case's claim to link rows.

[Automated review]

});
});

test('TopNavMenu', async ({page}) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This case passes on main as well, so it cannot catch the selection bug in TopNavMenu: core-topnavmenu--default has only link rows, where Chromium starts no selection, and first.x + 12 lands on TopNavMenu's 40 px icon cell rather than the label. TopNavMenu rows with onClick and no href still select text during a slide on this head. Consider restoring TopNavMenu's declaration with an action-row case, or naming this case (and the header's claim) as link rows only.

@cixzhang
cixzhang merged commit 21436d9 into main Oct 28, 2026
72 checks passed
@github-actions
github-actions Bot deleted the fix/nav-menus-no-text-selection branch October 31, 2026 06:09

This branch was successfully deployed

1 active deployment
Preview — a5b3d4bc Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant