UNPKG

react-transition-preset

Version:

Lightweight, zero-dependency transition component for React with common preset transition

538 lines (526 loc) 15.6 kB
'use strict'; var react = require('react'); var jsxRuntime = require('react/jsx-runtime'); // src/Transition.tsx // src/presets.ts var pop = { center: 0, top: -16, bottom: 16 }; var popIn = (from) => ({ in: { opacity: 1, transform: "scale(1)" }, out: { opacity: 0, transform: `scale(var(--transition-preset-pop-in-${from}-scale, 0.9)) translateY(var(--transition-preset-pop-in-${from}, ${pop[from]}px))` }, transitionProperty: "transform, opacity" }); var presets = { "fade": { in: { opacity: 1 }, out: { opacity: 0 }, transitionProperty: "opacity" }, "fade-up": { in: { opacity: 1, transform: "translateY(0)" }, out: { opacity: 0, transform: `translateY(var(--transition-preset-fade-up, -16px))` }, transitionProperty: "opacity, transform" }, "fade-down": { in: { opacity: 1, transform: "translateY(0)" }, out: { opacity: 0, transform: `translateY(var(--transtion-preset-fade-down, 16px)` }, transitionProperty: "opacity, transform" }, "fade-left": { in: { opacity: 1, transform: "translateX(0)" }, out: { opacity: 0, transform: `translateX(var(--transition-preset-fade-left, -16px))` }, transitionProperty: "opacity, transform" }, "fade-right": { in: { opacity: 1, transform: "translateX(0)" }, out: { opacity: 0, transform: `translateX(var(--transition-preset-fade-right, 16px)` }, transitionProperty: "opacity, transform" }, "scale": { in: { opacity: 1, transform: "scale(1)" }, out: { opacity: 0, transform: "scale(0)" }, common: { transformOrigin: "top" }, transitionProperty: "transform, opacity" }, "scale-y": { in: { opacity: 1, transform: "scaleY(1)" }, out: { opacity: 0, transform: "scaleY(0)" }, common: { transformOrigin: "top" }, transitionProperty: "transform, opacity" }, "scale-x": { in: { opacity: 1, transform: "scaleX(1)" }, out: { opacity: 0, transform: "scaleX(0)" }, common: { transformOrigin: "left" }, transitionProperty: "transform, opacity" }, "skew-up": { in: { opacity: 1, transform: "translateY(0) skew(0deg, 0deg)" }, out: { opacity: 0, transform: `translateY(var(--transition-preset-skew-up, -20px)) skew(var(--transition-preset-skew-up-deg, -10deg, -5deg))` }, common: { transformOrigin: "top" }, transitionProperty: "transform, opacity" }, "skew-down": { in: { opacity: 1, transform: "translateY(0) skew(0deg, 0deg)" }, out: { opacity: 0, transform: `translateY(var(--transition-preset-skew-down, 20px)) skew(var(--transition-preset-skew-down-deg, -10deg, -5deg))` }, common: { transformOrigin: "bottom" }, transitionProperty: "transform, opacity" }, "rotate-left": { in: { opacity: 1, transform: "translateY(0) rotate(0deg)" }, out: { opacity: 0, transform: `translateY(var(--transition-preset-rotate-left, 20px)) rotate(var(--transition-preset-rotate-left-deg, -5deg))` }, common: { transformOrigin: "bottom" }, transitionProperty: "transform, opacity" }, "rotate-right": { in: { opacity: 1, transform: "translateY(0) rotate(0deg)" }, out: { opacity: 0, transform: `translateY(var(--transition-preset-rotate-right, 20px)) rotate(var(--transition-preset-rotate-right-deg, 5deg))` }, common: { transformOrigin: "top" }, transitionProperty: "transform, opacity" }, "slide-down": { in: { opacity: 1, transform: "translateY(0)" }, out: { opacity: 0, transform: "translateY(100%)" }, common: { transformOrigin: "top" }, transitionProperty: "transform, opacity" }, "slide-up": { in: { opacity: 1, transform: "translateY(0)" }, out: { opacity: 0, transform: "translateY(-100%)" }, common: { transformOrigin: "bottom" }, transitionProperty: "transform, opacity" }, "slide-left": { in: { opacity: 1, transform: "translateX(0)" }, out: { opacity: 0, transform: "translateX(-100%)" }, common: { transformOrigin: "left" }, transitionProperty: "transform, opacity" }, "slide-right": { in: { opacity: 1, transform: "translateX(0)" }, out: { opacity: 0, transform: "translateX(100%)" }, common: { transformOrigin: "right" }, transitionProperty: "transform, opacity" }, "pop": { ...popIn("center"), common: { transformOrigin: "center center" } }, "pop-top": { ...popIn("top"), common: { transformOrigin: "top center" } }, "pop-bottom": { ...popIn("bottom"), common: { transformOrigin: "bottom center" } }, "pop-left": { ...popIn("center"), common: { transformOrigin: "center left" } }, "pop-right": { ...popIn("center"), common: { transformOrigin: "center right" } }, "pop-bottom-left": { ...popIn("bottom"), common: { transformOrigin: "bottom left" } }, "pop-bottom-right": { ...popIn("bottom"), common: { transformOrigin: "bottom right" } }, "pop-top-left": { ...popIn("top"), common: { transformOrigin: "top left" } }, "pop-top-right": { ...popIn("top"), common: { transformOrigin: "top right" } } }; var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"; var useIsomorphicLayoutEffect = isBrowser ? react.useLayoutEffect : react.useEffect; // src/hooks/use-did-update.ts function useDidUpdate(fn, options) { const { initialMounted, deps } = options; const mounted = react.useRef(initialMounted); useIsomorphicLayoutEffect( () => () => { mounted.current = initialMounted; }, [] ); useIsomorphicLayoutEffect(() => { if (mounted.current) { fn(); return; } mounted.current = true; return void 0; }, deps); } function useMemoizedFn(fn) { const fnRef = react.useRef(fn); fnRef.current = react.useMemo(() => fn, [fn]); const memoizedFn = react.useRef(); if (!memoizedFn.current) { memoizedFn.current = function(...args) { return fnRef.current.apply(this, args); }; } return memoizedFn.current; } // src/utils.ts function defaults(options, defaultOptions) { const result = { ...defaultOptions }; for (const key in options) { if (options[key] !== void 0) { result[key] = options[key]; } } return result; } function secToMs(ms) { return (ms || 0) * 1e3; } // src/use-transition.ts function useTransition(props) { const { duration: durationInSec, initial, exitDuration: exitDurationInSec, timingFunction, mounted, reduceMotion, onEnter, onExit, onEntered, onExited, enterDelay: enterDelayInSec, exitDelay: exitDelayInSec } = props; const [duration, exitDuration, enterDelay, exitDelay] = [ durationInSec, exitDurationInSec, enterDelayInSec, exitDelayInSec ].map(secToMs); const [transitionDuration, setTransitionDuration] = react.useState(reduceMotion ? 0 : duration); const [transitionStatus, setStatus] = react.useState(() => { if (mounted) { return initial ? "pre-entering" /* preEntering */ : "entered" /* entered */; } return "exited" /* exited */; }); const transitionTimeoutRef = react.useRef(-1); const delayTimeoutRef = react.useRef(-1); const rafRef = react.useRef(-1); const handleStateChange = useMemoizedFn((shouldMount) => { const preHandler = shouldMount ? onEnter : onExit; const handler = shouldMount ? onEntered : onExited; window.clearTimeout(transitionTimeoutRef.current); const newTransitionDuration = reduceMotion ? 0 : shouldMount ? duration : exitDuration; setTransitionDuration(newTransitionDuration); if (newTransitionDuration === 0) { typeof preHandler === "function" && preHandler(); typeof handler === "function" && handler(); setStatus(shouldMount ? "entered" /* entered */ : "exited" /* exited */); } else { rafRef.current = requestAnimationFrame(() => { setStatus(shouldMount ? "pre-entering" /* preEntering */ : "pre-exiting" /* preExiting */); rafRef.current = requestAnimationFrame(() => { typeof preHandler === "function" && preHandler(); setStatus(shouldMount ? "entering" /* entering */ : "exiting" /* exiting */); transitionTimeoutRef.current = window.setTimeout(() => { typeof handler === "function" && handler(); setStatus(shouldMount ? "entered" /* entered */ : "exited" /* exited */); }, newTransitionDuration); }); }); } }); const handleTransitionWithDelay = useMemoizedFn((shouldMount) => { window.clearTimeout(delayTimeoutRef.current); const delay = shouldMount ? enterDelay : exitDelay; if (typeof delay !== "number") { handleStateChange(shouldMount); return; } delayTimeoutRef.current = window.setTimeout( () => { handleStateChange(shouldMount); }, shouldMount ? enterDelay : exitDelay ); }); useDidUpdate( () => { handleTransitionWithDelay(mounted); }, { deps: [mounted], initialMounted: initial && mounted } ); useIsomorphicLayoutEffect( () => () => { window.clearTimeout(transitionTimeoutRef.current); cancelAnimationFrame(rafRef.current); }, [] ); return { transitionDuration, transitionStatus, transitionTimingFunction: timingFunction || "ease" }; } // src/get-transition-styles/get-transition-styles.ts var transitionStatuses = { ["entering" /* entering */]: "in", ["entered" /* entered */]: "in", ["exiting" /* exiting */]: "out", ["exited" /* exited */]: "out", ["pre-entering" /* preEntering */]: "out", ["pre-exiting" /* preExiting */]: "out" }; function getTransitionStyles({ transition, state, duration, timingFunction }) { const shared = { transitionDuration: `${duration}ms`, transitionTimingFunction: timingFunction }; if (typeof transition === "string") { if (!(transition in presets)) { return {}; } return { transitionProperty: presets[transition].transitionProperty, ...shared, ...presets[transition].common, ...presets[transition][transitionStatuses[state]] }; } return { transitionProperty: transition.transitionProperty, ...shared, ...transition.common, ...transition[transitionStatuses[state]] }; } // src/global-config.ts var defaultConfig = { transition: "fade", duration: 0.2, keepMounted: false, enterDelay: 0, exitDelay: 0, timingFunction: "ease", initial: false, reduceMotion: false }; var GlobalConfig = class { static config = defaultConfig; /** * 设置全局配置 */ static set(props) { this.config = this.addDefaults(props); return this.config; } /** * 从全局配置中合并配置, * 外部配置优先级高于全局配置, * 并不会改变全局配置 */ static merge(props) { const mergedValue = this.addDefaults(props); return mergedValue; } /** * 给配置添加默认值 */ static addDefaults(props) { const value = defaults(props, this.config); if (value.exitDuration === void 0) { value.exitDuration = value.duration; } return value; } static reset() { this.config = defaultConfig; } }; function setGlobalConfig(props) { GlobalConfig.set.call(GlobalConfig, props); } // src/viewport/index.ts var thresholds = { some: 0, all: 1 }; function resolveElements(elements) { if (typeof elements === "string") { elements = document.querySelectorAll(elements); } else if (elements instanceof Element) { elements = [elements]; } return Array.from(elements || []); } function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) { const elements = resolveElements(elementOrSelector); const activeIntersections = /* @__PURE__ */ new WeakMap(); const onIntersectionChange = (entries) => { entries.forEach((entry) => { const onEnd = activeIntersections.get(entry.target); if (entry.isIntersecting === Boolean(onEnd)) return; if (entry.isIntersecting) { const newOnEnd = onStart(entry); if (typeof newOnEnd === "function") { activeIntersections.set(entry.target, newOnEnd); } else { observer.unobserve(entry.target); } } else if (onEnd) { onEnd(entry); activeIntersections.delete(entry.target); } }); }; const observer = new IntersectionObserver(onIntersectionChange, { root, rootMargin, threshold: typeof amount === "number" ? amount : thresholds[amount] }); elements.forEach((element) => observer.observe(element)); return () => observer.disconnect(); } // src/hooks/use-in-view.ts function useInView(ref, { root, margin, amount = "some", once = false } = {}, options = {}) { const [isInView, setInView] = react.useState(false); const { enable } = options; react.useEffect(() => { if (!enable || !ref.current || once && isInView) return; const onEnter = () => { setInView(true); return once ? void 0 : () => setInView(false); }; const options2 = { root: root && root.current || void 0, margin, amount }; return inView(ref.current, onEnter, options2); }, [root, ref, margin, once, amount]); return isInView; } var Transition = ({ mounted: _mounted, children, onExit, onEntered, onEnter, onExited, viewport, unsafe_alwaysMounted, ...rest }) => { const { duration, enterDelay, exitDelay, exitDuration, initial, keepMounted, timingFunction, transition, reduceMotion } = GlobalConfig.merge(rest); const mountedInView = _mounted === "whileInView"; const el = react.useRef(null); const isInView = useInView(el, viewport, { enable: mountedInView }); const mounted = mountedInView ? isInView : _mounted; const { transitionDuration, transitionStatus, transitionTimingFunction } = useTransition({ mounted, initial, exitDuration, duration, timingFunction, onExit, onEntered, onEnter, onExited, enterDelay, exitDelay, reduceMotion }); const createChildren = useMemoizedFn((style, { mounted: mounted2 }) => { let element; if (mounted2 || keepMounted) { if (typeof children === "function") { element = children(style); } else { if (react.isValidElement(children)) { element = react.cloneElement(children, { style }); } else { element = children; } } } else { element = null; } if (!mountedInView) { return element; } const { placeholder, attributes } = viewport || {}; const Placeholder = placeholder || "div"; return /* @__PURE__ */ jsxRuntime.jsx(Placeholder, { ref: el, ...attributes, children: element }); }); const createTransitionChildren = useMemoizedFn(({ mounted: mounted2 }) => { return createChildren( getTransitionStyles({ transition, duration: transitionDuration, state: transitionStatus, timingFunction: transitionTimingFunction }), { mounted: mounted2 } ); }); const isExited = transitionStatus === "exited" /* exited */; if (isExited) { if (unsafe_alwaysMounted) { return createTransitionChildren({ mounted: false }); } return createChildren({ display: "none" }, { mounted: false }); } return createTransitionChildren({ mounted: true }); }; exports.Transition = Transition; exports.presets = presets; exports.setGlobalConfig = setGlobalConfig;