Repository: adobe/react-spectrum
File: packages/dev/s2-docs/src/client.tsx (TypeScript)

Code before:
let isClientLink = (link: HTMLAnchorElement, pathname: string) => {
  return (
    link &&
    link instanceof HTMLAnchorElement &&
    link.href &&
    (!link.target || link.target === '_self') &&
    link.origin === location.origin &&
    !link.hasAttribute('download') &&
    link.pathname.startsWith(pathname)
  );
};

// Hydrate initial RSC payload embedded in the HTML.
let updateRoot = hydrate({
  // Intercept HMR window reloads, and do it with RSC instead.
  onHmrReload() {
    navigate(location.pathname + location.search + location.hash);
  }
});

// Track the current navigation to prevent race conditions
let currentNavigationId = 0;
let currentAbortController: AbortController | null = null;

// Store scroll positions by pathname
const scrollPositions = new Map<string, {scrollTop: number, windowScrollTop: number}>();

// Track the current pathname so we can save scroll position on popstate
// (when popstate fires, location.pathname has already changed to the destination)
let currentPathname = location.pathname;

function getScrollContainer(): HTMLElement | null {
  return document.querySelector('main');
}

// Save scroll position for a given pathname
function saveScrollPosition(pathname: string) {
  let scrollContainer = getScrollContainer();
  let scrollTop = scrollContainer?.scrollTop ?? 0;
  let windowScrollTop = window.scrollY;
  scrollPositions.set(pathname, {scrollTop, windowScrollTop});
}

function restoreScrollPosition(pathname: string) {
  let position = scrollPositions.get(pathname);
  if (position) {
    requestAnimationFrame(() => {
      let scrollContainer = getScrollContainer();
      if (scrollContainer) {
        scrollContainer.scrollTop = position.scrollTop;
      }

Review comment (bug):
Maybe we can save these in the history entry itself? https://stackoverflow.com/a/16732655

Might also have to turn off the browser's defaults? https://developer.chrome.com/blog/history-api-scroll-restoration

I did wonder if the defaults would work if we moved the `key` from the `<main>` element further down (e.g. article) so that it doesn't get replaced on each navigation. Did you try that?

Code after:

if ('scrollRestoration' in history) {
  // Disable browser's automatic scroll restoration since we handle it manually
  history.scrollRestoration = 'manual';
}

// Hydrate initial RSC payload embedded in the HTML.
let updateRoot = hydrate({
  // Intercept HMR window reloads, and do it with RSC instead.
  onHmrReload() {
    navigate(location.pathname + location.search + location.hash);
  }
});

// Track the current navigation to prevent race conditions
let currentNavigationId = 0;
let currentAbortController: AbortController | null = null;

interface HistoryState {
  scrollTop?: number,
  windowScrollTop?: number
}

function getScrollContainer(): HTMLElement | null {
  return document.querySelector('main');
}

function saveScrollPosition() {
  let scrollContainer = getScrollContainer();
  let scrollTop = scrollContainer?.scrollTop ?? 0;
  let windowScrollTop = window.scrollY;
  let state: HistoryState = {
    ...(history.state as HistoryState | null),
    scrollTop,
    windowScrollTop
  };
  history.replaceState(state, '', location.href);
}

function restoreScrollPosition(state: HistoryState | null) {
  if (state?.scrollTop != null || state?.windowScrollTop != null) {
    requestAnimationFrame(() => {
      let scrollContainer = getScrollContainer();
      if (scrollContainer && state.scrollTop != null) {
        scrollContainer.scrollTop = state.scrollTop;
      }
      if (state.windowScrollTop != null) {
        window.scrollTo(0, state.windowScrollTop);
      }
    });
  }