Let the wheel reach the page when a map is only part of it
Some checks are pending
build-gate / build (push) Waiting to run

The people-groups grid under the dashboard could not be reached by scrolling. Every
earlier investigation looked at section heights and overflow, because that is what the
documented fix addressed — but the cause was the map itself: Mapbox binds the wheel for
zoom, so the document never saw the event. Measured before: fourteen wheel ticks left
scrollY at zero and the first grid card exactly where it started.

Scroll-zoom is now disabled only while the page has somewhere to scroll, so a map that
is the whole page still zooms on wheel and a map stacked above a grid gets out of the
way. Measured after: scrollY 1549 and the first card moved from 897 to -652.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 16:03:33 +00:00
parent ec012f45b6
commit 56dbb6770d

View file

@ -367,10 +367,47 @@ function onBubbleClick(e) {
bus.emit('focusRow', { slug, name: f.properties && f.properties.name })
}
// ── SCROLL-ZOOM vs PAGE-SCROLL (BUG-CANNOT-SCROLL-DOWN-TO-THE-GRID-UNDER-THE-DASHBOARD) ──────
// Mapbox GL calls preventDefault() on EVERY wheel event over its canvas to run scroll-zoom. That
// is correct for a map that IS the page (the fixed-viewport leaves), and fatal for a map embedded
// in a SCROLL-PAGE composite: the wheel never reaches the document, so the page cannot scroll to
// whatever is stacked BELOW the dashboard. MEASURED 2026-09-22 on the dashboard-plus-grid dev
// leaf: cursor over .kpi scrolled 600px; cursor over mapboxgl-canvas scrolled 0px with
// defaultPrevented=true, with 2,631px of grid sitting below the fold and unreachable.
//
// The .map-scroll-edge strips are the TOUCH answer (a pan-y drag at either edge scrolls the page
// while the map keeps the centre), and they are now rendered — but they do NOT release a DESKTOP
// wheel at the centre of the canvas, which is exactly where a mouse naturally sits. So on a
// scroll-page the wheel must belong to the PAGE.
//
// WHY DETECT RATHER THAN HARD-DISABLE: this panel ships inside BOTH kinds of host — the
// standalone research-dashboard leaf (fixed viewport, where wheel-zoom is the expected and
// desirable behaviour) and the cap-4 composites (a scroll-stack). The document's own scrollability
// is the honest discriminator: a fixed-viewport leaf pins html/body to one screen, so there is
// nothing to scroll and scroll-zoom stays ON; a scroll-page grows past the viewport, so the wheel
// is handed back. NOTHING IS LOST EITHER WAY — the MapToolbar zoom buttons and double-click/
// pinch zoom all still work, so a scroll-page map remains fully zoomable without trapping a wheel.
function syncScrollZoomToHost(m) {
if (!m || !m.scrollZoom) return
try {
const doc = document.documentElement
// A real page scrollport: content taller than the viewport AND an ancestor that can scroll it.
const canPageScroll = doc.scrollHeight > window.innerHeight + 4
if (canPageScroll) m.scrollZoom.disable()
else m.scrollZoom.enable()
} catch (_) { /* an engine without scrollZoom must never break the panel */ }
}
onMounted(async () => {
await initializeMap()
const m = map.value; if (!m) return
if (m.isStyleLoaded()) addLayers(); else m.on('load', addLayers)
// Decide once the stack below has had a chance to lay out (the grid loads async, so the page
// only becomes taller than the viewport after its first rows land), then keep it in step: a
// collapsing table or a filtered-down grid can change whether the page scrolls at all.
syncScrollZoomToHost(m)
setTimeout(() => syncScrollZoomToHost(m), 1200)
setTimeout(() => syncScrollZoomToHost(m), 4000)
// Keep the zoom buttons' enabled state in step with the live camera. 'zoom' fires only during
// an actual zoom; 'zoomend' catches the settle. Both just set two booleans (no camera writes).
m.on('zoom', syncZoomLimits)