CI / verify (push) Failing after 5m9s
Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef } from "react";
|
|
|
|
function fragmentTarget(hash: string) {
|
|
if (!hash) return null;
|
|
try {
|
|
return document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function focusFragment(target: HTMLElement) {
|
|
if (!target.hasAttribute("tabindex")) target.tabIndex = -1;
|
|
target.focus({ preventScroll: true });
|
|
}
|
|
|
|
/** 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 navigation = performance.getEntriesByType?.("navigation")[0] as
|
|
| PerformanceNavigationTiming
|
|
| undefined;
|
|
// A history restoration owns its saved position, not the URL's old fragment.
|
|
let pending =
|
|
navigation?.type === "back_forward" ? "" : window.location.hash;
|
|
|
|
const resolve = () => {
|
|
if (!ready.current || !pending) return;
|
|
const hash = pending;
|
|
pending = "";
|
|
if (hash !== window.location.hash) return;
|
|
const target = fragmentTarget(hash);
|
|
if (!target) return;
|
|
focusFragment(target);
|
|
target.scrollIntoView({ block: "start", behavior: "instant" });
|
|
};
|
|
|
|
const cancelArrival = () => {
|
|
pending = "";
|
|
};
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (
|
|
[
|
|
"ArrowUp",
|
|
"ArrowDown",
|
|
"PageUp",
|
|
"PageDown",
|
|
"Home",
|
|
"End",
|
|
" ",
|
|
"Tab",
|
|
].includes(event.key)
|
|
) {
|
|
cancelArrival();
|
|
}
|
|
};
|
|
const onClick = (event: MouseEvent) => {
|
|
if (
|
|
event.defaultPrevented ||
|
|
event.button !== 0 ||
|
|
event.metaKey ||
|
|
event.ctrlKey ||
|
|
event.shiftKey ||
|
|
event.altKey
|
|
)
|
|
return;
|
|
const link =
|
|
event.target instanceof Element
|
|
? event.target.closest("a[href]")
|
|
: null;
|
|
if (
|
|
!(link instanceof HTMLAnchorElement) ||
|
|
link.hasAttribute("download") ||
|
|
(link.target && link.target !== "_self")
|
|
)
|
|
return;
|
|
const url = new URL(link.href);
|
|
if (
|
|
url.origin !== window.location.origin ||
|
|
url.pathname !== window.location.pathname ||
|
|
url.search !== window.location.search
|
|
)
|
|
return;
|
|
const target = fragmentTarget(url.hash);
|
|
if (!target) return;
|
|
pending = ready.current ? "" : url.hash;
|
|
// Capture before a mobile menu removes its activated link. Do not cancel navigation.
|
|
focusFragment(target);
|
|
};
|
|
const onHashChange = () => {
|
|
if (pending && pending !== window.location.hash) cancelArrival();
|
|
if (!ready.current) return;
|
|
const target = fragmentTarget(window.location.hash);
|
|
if (target) focusFragment(target);
|
|
};
|
|
|
|
resolveArrival.current = resolve;
|
|
resolve();
|
|
document.addEventListener("click", onClick, true);
|
|
window.addEventListener("hashchange", onHashChange);
|
|
window.addEventListener("wheel", cancelArrival, { passive: true });
|
|
window.addEventListener("touchmove", cancelArrival, { passive: true });
|
|
window.addEventListener("keydown", onKeyDown);
|
|
window.addEventListener("pointerdown", cancelArrival, { passive: true });
|
|
return () => {
|
|
resolveArrival.current = null;
|
|
document.removeEventListener("click", onClick, true);
|
|
window.removeEventListener("hashchange", onHashChange);
|
|
window.removeEventListener("wheel", cancelArrival);
|
|
window.removeEventListener("touchmove", cancelArrival);
|
|
window.removeEventListener("keydown", onKeyDown);
|
|
window.removeEventListener("pointerdown", cancelArrival);
|
|
};
|
|
}, []);
|
|
|
|
return useCallback(() => {
|
|
ready.current = true;
|
|
resolveArrival.current?.();
|
|
}, []);
|
|
}
|