From 56dbb6770d23a88f4185a1e4527dbbac10550495 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:03:33 +0000 Subject: [PATCH] Let the wheel reach the page when a map is only part of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dashboards/DashboardMapPanel.vue | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue index a6113990c..eb87fd189 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue @@ -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)