Closed Bug 2062197 Opened 12 days ago Closed 2 days ago

theladcollective.com - Navigation dropdown stays open after the pointer leaves it interfering with the photo gallery

Categories

(Web Compatibility :: Site Reports, defect, P3)

Desktop
Windows 10

Tracking

(Webcompat Priority:P3, Webcompat Score:3, firefox156 fixed)

RESOLVED FIXED
156 Branch
Webcompat Priority P3
Webcompat Score 3
Tracking Status
firefox156 --- fixed

People

(Reporter: rbucata, Assigned: emilio)

References

()

Details

(Keywords: webcompat:needs-diagnosis, webcompat:site-report, Whiteboard: [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success], [wptsync upstream])

User Story

autowebcompat-repro-status:success
autowebcompat-repro-chrome-mask-fixed:false
autowebcompat-repro-channels:nightly,stable,esr
platform:windows,mac,linux
impact:feature-broken
configuration:general
affects:all
branch:release
diagnosis-team:dom
user-impact-score:30
autowebcompat-diagnosis-status:success

Attachments

(4 files)

Environment:
Operating system: Windows 10
Firefox version: Firefox 153.0

Steps to reproduce:

  1. Navigate to https://theladcollective.com/products/3-in-1-adjustable-comfort-pillow?variant=44534612525103
  2. Hover over a navigation menu item with a dropdown (e.g. BEDDING or BATH & LAUNDRY)
  3. Move the pointer away from the menu and observe

Expected Behavior:
The dropdown should close once the pointer leaves it.

Actual Behavior:
The dropdown stays open and covers the page content.

Notes:

  • Reproduces regardless of the status of ETP
  • Reproduces in firefox-nightly, and firefox-release
  • Does not reproduce in chrome

Created from https://github.com/webcompat/web-bugs/issues/231214

Attached video Chr vs ff
Whiteboard: [webcompat-source:web-bugs] → [webcompat-source:web-bugs][autowebcompat:processed]
User Story: (updated)
Whiteboard: [webcompat-source:web-bugs][autowebcompat:processed] → [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success]

Reproduced in Firefox Nightly: this is a genuine Firefox web-compat issue.

On https://theladcollective.com/products/3-in-1-adjustable-comfort-pillow?variant=44534612525103 the desktop header's mega-menu items (BEDDING, BATH & LAUNDRY, EXPLORE, BLOG) are opened via pointerenter on the wrapping li.menu-list__list-item and are meant to close again via pointerleave. Hovering BEDDING opens the panel in both browsers (computed visibility of .menu-list__submenu goes hidden -> visible).

When the pointer is then moved away from the menu — straight down past the bottom edge of the open panel and into the page content (the product photo gallery) — Chrome closes the dropdown (visibility returns to hidden), but Firefox leaves it open indefinitely: visibility stays visible, so the full-width mega menu keeps covering the product image gallery, price and variant selectors. The stuck panel also swallows clicks aimed at the gallery underneath it. This matches the reported "dropdown stays open and covers the page content / interferes with the photo gallery".

Verified two ways: (a) manually with the Firefox DevTools MCP, where after hovering BEDDING and then hovering the product gallery image the submenu remained visible and the trigger li still matched :hover; and (b) with a Puppeteer script driving the real site, which reproduced 0/3 working runs in Firefox (exit 1) and 3/3 working runs in Chrome (exit 0). The difference is not dependent on how the mouse is moved — both a gradual stepped path and a single teleporting move close the menu in Chrome and leave it open in Firefox. No changes to Firefox configuration were needed; ETP was left at its default, and reader mode/autofill are not involved.

The breakage is an interaction/hover-state failure rather than a layout or rendering error, so no screenshot was captured.

User Story: (updated)
Severity: -- → S3
User Story: (updated)
Webcompat Priority: --- → P3
Webcompat Score: --- → 3
Priority: -- → P3
Whiteboard: [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success] → [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success][autowebcompat:diagnose]
Whiteboard: [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success][autowebcompat:diagnose] → [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success]

Root cause analysis generated by autowebcompat bot:

The site's header mega-menu (Shopify "Horizon" theme, assets/header-menu.js, <header-menu> custom element) opens/closes its submenu from declarative on:pointerenter="/activate" / on:pointerleave="/deactivate" bindings on li.menu-list__list-item. The close path, HeaderMenu#deactivate(), does not trust the event alone — inside the pointerleave handler it re-reads the CSS :hover element state as a guard:

