Skip to content

fix(header): open hover menus from pointerenter instead of mouseenter - #883

Draft
mcstover wants to merge 1 commit into
mainfrom
fix-header-drop-down-trigger-improvements
Draft

fix(header): open hover menus from pointerenter instead of mouseenter#883
mcstover wants to merge 1 commit into
mainfrom
fix-header-drop-down-trigger-improvements

Conversation

@mcstover

Copy link
Copy Markdown
Collaborator

Problem

Hover-triggered header menus (avatar/MyKiva, Lend, About, Take action) were gated on navigator.maxTouchPoints:

if (!navigator.maxTouchPoints) { /* open menu */ }

Browsers report that value inconsistently for identical hardware — Chrome and Firefox can disagree on the same machine — so on whichever browser reports a non-zero count, the hover menus silently stop opening altogether.

maxTouchPoints is a raw hardware capability count, not a statement about how the user is currently interacting. It was standing in for a question it can't answer.

Approach

Listen on pointerenter/pointerleave and act only when event.pointerType === 'mouse'.

The reason this works is in how browsers sequence a tap. Dispatching a real touch and logging every event gives:

pointerover(touch)  pointerenter(touch)  pointerdown(touch)  touchstart
pointerup(touch)  pointerleave(touch)  touchend
mouseover  mouseenter  mousedown  mouseup  click     <- compatibility events

There is exactly one pointerenter, and it always carries pointerType: "touch". The compatibility events browsers synthesize afterwards are plain MouseEvents with no pointerType — so they never reach a pointerenter listener at all. A tap that just closed a menu can't be reopened by the synthetic event trailing it, with no timers and no device sniffing.

Because this discriminates per event rather than per device, hybrid touch-and-mouse machines (Surface, touchscreen laptops) get working hover and working taps — a device-level check can't do both.

Changes

  • KvHeaderDropdownLink.vue — filters on pointerType before emitting on-hover, so the emitted contract is unchanged for both header hosts. Shared by both headers, covers Lend / About / Take action.
  • KvWwwHeader/KvHeaderLinkBar.vue and KvWwwHeaderBasic/LinkBar.vue — hamburger and avatar triggers filter via handlePointerHover / handlePointerOut; both maxTouchPoints guards removed. handleTouchStart and the tap paths are untouched.
  • tests/unit/jest-setup.js — jsdom ships no PointerEvent, so fireEvent.pointerEnter() silently drops pointerType. Adds a minimal MouseEvent-backed stand-in.

Tests

New KvHeaderLinkBar.spec.ts (the component had no spec) plus additions to LinkBar.spec.ts, covering: a real mouse hover opens the menu; a bare mouseenter (what a browser fires after a tap) does not; a touch-type pointerenter does not; tapping still opens.

Verified these fail before the fix — the compatibility-mouseenter test reproduces the double-toggle on main — and that removing the pointerType guards fails 4 of them.

npm run test (lint + jest): 65 suites, 553 tests passing.

Verification notes

The event sequence above was captured from Chrome via CDP with real dispatched touch events, since JS-dispatched events don't generate compatibility mouse events. Worth a confirmation pass in Firefox on a machine that reproduced the original bug before this comes out of draft.

Hover-triggered header menus were gated on `navigator.maxTouchPoints`, which
browsers report inconsistently for identical hardware — Chrome and Firefox can
disagree on the same machine, so whichever reported a non-zero count silently
lost hover entirely.

Listen on `pointerenter`/`pointerleave` and act only when `pointerType` is
`mouse`. The compatibility mouse events a browser synthesizes after a tap are
plain MouseEvents, so they never reach a pointer listener at all — a tap that
just closed a menu can no longer be reopened by the synthetic event trailing
it. This discriminates per event rather than per device, so hybrid
touch-and-mouse machines get working hover and working taps.

KvHeaderDropdownLink filters before emitting `on-hover`, keeping its contract
unchanged; the hamburger and avatar triggers filter in the header components.

jsdom ships no PointerEvent, so jest-setup provides a MouseEvent-backed
stand-in that preserves `pointerType`.
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