UNPKG

masonic

Version:

<hr> <div align="center"> <h1 align="center"> 🧱 masonic </h1>

130 lines (118 loc) • 4.16 kB
import useEvent from "@react-hook/event"; import useLatest from "@react-hook/latest"; import { useThrottleCallback } from "@react-hook/throttle"; import * as React from "react"; /** * A hook that creates a callback for scrolling to a specific index in * the "items" array. * * @param positioner - A positioner created by the `usePositioner()` hook * @param options - Configuration options */ export function useScrollToIndex(positioner, options) { var _latestOptions$curren; const { align = "top", element = typeof window !== "undefined" && window, offset = 0, height = typeof window !== "undefined" ? window.innerHeight : 0 } = options; const latestOptions = useLatest({ positioner, element, align, offset, height }); const getTarget = React.useRef(() => { const latestElement = latestOptions.current.element; return latestElement && "current" in latestElement ? latestElement.current : latestElement; }).current; const [state, dispatch] = React.useReducer((state, action) => { const nextState = { position: state.position, index: state.index, prevTop: state.prevTop }; /* istanbul ignore next */ if (action.type === "scrollToIndex") { var _action$value; return { position: latestOptions.current.positioner.get((_action$value = action.value) !== null && _action$value !== void 0 ? _action$value : -1), index: action.value, prevTop: void 0 }; } else if (action.type === "setPosition") { nextState.position = action.value; } else if (action.type === "setPrevTop") { nextState.prevTop = action.value; } else if (action.type === "reset") { return defaultState; } return nextState; }, defaultState); const throttledDispatch = useThrottleCallback(dispatch, 15); // If we find the position along the way we can immediately take off // to the correct spot. useEvent(getTarget(), "scroll", () => { if (!state.position && state.index) { const position = latestOptions.current.positioner.get(state.index); if (position) { dispatch({ type: "setPosition", value: position }); } } }); // If the top changes out from under us in the case of dynamic cells, we // want to keep following it. const currentTop = state.index !== void 0 && ((_latestOptions$curren = latestOptions.current.positioner.get(state.index)) === null || _latestOptions$curren === void 0 ? void 0 : _latestOptions$curren.top); React.useEffect(() => { const target = getTarget(); if (!target) return; const { height, align, offset, positioner } = latestOptions.current; if (state.position) { let scrollTop = state.position.top; if (align === "bottom") { scrollTop = scrollTop - height + state.position.height; } else if (align === "center") { scrollTop -= (height - state.position.height) / 2; } target.scrollTo(0, Math.max(0, scrollTop += offset)); // Resets state after 400ms, an arbitrary time I determined to be // still visually pleasing if there is a slow network reply in dynamic // cells let didUnsubscribe = false; const timeout = setTimeout(() => !didUnsubscribe && dispatch({ type: "reset" }), 400); return () => { didUnsubscribe = true; clearTimeout(timeout); }; } else if (state.index !== void 0) { // Estimates the top based upon the average height of current cells let estimatedTop = positioner.shortestColumn() / positioner.size() * state.index; if (state.prevTop) estimatedTop = Math.max(estimatedTop, state.prevTop + height); target.scrollTo(0, estimatedTop); throttledDispatch({ type: "setPrevTop", value: estimatedTop }); } }, [currentTop, state, latestOptions, getTarget, throttledDispatch]); return React.useRef(index => { dispatch({ type: "scrollToIndex", value: index }); }).current; } const defaultState = { index: void 0, position: void 0, prevTop: void 0 };