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.
This commit is contained in:
driver 2026-09-30 03:28:29 +00:00
parent 196817f1da
commit 617e9561ff

View file

@ -53,18 +53,42 @@ function fitBelowHeader() {
// is scrolled down a little, <html> 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<HTMLElement>()
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.