Skip to content

NMS-20273: Open side menu submenus on hover, in both rail states - #8831

Merged
synqotik merged 2 commits into
foundation-2026from
jira/NMS-20273-sidemenu-hover-flyout-when-collapsed-smoke
Sep 1, 2026
Merged

NMS-20273: Open side menu submenus on hover, in both rail states#8831
synqotik merged 2 commits into
foundation-2026from
jira/NMS-20273-sidemenu-hover-flyout-when-collapsed-smoke

Conversation

@synqotik

@synqotik synqotik commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

NMS-20273: Open side menu submenus on hover, in both rail states

Hovering a top-level entry in the side menu now flies its submenu out, instead of requiring a
click. This applies whether the rail is collapsed or pinned open.


Background

The rail used to expand on hover and open its flyouts (NMS-18287). NMS-20167 removed both,
because the combination was jarring, leaving click-to-open. That turned out to be dialed back too
far: the jarring part was the rail resizing from under the pointer — reflowing the page and moving
every icon the user was aiming at — not the flyouts themselves.

So the flyouts come back, and the rail's width does not move. isPinned is now the only thing that
sets it, and only the toggle button and Ctrl+\ change isPinned.

Behavior

  • Hovering a top-level entry opens its submenu after a 150ms dwell. The rail keeps its width.
  • Once the first flyout is open the rail is in hover mode: moving between entries switches flyouts
    instantly, with no further delay — the macOS menu-bar feel.
  • Leaving the rail closes any open flyout after a 200ms grace, and leaves hover mode, so
    re-entering needs a fresh dwell.
  • Identical in both rail states. Click-to-open still works and is unchanged; so is keyboard
    navigation.

The dwell is the point of the design. Opening on bare mouseenter fires a flyout at every entry
the pointer merely crosses on its way somewhere else, which is the same class of noise the old
hover mode had. 150ms is long enough to ignore a pass-through and short enough to read as instant
on a deliberate hover.

The close grace matters because a flyout is not always beside its own entry: positionFlyouts
clamps tall flyouts to the rail's vertical band, so reaching one can take the pointer briefly
outside the rail. Closing on the first mouseleave would cut that travel off.

Tooltips

While collapsed, top-level entries had a label tooltip, since their labels are hidden. Now:

  • Entries with a submenu no longer show one.
  • Entries without a submenu (Topology Map, Geographical Map — direct links, nothing to fly out)
    keep theirs, and its showDelay drops from 300ms to the same 150ms as the flyout, so every
    top-level entry responds on the same beat regardless of which kind it is.
  • Expanded, labels are visible and tooltips stay suppressed, as before.

Notes for reviewers

  • This reaches into TieredMenu internals — the dirty flag, and a synthetic event aimed at a
    PrimeVue-owned element. A PrimeVue upgrade could change either. It is contained to this one
    component, which already carries a comment explaining why TieredMenu is used unwrapped rather
    than behind an onms-ui seam component, now extended to cover this. The alternative — calling
    onItemChange() with a processed item pulled off the instance — couples to more internals, not
    fewer, and skips PrimeVue's own handler.
  • An expanded-rail flyout now closes when the pointer leaves the rail, where before it stayed
    until an outside click. Deliberate: the interaction should not differ between the two states.
  • Touch is unaffected. There is no hover to dwell on, and click-to-open is untouched.
  • The two timing constants are the only tuning knobs; both are named and commented at their
    definitions.

External References

Hovering a top-level entry now flies its submenu out instead of
requiring a click. What the rail never does on hover is change its own
width: an earlier iteration expanded it from under the pointer, and
that — rather than the flyouts — was the jarring part.

TieredMenu only opens submenus on hover once its internal `dirty` flag
is set, normally by a first click. Setting it the moment the pointer
touches the rail makes flyouts fire at every entry the pointer merely
passes over, so the first entry has to be dwelled on for 150ms: a
delegated mouseover starts the timer, and when it elapses we set
`dirty` and re-dispatch mouseenter on the item (TieredMenu binds it on
the item's content wrapper, and mouseenter does not bubble) so
PrimeVue's own handler does the opening. After that the rail is in
hover mode and TieredMenu switches between entries itself, with no
further dwell, until the pointer leaves. Leaving closes any open
flyout after a 200ms grace, since a clamped flyout can take the
pointer briefly outside the rail on its way there.

Collapsed, entries that are direct links (Topology, the maps) have no
submenu, so they get their label tooltip on the same 150ms beat
instead of the previous 300ms; entries that do have a submenu no
longer show one, the flyout being the affordance. Expanded, labels are
already visible and tooltips stay suppressed.

The tests render transitions for real: VTU stubs them by default,
which parks the root list's class on the stub rather than the <ul>, so
the '.p-tieredmenu-root-list > .p-tieredmenu-item' selector the rail
uses in production would match nothing.
@synqotik
synqotik marked this pull request as draft September 1, 2026 01:39
Three fixes, all in the hover teardown path, plus one shared helper for
it.

A pending dwell was not cancelled when the pointer moved onto the
rail's own chrome. closest() returns null for the toggle button and the
gap below the last entry, and the handler returned early without
clearing the timer or hoveredItem — so the timer's only guard
(hoveredItem !== item) still held and it fired, opening a flyout for an
entry the pointer had already left. Reaching for the collapse toggle
after hovering an entry is the common case. Clearing hoveredItem there
also re-arms the dwell when the pointer comes back to that same entry,
which the old code left inert.

togglePinned left `dirty` set, contradicting its own comment.
TieredMenu never clears it when the active path empties, which happens
whenever a direct link (Topology, the maps) is hovered in hover mode,
so the activeItemPath guard skipped hide() and the rail stayed in hover
mode across the toggle — every subsequent hover opening with no dwell.

The rail's mouseleave discarded keyboard state: hide() resets
focusedItemInfo, and it ran whether the flyout had been hovered open or
clicked/tabbed into. Hover never focuses the menubar (verified in a
browser: a hover leaves activeElement on <body>, a click moves it to
the menubar <ul>), so `focused` cleanly separates the two. When the
menu holds focus, the focused index is now put back on the root entry
that was open — where TieredMenu itself lands after closing a submenu.
A deeper position is deliberately not restored as-is: it points into a
submenu that is now closed, and TieredMenu would resolve the stale
index against the root list.

The close-and-leave-hover-mode logic was duplicated between
togglePinned and the mouseleave timer; both now call one closeFlyouts()
helper.

Reviewed and not changed: `hasSubmenu` in the #item slot. TieredMenuSub
binds it as :hasSubmenu="!!getItemProp(processedItem, 'items')" — the
double-bang is on the slot binding, so it is already a boolean and
matches the .d.ts. The two tooltip assertions use toBe(), which is
Object.is: an items array would not equal true, nor undefined false.
@synqotik
synqotik marked this pull request as ready for review September 1, 2026 02:04

@mershad-manesh mershad-manesh left a comment

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.

LGTM

@synqotik
synqotik merged commit 2541cd2 into foundation-2026 Sep 1, 2026
15 checks passed
@synqotik
synqotik deleted the jira/NMS-20273-sidemenu-hover-flyout-when-collapsed-smoke branch September 1, 2026 14:17
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.

2 participants