fix(nav): hand scroll restoration back to the browser after arrival
This commit is contained in:
1 parent
9aff4d1035
commit
b181216a35
3 files changed
+45
-8
No files matched your search
@@ -114,7 +114,7 @@ O notebook 3D permanece em celulares, tablets e desktop: tampa, tela, abas e cur
|
|||||||
|
|
||||||
As quatro capturas desktop reais ficam em `public/assets/notebook/`, em WebP de 1600 × 1000. A troca aguarda a decodificação da próxima imagem, mantendo a anterior visível durante o carregamento ou uma falha.
|
As quatro capturas desktop reais ficam em `public/assets/notebook/`, em WebP de 1600 × 1000. A troca aguarda a decodificação da próxima imagem, mantendo a anterior visível durante o carregamento ou uma falha.
|
||||||
|
|
||||||
Uma entrada normal na homepage começa no topo. A recarga também volta ao início, removendo o fragmento deixado por uma navegação anterior. Links diretos como `/#about` continuam chegando à seção após a estabilização do notebook; rolagem deliberada durante a abertura cancela esse ajuste. A política inicial de scroll é aplicada antes da hidratação, sem disputar com a rolagem suave dos links posteriores.
|
Uma entrada normal na homepage começa no topo. A recarga também volta ao início, removendo o fragmento deixado por uma navegação anterior. Links diretos como `/#about` continuam chegando à seção após a estabilização do notebook; rolagem deliberada durante a abertura cancela esse ajuste. A política inicial de scroll é aplicada antes da hidratação, sem disputar com a rolagem suave dos links posteriores. Assim que a chegada se resolve, a restauração volta a pertencer ao navegador: Voltar e Avançar restauram a posição do histórico em vez de forçar o topo.
|
||||||
|
|
||||||
### Identidade visual
|
### Identidade visual
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation";
|
import {
|
||||||
|
resetHomeArrivalState,
|
||||||
|
useHomeFragmentNavigation,
|
||||||
|
} from "./useHomeFragmentNavigation";
|
||||||
|
|
||||||
function HomeNavigation() {
|
function HomeNavigation() {
|
||||||
const onLayoutReady = useHomeFragmentNavigation();
|
const onLayoutReady = useHomeFragmentNavigation();
|
||||||
@@ -23,6 +26,7 @@ function HomeNavigation() {
|
|||||||
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
|
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
resetHomeArrivalState();
|
||||||
window.history.replaceState(null, "", "/");
|
window.history.replaceState(null, "", "/");
|
||||||
HTMLElement.prototype.scrollIntoView = jest.fn();
|
HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||||
});
|
});
|
||||||
@@ -53,6 +57,26 @@ describe("home fragment navigation", () => {
|
|||||||
expect(restoredPosition).toBe(0);
|
expect(restoredPosition).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaves a same-document return to the position the browser restores", () => {
|
||||||
|
const scrollTo = jest
|
||||||
|
.spyOn(window, "scrollTo")
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
const firstVisit = render(<HomeNavigation />);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Establish notebook layout" }),
|
||||||
|
);
|
||||||
|
firstVisit.unmount();
|
||||||
|
expect(window.history.scrollRestoration).not.toBe("manual");
|
||||||
|
|
||||||
|
scrollTo.mockClear();
|
||||||
|
render(<HomeNavigation />);
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: "Establish notebook layout" }),
|
||||||
|
);
|
||||||
|
expect(scrollTo).not.toHaveBeenCalled();
|
||||||
|
expect(window.history.scrollRestoration).not.toBe("manual");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not reset the visitor's position after intentional scrolling during startup", () => {
|
it("does not reset the visitor's position after intentional scrolling during startup", () => {
|
||||||
let position = 0;
|
let position = 0;
|
||||||
jest
|
jest
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ function focusFragment(target: HTMLElement) {
|
|||||||
target.focus({ preventScroll: true });
|
target.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
/** Correct only an outstanding arrival; ordinary links and history keep native scrolling. */
|
/** Correct only an outstanding arrival; ordinary links and history keep native scrolling. */
|
||||||
export function useHomeFragmentNavigation() {
|
export function useHomeFragmentNavigation() {
|
||||||
const ready = useRef(false);
|
const ready = useRef(false);
|
||||||
@@ -26,12 +37,11 @@ export function useHomeFragmentNavigation() {
|
|||||||
| PerformanceNavigationTiming
|
| PerformanceNavigationTiming
|
||||||
| undefined;
|
| undefined;
|
||||||
// A history restoration owns its saved position, not the URL's old fragment.
|
// A history restoration owns its saved position, not the URL's old fragment.
|
||||||
let pending =
|
const restoration = navigation?.type === "back_forward";
|
||||||
navigation?.type === "back_forward" ? "" : window.location.hash;
|
const arrival = !restoration && !arrivalSettled;
|
||||||
let pendingTop =
|
let pending = restoration ? "" : window.location.hash;
|
||||||
navigation?.type !== "back_forward" && !window.location.hash;
|
let pendingTop = arrival && !window.location.hash;
|
||||||
if (navigation?.type !== "back_forward")
|
if (arrival) window.history.scrollRestoration = "manual";
|
||||||
window.history.scrollRestoration = "manual";
|
|
||||||
|
|
||||||
const resolve = () => {
|
const resolve = () => {
|
||||||
if (!ready.current) return;
|
if (!ready.current) return;
|
||||||
@@ -69,6 +79,7 @@ export function useHomeFragmentNavigation() {
|
|||||||
const cancelArrival = () => {
|
const cancelArrival = () => {
|
||||||
pending = "";
|
pending = "";
|
||||||
pendingTop = false;
|
pendingTop = false;
|
||||||
|
window.history.scrollRestoration = "auto";
|
||||||
};
|
};
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -151,5 +162,7 @@ export function useHomeFragmentNavigation() {
|
|||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
ready.current = true;
|
ready.current = true;
|
||||||
resolveArrival.current?.();
|
resolveArrival.current?.();
|
||||||
|
arrivalSettled = true;
|
||||||
|
window.history.scrollRestoration = "auto";
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user