{"version":3,"sources":["../../src/components/ScrollRestoration.tsx"],"sourcesContent":["// Remember where the reader was, and put them back there on Back (R3-627).\n//\n// Mount once, near the root, beside `ScrollAfterNavigation`. With no props it\n// remembers the document's scroll offset, which is right for an app whose page\n// scrolls; an app that scrolls its own container passes a ref to it.\n//\n// What it does NOT do: fight the reader, fight a deep link, or scroll a page the\n// reader arrived at by clicking a link. Only a history traversal restores, only if\n// the entry carries an offset, and only until the reader touches the scroller.\n\nimport { type RefObject, use, useEffect, useRef } from 'react';\n\nimport { registerEntryStateCollector } from '../entryState';\nimport { nextRestoreAction, RESTORE_DEADLINE_MS } from '../scrollRestore';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { useEntryState, useNavigationDirection } from '../useEntryState';\n\n/** The key this component owns in the entry scratch. One owner per key. */\nconst SCROLL_KEY = 'ir.scroll';\n\n/** The geometry of whatever is scrolling, read uniformly for the document and for a\n *  container, so the decision module never has to care which it is. */\ninterface Scroller {\n  offset(): number;\n  scrollTo(top: number): void;\n  scrollHeight(): number;\n  clientHeight(): number;\n  listen(onScroll: () => void): () => void;\n}\n\nconst documentScroller = (): Scroller | null => {\n  if (typeof document === 'undefined' || typeof window === 'undefined') return null;\n  const el = () => document.scrollingElement ?? document.documentElement;\n  return {\n    offset: () => window.scrollY,\n    scrollTo: (top) => window.scrollTo(0, top),\n    scrollHeight: () => el().scrollHeight,\n    clientHeight: () => window.innerHeight,\n    listen: (onScroll) => {\n      window.addEventListener('scroll', onScroll, { passive: true });\n      return () => window.removeEventListener('scroll', onScroll);\n    },\n  };\n};\n\nconst elementScroller = (el: HTMLElement): Scroller => ({\n  offset: () => el.scrollTop,\n  scrollTo: (top) => {\n    el.scrollTop = top;\n  },\n  scrollHeight: () => el.scrollHeight,\n  clientHeight: () => el.clientHeight,\n  listen: (onScroll) => {\n    el.addEventListener('scroll', onScroll, { passive: true });\n    return () => el.removeEventListener('scroll', onScroll);\n  },\n});\n\nexport interface ScrollRestorationProps {\n  /** The element that scrolls. Omit when the document scrolls. */\n  scroller?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Null-rendering. Remembers the scroller's offset on the entry being left, and\n * restores it when the reader returns to that entry.\n */\nexport const ScrollRestoration = ({ scroller }: ScrollRestorationProps = {}): null => {\n  const { value: rememberedOffset } = useEntryState<number>(SCROLL_KEY);\n  const direction = useNavigationDirection();\n  const { navigationState } = use(TinkerableContext);\n  const hasFragment = Boolean(navigationState.hash);\n  // Read through a ref so the collector registered below is stable for the life of\n  // the component: re-registering per render would churn the one-owner-per-key map.\n  const scrollerRef = useRef(scroller);\n  scrollerRef.current = scroller;\n\n  const resolve = (): Scroller | null => {\n    const el = scrollerRef.current?.current;\n    return el ? elementScroller(el) : documentScroller();\n  };\n\n  // Save: asked at navigation time, from inside `navigate()`.\n  useEffect(() => {\n    return registerEntryStateCollector(SCROLL_KEY, () => {\n      const s = resolve();\n      const offset = s?.offset() ?? 0;\n      // Nothing to remember about the top of a page; leaving the key out keeps the\n      // scratch empty in the common case, so most entries carry no state at all.\n      return offset > 0 ? Math.round(offset) : undefined;\n    });\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Restore: only on a traversal, only with an offset, and never over a deep link.\n  useEffect(() => {\n    if (direction === 'push') return;\n    if (hasFragment) return; // an explicit #fragment outranks a remembered position\n    if (typeof rememberedOffset !== 'number') return;\n    const s = resolve();\n    if (!s) return;\n\n    const startedAt = Date.now();\n    let userScrolled = false;\n    let done = false;\n\n    const finish = () => {\n      done = true;\n      stopListening();\n      observer?.disconnect();\n      timers.forEach(clearTimeout);\n    };\n\n    const attempt = () => {\n      if (done) return;\n      const action = nextRestoreAction({\n        target: rememberedOffset,\n        scrollHeight: s.scrollHeight(),\n        clientHeight: s.clientHeight(),\n        current: s.offset(),\n        elapsedMs: Date.now() - startedAt,\n        userScrolled,\n      });\n      if (action === 'wait') return;\n      if (action === 'apply') s.scrollTo(rememberedOffset);\n      finish();\n    };\n\n    // The reader wins: the first scroll they make ends the attempt. Registered\n    // before the first attempt so a scroll during that frame is not missed.\n    const stopListening = s.listen(() => {\n      // Our own `scrollTo` fires this too; `done` is set before the event lands\n      // because `finish()` runs synchronously after `apply`.\n      if (!done) userScrolled = true;\n    });\n\n    // The content arrives asynchronously, so sample as it grows: on DOM changes, on\n    // the same ladder `ScrollAfterNavigation` uses, and once at the deadline.\n    const observer = typeof MutationObserver === 'undefined' ? null : new MutationObserver(() => attempt());\n    observer?.observe(document.body, { childList: true, subtree: true });\n    const timers = [0, 120, 300, 600, RESTORE_DEADLINE_MS].map((ms) => setTimeout(attempt, ms));\n\n    return finish;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [rememberedOffset, direction, hasFragment]);\n\n  return null;\n};\n"],"mappings":"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","names":[]}