From 617e9561ff05baf67c31e4f05b7e79adc72275de Mon Sep 17 00:00:00 2001 From: driver Date: Wed, 30 Sep 2026 03:28:29 +0000 Subject: [PATCH] Deep Research: the header also leaves when a map scrolls inside its own shadow root dx-life measured with a real finger: on the Regions map the header stayed. Regions scrolls in its own containers (.rd-scroll, .rl-scroll) inside its own shadow root, and scroll events never cross a shadow root, so the gallery-level listener never saw them. Touch and wheel events do cross: on the first one over such a scroller, the page finds it in the event's composed path and listens to its scrolls directly (once each), which also covers momentum. The same half-the-gallery size rule keeps small lists, such as a legend sheet's, out. --- app/pages/deep-research/index.vue | 34 ++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/app/pages/deep-research/index.vue b/app/pages/deep-research/index.vue index d3e7d506..7a10cb82 100644 --- a/app/pages/deep-research/index.vue +++ b/app/pages/deep-research/index.vue @@ -53,18 +53,42 @@ function fitBelowHeader() { // is scrolled down a little, gets rg-header-hidden (the header hides and the gallery takes // the full screen); back at the pane's top, the header returns. One scroller, so nothing fights // the pane's own momentum. Only a scroller at least half the gallery's height counts (its main -// pane), not a small list such as a legend sheet's. +// pane, or a map's own main list), not a small list such as a legend sheet's. function watchPaneForHeader(el: HTMLElement, root: ShadowRoot) { + const cls = document.documentElement.classList + const large = (n: HTMLElement) => n.clientHeight >= el.clientHeight / 2 const onScroll = (e: Event) => { const pane = e.target - if (!(pane instanceof HTMLElement) || pane.clientHeight < el.clientHeight / 2) return - const cls = document.documentElement.classList + if (!(pane instanceof HTMLElement) || !large(pane)) return if (pane.scrollTop > 8) cls.add('rg-header-hidden') else if (pane.scrollTop <= 0) cls.remove('rg-header-hidden') } - // Scroll events don't bubble, but they do pass the shadow root in the capture phase. + // Scroll events don't bubble, but they do pass the gallery's shadow root in the capture phase. root.addEventListener('scroll', onScroll, { capture: true, passive: true }) - unwatchPane = () => root.removeEventListener('scroll', onScroll, true) + // A map inside the gallery may scroll in ITS OWN shadow root (Regions does), and scroll events + // never cross a shadow root. Touch and wheel events do: on the first one over such a scroller, + // find it in the event's composed path and listen to its scrolls directly (once each), which + // also covers the momentum after the finger lifts. + const watched = new WeakSet() + const discover = (e: Event) => { + for (const n of e.composedPath()) { + if (n === el) break + if (!(n instanceof HTMLElement) || watched.has(n)) continue + const oy = getComputedStyle(n).overflowY + if ((oy === 'auto' || oy === 'scroll') && n.scrollHeight > n.clientHeight + 1 && large(n)) { + watched.add(n) + n.addEventListener('scroll', onScroll, { passive: true }) + break + } + } + } + el.addEventListener('touchmove', discover, { passive: true }) + el.addEventListener('wheel', discover, { passive: true }) + unwatchPane = () => { + root.removeEventListener('scroll', onScroll, true) + el.removeEventListener('touchmove', discover) + el.removeEventListener('wheel', discover) + } } // The gallery attaches its shadow root once its script has defined the element.