"use client"; import { useCallback, useEffect, useRef } from "react"; import { elementForHash, type FragmentHit, focusFragment, onFragmentClick, setScrollRestoration, } from "./fragment-nav"; /** Teclas de rolagem: quem as usa já escolheu para onde ir, e a chegada por fragmento deixa de corrigir. */ const SCROLL_KEYS = new Set([ "ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Tab", ]); /** * A document owns one outstanding homepage arrival. Later mounts are same-document * history, where the browser restores its own position. */ let arrivalSettled = false; /** Tests replay several documents inside one jsdom document. */ export function resetHomeArrivalState() { arrivalSettled = false; } /** Rola a página até o destino do fragmento, descontando o cabeçalho fixo (scroll-padding) e a margem do próprio alvo. */ function scrollToFragment(target: HTMLElement) { const padding = Number.parseFloat( getComputedStyle(document.documentElement).scrollPaddingTop, ) || 0; const margin = Number.parseFloat(getComputedStyle(target).scrollMarginTop) || 0; window.scrollTo({ top: Math.max( 0, window.scrollY + target.getBoundingClientRect().top - padding - margin, ), left: 0, behavior: "instant", }); } /** * A chegada (uma por documento): quer o topo, quando a URL não traz fragmento, ou o destino do fragmento, depois * que o layout da home está pronto. Corrige só uma chegada pendente; links comuns e histórico seguem nativos. */ function createArrival(isReady: () => boolean) { const navigation = performance.getEntriesByType?.("navigation")[0] as | PerformanceNavigationTiming | undefined; // A history restoration owns its saved position, not the URL's old fragment. const restoration = navigation?.type === "back_forward"; const arrival = !restoration && !arrivalSettled; let pending = restoration ? "" : window.location.hash; let pendingTop = arrival && !window.location.hash; if (arrival) setScrollRestoration("manual"); const goToTop = () => { pendingTop = false; if (!window.location.hash) window.scrollTo({ top: 0, left: 0, behavior: "instant" }); }; return { /** Aplica o que ficou pendente, se o layout já está pronto. */ resolve() { if (!isReady()) return; if (pendingTop) goToTop(); if (!pending) return; const hash = pending; pending = ""; if (hash !== window.location.hash) return; const target = elementForHash(hash); if (!target) return; focusFragment(target); scrollToFragment(target); }, /** O visitante assumiu a rolagem: nada mais é corrigido, e a restauração do navegador volta. */ cancel() { pending = ""; pendingTop = false; setScrollRestoration("auto"); }, onKeyDown(event: KeyboardEvent) { if (SCROLL_KEYS.has(event.key)) this.cancel(); }, /** Clique num link de âncora (o ouvinte é de fragment-nav): sem cancelar a navegação, o foco vai ao destino. */ onFragmentClick({ target, hash }: FragmentHit) { pendingTop = false; pending = isReady() ? "" : hash; // Capture before a mobile menu removes its activated link. focusFragment(target); }, onHashChange() { pendingTop = false; if (pending && pending !== window.location.hash) this.cancel(); if (!isReady()) return; const target = elementForHash(window.location.hash); if (target) focusFragment(target); }, }; } /** Correct only an outstanding arrival; ordinary links and history keep native scrolling. */ export function useHomeFragmentNavigation() { const ready = useRef(false); const resolveArrival = useRef<(() => void) | null>(null); useEffect(() => { const arrival = createArrival(() => ready.current); const cancel = () => arrival.cancel(); const onKeyDown = (event: KeyboardEvent) => arrival.onKeyDown(event); const onHashChange = () => arrival.onHashChange(); resolveArrival.current = () => arrival.resolve(); arrival.resolve(); const stopClicks = onFragmentClick((hit) => arrival.onFragmentClick(hit)); window.addEventListener("hashchange", onHashChange); window.addEventListener("wheel", cancel, { passive: true }); window.addEventListener("touchmove", cancel, { passive: true }); window.addEventListener("keydown", onKeyDown); window.addEventListener("pointerdown", cancel, { passive: true }); return () => { resolveArrival.current = null; setScrollRestoration("auto"); stopClicks(); window.removeEventListener("hashchange", onHashChange); window.removeEventListener("wheel", cancel); window.removeEventListener("touchmove", cancel); window.removeEventListener("keydown", onKeyDown); window.removeEventListener("pointerdown", cancel); }; }, []); return useCallback(() => { ready.current = true; resolveArrival.current?.(); arrivalSettled = true; setScrollRestoration("auto"); }, []); }