Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | 33x 33x 33x 33x 33x 33x | import { useEffect, useRef } from "react" import { VisualElement } from "../../../render/types" import { AnimationType } from "../../../render/utils/types" import { warnOnce } from "../../../utils/warn-once" import { FeatureProps } from "../types" import { observeIntersection } from "./observers" import { ViewportOptions, ViewportState } from "./types" export function useViewport({ visualElement, whileInView, onViewportEnter, onViewportLeave, viewport = {}, }: FeatureProps) { const state = useRef<ViewportState>({ hasEnteredView: false, isInView: false, }) let shouldObserve = Boolean( whileInView || onViewportEnter || onViewportLeave ) if (viewport.once && state.current.hasEnteredView) shouldObserve = false const useObserver = typeof IntersectionObserver === "undefined" ? useMissingIntersectionObserver : useIntersectionObserver useObserver(shouldObserve, state.current, visualElement, viewport) } const thresholdNames = { some: 0, all: 1, } function useIntersectionObserver( shouldObserve: boolean, state: ViewportState, visualElement: VisualElement, { root, margin: rootMargin, amount = "some", once }: ViewportOptions ) { useEffect(() => { if (!shouldObserve) return const options = { root: root?.current, rootMargin, threshold: typeof amount === "number" ? amount : thresholdNames[amount], } const intersectionCallback = (entry: IntersectionObserverEntry) => { const { isIntersecting } = entry /** * If there's been no change in the viewport state, early return. */ if (state.isInView === isIntersecting) return state.isInView = isIntersecting /** * Handle hasEnteredView. If this is only meant to run once, and * element isn't visible, early return. Otherwise set hasEnteredView to true. */ if (once && !isIntersecting && state.hasEnteredView) { return } else if (isIntersecting) { state.hasEnteredView = true } visualElement.animationState?.setActive( AnimationType.InView, isIntersecting ) /** * Use the latest committed props rather than the ones in scope * when this observer is created */ const props = visualElement.getProps() const callback = isIntersecting ? props.onViewportEnter : props.onViewportLeave callback?.(entry) } return observeIntersection( visualElement.getInstance(), options, intersectionCallback ) }, [shouldObserve, root, rootMargin, amount]) } /** * If IntersectionObserver is missing, we activate inView and fire onViewportEnter * on mount. This way, the page will be in the state the author expects users * to see it in for everyone. */ function useMissingIntersectionObserver( shouldObserve: boolean, state: ViewportState, visualElement: VisualElement ) { useEffect(() => { if (!shouldObserve) return if (process.env.NODE_ENV !== "production") { warnOnce( false, "IntersectionObserver not available on this device. whileInView animations will trigger on mount." ) } /** * Fire this in an rAF because, at this point, the animation state * won't have flushed for the first time and there's certain logic in * there that behaves differently on the initial animation. * * This hook should be quite rarely called so setting this in an rAF * is preferred to changing the behaviour of the animation state. */ requestAnimationFrame(() => { state.hasEnteredView = true const { onViewportEnter } = visualElement.getProps() onViewportEnter?.(null) visualElement.animationState?.setActive(AnimationType.InView, true) }) }, [shouldObserve]) } |