#deactivate = (item = this.#state.activeItem) => {
  if (!item || item != this.#state.activeItem || this.overflowListHovered || this.overflowMenu?.matches(":hover")) return;
  const activeSubmenu = findSubmenu(item), activeListItem = item.closest(".menu-list__list-item");
  if (activeSubmenu?.matches(":hover") || activeListItem?.matches(":hover")) return;   // <-- bails here in Firefox
  ... item.ariaExpanded = "false"; delete submenu.dataset.active;
}

Visibility is driven purely off that state: .menu-list__list-item:has([aria-expanded='true']) > .menu-list__submenu { visibility: visible }.

The browser divergence is when the :hover element state is updated relative to the dispatch of the Pointer Events boundary events. Chrome/Blink flushes the hover state before it dispatches pointerout/pointerleave, so inside the handler li.matches(':hover') is already false, the guard passes, aria-expanded flips to "false" and the mega menu closes. Firefox/Gecko still has the old hover chain in place when it dispatches pointerenter/pointerleave — inside pointerleave the trigger <li> (and .menu-list__submenu) still match :hover (and inside pointerenter they do not yet match). Gecko only flushes the hover state immediately afterwards: the mouseleave fired for the very same pointer move already reports :hover === false.

Because of that stale :hover reading, the theme's guard short-circuits on every pointerleave in Firefox, #deactivate() never runs its close code, aria-expanded stays "true" and data-active stays on the submenu, so the full-width mega-menu panel remains visibility: visible indefinitely, covering the product gallery/price/variant pickers and swallowing clicks. No further event ever retries the close, so the state is permanently stuck (the reproduction's hoveredAfterLeave: true is just the script sampling the same stale-then-flushed state; the real defect is the skipped close).

Mechanism in one line: the page depends on Element.matches(':hover') being up to date inside a pointerleave listener — true in Chrome, not in Firefox, where the hover element state is updated after the pointer* boundary events (before the mouse* ones).

Evidence:

  1. Theme source (https://theladcollective.com/cdn/shop/t/281/assets/header-menu.js) contains the :hover-guarded #deactivate shown above; the markup is <li class="menu-list__list-item" on:pointerenter="/activate" on:pointerleave="/deactivate"> with a child div.menu-list__submenu[ref="submenu[]"].
  2. CSS (compiled_assets/styles.css): .menu-list__list-item:has([aria-expanded='true']) > .menu-list__submenu, ... , .menu-list__submenu:is(:hover) { --submenu-content-opacity: 1; visibility: visible; } — so aria-expanded is the only thing keeping the panel open once the pointer is gone.
  3. Instrumented Puppeteer run on the real product page (capture listeners on the BEDDING <li> logging li.matches(':hover') from inside each boundary event):
    • Firefox Nightly: pointerenter → liHoverInHandler: false; final pointerleave (relatedTarget IMG.product-media__image, i.e. pointer already over the product photo at y≈548) → liHoverInHandler: true, subHoverInHandler: true; the mouseleave that follows for the same move → liHoverInHandler: false. End state: visibility: 'visible', aria-expanded: 'true', data-active still present, clip-path still inset(96px 0% calc(100% - 448px) 0px) (panel open).
    • Chrome stable: pointerenter → liHoverInHandler: true; final pointerleave (same relatedTarget IMG.product-media__image) → liHoverInHandler: false, and aria-expanded is observed flipping to 'false' within that same handler sequence. End state: visibility: 'hidden', aria-expanded: 'false', data-active gone, clip-path back to inset(96px 0% 100% 0px).
      document.elementsFromPoint() at the pointer end position returned the product image (not the submenu) in both browsers, ruling out a hit-testing/geometry (clip-path, content-visibility: auto) difference — the pointer really had left the menu in Firefox too; only the :hover element state read inside pointerleave differed.
  4. No console errors or failed/blocked network requests were involved; the divergence is purely the event-vs-hover-state ordering.
  5. Reduced testcase /app/diagnosis/testcase=ueygv6es.html (a trigger div with an absolutely-positioned child panel, opened on pointerenter, closed on pointerleave behind the same matches(':hover') guard). Loaded via file:// in both browsers and driven by hovering the trigger then the content box below:
    • Firefox Nightly: readings [{pointerenter, hover:false}, {pointerleave, hover:true}, {mouseleave, hover:false}]; on-page result "FAIL (Firefox behaviour): panel is still OPEN after the pointer left"; computed panel visibility: "visible".
    • Chrome stable: readings [{pointerenter, hover:true}, {pointerleave, hover:false}, {mouseleave, hover:false}]; on-page result "PASS (Chrome behaviour): panel closed after the pointer left"; computed panel visibility: "hidden".
      That matches the real-site behaviour, including the detail that Firefox's mouseleave (same pointer move, dispatched right after pointerleave) already sees :hover === false.
User Story: (updated)

Emilio, do you have thoughts on this auto-diagnosis? Should we be doing something differently?

User Story: (updated)
Flags: needinfo?(emilio)

Looks like bug 1360335 where mouseleave still matches :hover?

Seems like it could be tweaked around here, but it needs some looking into what other browsers do. We seem to be updating hover state only on mousemove, if I'm reading the code correctly.

Hmm, I knew this rang a bell, I fixed bug 1446832 a while ago :)

