Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.

Expand scroll restoration to both X/Y scrolls

Changed files
+28 -21
src
+25 -18
src/useScrollRestoration.ts
···
import { useLayoutEffect } from './utils/react';
-
import { observeScrollHeight } from './utils/observeScrollHeight';
import { Ref } from './types';
const getIdForState = (() => {
···
};
})();
-
const scrollPositions: Record<string, number> = {};
export function useScrollRestoration<T extends HTMLElement>(
ref: 'window' | Ref<T>
···
const id = `${addonId}${getIdForState(
event ? event.state : history.state
)}:${window.location}`;
-
const scrollHeight =
-
ref === 'window'
-
? document.body.scrollHeight
-
: ref.current!.scrollHeight;
-
const scrollY = scrollPositions[id];
-
if (!scrollY) {
// noop
-
} else if (scrollHeight >= scrollY) {
-
scrollTarget.scrollTo(0, scrollY);
} else {
if (unsubscribe) unsubscribe();
-
unsubscribe = observeScrollHeight(
ref === 'window' ? document.body : ref.current!,
-
(scrollHeight: number) => {
-
// the scroll position shouldn't have changed by more than half the screen height
const hasMoved =
-
Math.abs(scrollY - scrollPositions[id]) > window.innerHeight / 2;
// then we restore the position as it's now possible
-
if (!hasMoved && scrollHeight >= scrollY)
-
scrollTarget.scrollTo(0, scrollY);
-
if (unsubscribe) unsubscribe();
}
);
}
···
const id = `${addonId}${getIdForState(history.state)}:${window.location}`;
const scrollY =
ref === 'window' ? window.scrollY : ref.current!.scrollTop;
-
scrollPositions[id] = scrollY || 0;
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
···
import { useLayoutEffect } from './utils/react';
+
import { observeScrollArea } from './utils/observeScrollArea';
import { Ref } from './types';
const getIdForState = (() => {
···
};
})();
+
const scrollPositions: Record<string, [number, number]> = {};
export function useScrollRestoration<T extends HTMLElement>(
ref: 'window' | Ref<T>
···
const id = `${addonId}${getIdForState(
event ? event.state : history.state
)}:${window.location}`;
+
const { scrollWidth, scrollHeight } = scrollTarget;
+
const scrollTo = scrollPositions[id];
+
if (!scrollTo) {
// noop
+
} else if (scrollWidth >= scrollTo[0] && scrollHeight >= scrollTo[1]) {
+
scrollTarget.scrollTo(scrollTo[0], scrollTo[1]);
} else {
if (unsubscribe) unsubscribe();
+
unsubscribe = observeScrollArea(
ref === 'window' ? document.body : ref.current!,
+
(scrollWidth: number, scrollHeight: number) => {
+
// check whether the scroll position has already moved too far
+
const halfViewportX = window.innerWidth / 2;
+
const halfViewportY = window.innerHeight / 2;
+
const newScrollTo = scrollPositions[id];
const hasMoved =
+
Math.abs(scrollTo[0] - newScrollTo[0]) > halfViewportX ||
+
Math.abs(scrollTo[1] - newScrollTo[1]) > halfViewportY;
// then we restore the position as it's now possible
+
if (
+
hasMoved ||
+
(scrollWidth >= scrollTo[0] && scrollHeight >= scrollTo[1])
+
) {
+
if (!hasMoved) scrollTarget.scrollTo(scrollTo[0], scrollTo[1]);
+
if (unsubscribe) unsubscribe();
+
}
}
);
}
···
const id = `${addonId}${getIdForState(history.state)}:${window.location}`;
const scrollY =
ref === 'window' ? window.scrollY : ref.current!.scrollTop;
+
const scrollX =
+
ref === 'window' ? window.scrollX : ref.current!.scrollLeft;
+
scrollPositions[id] = [scrollX, scrollY];
if (unsubscribe) {
unsubscribe();
unsubscribe = undefined;
+3 -3
src/utils/observeScrollHeight.ts src/utils/observeScrollArea.ts
···
}
});
-
export function observeScrollHeight(
element: HTMLElement,
-
onScrollHeightChange: (scrollHeight: number) => void
): () => void {
const listeners = resizeListeners.get(element) || [];
const isFirstListener = !listeners.length;
···
const onResize = () => {
const scrollHeight = element.scrollHeight || 0;
if (!hasUnmounted && scrollHeight !== previousScrollHeight) {
-
onScrollHeightChange(element.scrollHeight);
previousScrollHeight = scrollHeight;
}
};
···
}
});
+
export function observeScrollArea(
element: HTMLElement,
+
onAreaChange: (width: number, height: number) => void
): () => void {
const listeners = resizeListeners.get(element) || [];
const isFirstListener = !listeners.length;
···
const onResize = () => {
const scrollHeight = element.scrollHeight || 0;
if (!hasUnmounted && scrollHeight !== previousScrollHeight) {
+
onAreaChange(element.scrollWidth, element.scrollHeight);
previousScrollHeight = scrollHeight;
}
};