theladcollective.com - Navigation dropdown stays open after the pointer leaves it interfering with the photo gallery
Categories
(Web Compatibility :: Site Reports, defect, P3)
Tracking
(Webcompat Priority:P3, Webcompat Score:3, firefox156 fixed)
| 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:
- Navigate to https://theladcollective.com/products/3-in-1-adjustable-comfort-pillow?variant=44534612525103
- Hover over a navigation menu item with a dropdown (e.g. BEDDING or BATH & LAUNDRY)
- 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
| Reporter | ||
Comment 1•12 days ago
|
||
Updated•12 days ago
|
Updated•12 days ago
|
Comment 2•12 days ago
|
||
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.
Updated•12 days ago
|
Updated•11 days ago
|
Updated•8 days ago
|
Updated•8 days ago
|
Comment 3•8 days ago
|
||
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:
- Theme source (
https://theladcollective.com/cdn/shop/t/281/assets/header-menu.js) contains the:hover-guarded#deactivateshown above; the markup is<li class="menu-list__list-item" on:pointerenter="/activate" on:pointerleave="/deactivate">with a childdiv.menu-list__submenu[ref="submenu[]"]. - 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; }— soaria-expandedis the only thing keeping the panel open once the pointer is gone. - Instrumented Puppeteer run on the real product page (capture listeners on the BEDDING
<li>loggingli.matches(':hover')from inside each boundary event):- Firefox Nightly:
pointerenter → liHoverInHandler: false; finalpointerleave(relatedTargetIMG.product-media__image, i.e. pointer already over the product photo at y≈548) →liHoverInHandler: true, subHoverInHandler: true; themouseleavethat follows for the same move →liHoverInHandler: false. End state:visibility: 'visible',aria-expanded: 'true',data-activestill present, clip-path stillinset(96px 0% calc(100% - 448px) 0px)(panel open). - Chrome stable:
pointerenter → liHoverInHandler: true; finalpointerleave(same relatedTargetIMG.product-media__image) →liHoverInHandler: false, andaria-expandedis observed flipping to'false'within that same handler sequence. End state:visibility: 'hidden',aria-expanded: 'false',data-activegone, clip-path back toinset(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:hoverelement state read insidepointerleavediffered.
- Firefox Nightly:
- No console errors or failed/blocked network requests were involved; the divergence is purely the event-vs-hover-state ordering.
- Reduced testcase
/app/diagnosis/testcase=ueygv6es.html(a trigger div with an absolutely-positioned child panel, opened onpointerenter, closed onpointerleavebehind the samematches(':hover')guard). Loaded viafile://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 panelvisibility: "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 panelvisibility: "hidden".
That matches the real-site behaviour, including the detail that Firefox'smouseleave(same pointer move, dispatched right afterpointerleave) already sees:hover === false.
- Firefox Nightly: readings
Comment 4•8 days ago
|
||
Comment 5•8 days ago
|
||
Emilio, do you have thoughts on this auto-diagnosis? Should we be doing something differently?
| Assignee | ||
Comment 6•8 days ago
|
||
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 :)
| Assignee | ||
Comment 7•8 days ago
|
||
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.
| Assignee | ||
Comment 8•8 days ago
|
||
Updated•8 days ago
|
Updated•8 days ago
|
Comment 9•8 days ago
•
|
||
(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!
Comment 10•8 days ago
|
||
Comment 11•7 days ago
|
||
Comment 12•7 days ago
|
||
Backed out for causing mochitest failures @ test_group_touchevents-6.html
| Assignee | ||
Comment 13•7 days ago
|
||
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?
Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/61958 for changes under testing/web-platform/tests
Upstream PR was closed without merging
Comment 16•5 days ago
|
||
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.
Comment 17•5 days ago
|
||
(In reply to Hiroyuki Ikezoe (:hiro) from comment #16)
I am not sure the route of
a pointercancel event -> NotifyMouseOutis 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?
Comment 18•2 days ago
|
||
(In reply to Hiroyuki Ikezoe (:hiro) from comment #16)
I am not sure the route of
a pointercancel event -> NotifyMouseOutis 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
:hoverstate 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.
Comment 20•2 days ago
|
||
Comment 21•2 days ago
|
||
| bugherder | ||
| Assignee | ||
Updated•2 days ago
|
Upstream PR merged by moz-wptsync-bot
Description
•