Flags: needinfo?(emilio)
See Also: → 1360335
See Also: → 1446832

Olli do you know why the !isPointer condition here? I guess if we dispatch pointer events before mouse events we should flip it or so? Wdyt?

That goes back to the original pointer events implementation fwiw.

Flags: needinfo?(smaug)
Assignee: nobody → emilio
Attachment #9626313 - Attachment description: WIP: Bug 2062197 - Update hover state on pointer events not mouse events. r=smaug → Bug 2062197 - Update hover state on pointer events not mouse events. r=smaug
Status: NEW → ASSIGNED
Attachment #9626313 - Attachment description: Bug 2062197 - Update hover state on pointer events not mouse events. r=smaug → Bug 2062197 - Update hover state on pointer events as well as mouse events. r=smaug

(In reply to Emilio Cobos Álvarez [:emilio] from comment #7)

Olli do you know why the !isPointer condition here? I guess if we dispatch pointer events before mouse events we should flip it or so? Wdyt?

That goes back to the original pointer events implementation fwiw.

As I said in phab, looks like nothing of this is defined properly anywhere, I mean in any specification :/
Thanks for looking into this!

Flags: needinfo?(smaug)
Pushed by pstanciu@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/303e4b8e5131 https://hg.mozilla.org/integration/autoland/rev/2a0066ab695c Revert "Bug 2062197 - Update hover state on pointer events as well as mouse events. r=smaug" for causing mochitest failures @ test_group_touchevents-6.html
Flags: needinfo?(emilio)

Hiro, can you take a quick look at those? I missed these because of bug 1989729, but I think updating :hover state on pointer events makes sense generally, so maybe APZ needs some tweaks, or maybe the behavior change is ok?

Flags: needinfo?(emilio) → needinfo?(hikezoe.birchill)

Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/61958 for changes under testing/web-platform/tests

Whiteboard: [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success] → [webcompat-source:web-bugs][autowebcompat:processed][autowebcompat:repro-success], [wptsync upstream]

Upstream PR was closed without merging

In the test (helper_hover_state_while_scroll.html) scenario the touch events are used to scroll. From 5.1.3.3 Suppressing a pointer event stream in the pointer events spec

The pointer is subsequently used by the user agent to manipulate the page viewport (e.g. panning or zooming).

In such cases, a pointercancel event needs to be fired. It's dispatched here.

It ends up calling EventStateManager::NotifyMouseOut, thus with D318502 the :hover state unexpectedly reset.

I am not sure the route of a pointercancel event -> NotifyMouseOut is correct or not. I don't immediately see any reasons why a pointercancel triggers a mouseout so there may be a bug?

Re-directing NI to Edgar.

Flags: needinfo?(hikezoe.birchill) → needinfo?(echen)

(In reply to Hiroyuki Ikezoe (:hiro) from comment #16)

I am not sure the route of a pointercancel event -> NotifyMouseOut is correct or not. I don't immediately see any reasons why a pointercancel triggers a mouseout so there may be a bug?

From the same spec text:

A pointer input device is physically disconnected

In this case, a mouseout is definitely necessary so we need to handle scrolling specifically?

(In reply to Hiroyuki Ikezoe (:hiro) from comment #16)

I am not sure the route of a pointercancel event -> NotifyMouseOut is correct or not. I don't immediately see any reasons why a pointercancel triggers a mouseout so there may be a bug?

A pointercancel triggers a pointerout actually, and it is necessary to notify the pointer is leaving.


(In reply to Emilio Cobos Álvarez [:emilio] from comment #13)

I think updating :hover state on pointer events makes sense generally

Yes, that make sense, but I think not all pointer type should update the :hover state, like touch, so I guess we need to check WidgetMouseEventBase::InputSourceSupportsHover() as well.

Flags: needinfo?(echen)

Ok, let me try with that check.

Flags: needinfo?(emilio)
Status: ASSIGNED → RESOLVED
Closed: 2 days ago
Resolution: --- → FIXED
Target Milestone: --- → 156 Branch
Flags: needinfo?(emilio)

Upstream PR merged by moz-wptsync-bot

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: