All files / comps Axis.js

0% Statements 0/30
0% Branches 0/35
0% Functions 0/3
0% Lines 0/30

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                                                                                                                                                                                                             
/*
 * Copyright (c) 2022-2023 Braun Nathanael
 *
 * This project is dual licensed under one of the following licenses:
 * - Creative Commons Attribution-NoDerivatives 4.0 International License.
 * - GNU AFFERO GENERAL PUBLIC LICENSE Version 3
 *
 * You should have received a copy of theses licenses along with this work.
 * If not, see <http://creativecommons.org/licenses/by-nd/4.0/> or <http://www.gnu.org/licenses/agpl-3.0.txt>.
 */
 
import deepEqual from "fast-deep-equal";
import React     from 'react';
import useVoodoo from "../hooks/useVoodoo";
 
/**
 * Axis — a zero-render declarative component that registers a scrollable animation
 * timeline with the nearest parent Tweener.
 *
 * Unlike most React components, the registration calls (initAxis / addScrollableAnim)
 * happen during render, not inside a useEffect. This is intentional: children that
 * mount in the same render pass (e.g. Node components) need the axis to already exist
 * so their tweenRef calls can resolve initial positions correctly. The component itself
 * renders nothing (`<React.Fragment/>`).
 *
 * Cleanup on unmount is handled by a single effect with an empty dependency array —
 * it removes the axis timeline from the tweener so that its numeric contributions are
 * zeroed and the CssTweenAxis instance is returned to the object pool.
 */
export default ( {
	                 children,
	                 id,
	                 axe = id,
	                 scrollFirst, bounds,
	                 scrollableWindow, inertia, size, defaultPosition,
	                 items = [],
                 } ) => {
	const µ         = React.useRef({}).current,
	      [tweener] = useVoodoo(true);
	
	// First render for this axis id — reset axis to defaultPosition so prior
	// scroll state from a different axis id does not bleed through.
	if ( !µ.previousAxis || µ.previousAxis !== axe ) {
		µ.previousAxis    = axe;
		µ.previousInertia = inertia;
		tweener.initAxis(axe, {
			inertia,
			scrollableArea: size,
			scrollableWindow,
			defaultPosition,
			scrollFirst,
			scrollableBounds: bounds
		}, true);
	}
	// Inertia config, bounds, or viewport window changed — re-init without resetting
	// the scroll position so the user's current scroll is preserved.
	else if ( !µ.previousInertia || µ.previousInertia !== inertia || µ.previousBounds !== bounds || µ.previousScrollableWindow !== scrollableWindow ) {
		µ.previousInertia          = inertia;
		µ.previousAxis             = axe;
		µ.previousBounds           = bounds;
		µ.previousScrollableWindow = scrollableWindow;
		tweener.initAxis(axe, {
			inertia,
			scrollableArea: size,
			scrollableWindow,
			defaultPosition,
			scrollFirst,
			scrollableBounds: bounds
		});
	}
	// Parent tweener changed (component was reparented) — unregister from the old
	// tweener and re-register with the new one to avoid orphaned timeline entries.
	if ( !µ.previousTweener || µ.previousTweener !== tweener ) {
		µ.previousTweener && µ.lastTL && µ.previousTweener.rmScrollableAnim(µ.lastTL, µ.previousAxis);
		if ( items.length )
			µ.lastTL = tweener.addScrollableAnim(items, axe, size);
		µ.previousTweener = tweener;
		µ.previousTweens  = items;
	}
	// Items array changed — rebuild the timeline. deepEqual guards against the common
	// pattern where the consumer creates a new array literal each render with identical
	// contents; without it every re-render would teardown and re-create the axis.
	else if ( µ.previousTweens !== items && !(µ.previousTweens && deepEqual(items, µ.previousTweens)) ) {
		µ.lastTL && µ.previousTweener && µ.previousTweener.rmScrollableAnim(µ.lastTL, µ.previousAxis);
		µ.lastTL = null;
		if ( items.length )
			µ.lastTL = tweener.addScrollableAnim(items, axe, size);
		µ.previousTweens = items;
	}
	
	React.useEffect(
		() => {
			
			return () => {
				µ.lastTL && µ.previousTweener && µ.previousTweener.rmScrollableAnim(µ.lastTL, µ.previousAxis);
				
				delete µ.previousTweener;
				delete µ.previousScrollable;
			}
		}, [])
	return <React.Fragment/>;
}