UNPKG

@syncfusion/react-base

Version:

A common package of core React base, methods and class definitions

442 lines (441 loc) 16.2 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { forwardRef, cloneElement, useRef, useState, useMemo, useCallback, useEffect, useImperativeHandle } from 'react'; import { useProviderContext } from './provider'; import { getActualElement, getElementRef } from './util'; /** * Collection of easing functions represented as cubic-bezier values * * @type {Record<string, string>} */ const easing = { ease: 'cubic-bezier(0.250, 0.100, 0.250, 1.000)', linear: 'cubic-bezier(0.250, 0.250, 0.750, 0.750)', easeIn: 'cubic-bezier(0.420, 0.000, 1.000, 1.000)', easeOut: 'cubic-bezier(0.000, 0.000, 0.580, 1.000)', easeInOut: 'cubic-bezier(0.420, 0.000, 0.580, 1.000)', elasticInOut: 'cubic-bezier(0.5,-0.58,0.38,1.81)', elasticIn: 'cubic-bezier(0.17,0.67,0.59,1.81)', elasticOut: 'cubic-bezier(0.7,-0.75,0.99,1.01)' }; /** * Maps animate categories to their corresponding effect types * * @type {Record<string, Effect[]>} */ const validEffectGroups = { 'Fade': ['FadeIn', 'FadeOut', 'FadeZoomIn', 'FadeZoomOut'], 'Flip': ['FlipLeftDownIn', 'FlipLeftDownOut', 'FlipLeftUpIn', 'FlipLeftUpOut', 'FlipRightDownIn', 'FlipRightDownOut', 'FlipRightUpIn', 'FlipRightUpOut', 'FlipXDownIn', 'FlipXDownOut', 'FlipXUpIn', 'FlipXUpOut', 'FlipYLeftIn', 'FlipYLeftOut', 'FlipYRightIn', 'FlipYRightOut'], 'Slide': ['SlideBottomIn', 'SlideBottomOut', 'SlideDown', 'SlideLeft', 'SlideLeftIn', 'SlideLeftOut', 'SlideRight', 'SlideRightIn', 'SlideRightOut', 'SlideTopIn', 'SlideTopOut', 'SlideUp'], 'Zoom': ['ZoomIn', 'ZoomOut'] }; /** * The Animate component provides options to animate HTML DOM elements. * It allows wrapping content with animation effects that can be controlled via props. * * ```tsx * <Animate effect="FadeIn" duration={3000}> * <div>Animation content</div> * </Animate> * ``` */ export const Animate = forwardRef((props, ref) => { const { effect = 'FadeIn', duration = 400, timingFunction = 'ease', delay = 0, children, onProgress, onBegin, onEnd, onFail, componentType = 'Animation', className, style, appear = true, ...rest } = props; const elementRef = useRef(null); const animationIdRef = useRef(0); const isAnimatingRef = useRef(false); const timeStampRef = useRef(0); const prevTimeStampRef = useRef(0); const { animate } = useProviderContext(); const combinedRef = (node) => { elementRef.current = node; const childRef = getElementRef(children); if (typeof childRef === 'function') { childRef(node); } else if (childRef && 'current' in childRef) { childRef.current = node; } }; const reflow = (node) => node.scrollTop; const getTimingFunction = useCallback(() => { return (timingFunction in easing) ? easing[timingFunction] : timingFunction; }, [timingFunction]); const validateAnimationType = useCallback(() => { if (componentType === 'Animation') { return true; } return validEffectGroups[`${componentType}`]?.includes(effect) || false; }, [effect, componentType]); const stopAnimation = useCallback(() => { if (elementRef.current) { elementRef.current = getActualElement(elementRef); elementRef.current.style.animation = ''; isAnimatingRef.current = false; if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = 0; } if (onEnd) { const eventData = { element: elementRef.current, effect, duration, delay, timingFunction }; onEnd(eventData); } } }, [duration, delay, effect, onEnd, timingFunction]); const animationStep = useCallback((timestamp) => { try { if (!elementRef.current || !isAnimatingRef.current) { return; } elementRef.current = getActualElement(elementRef); prevTimeStampRef.current = prevTimeStampRef.current === 0 ? timestamp : prevTimeStampRef.current; timeStampRef.current = timestamp - prevTimeStampRef.current; if (timeStampRef.current === 0 && onBegin) { const eventData = { element: elementRef.current, effect, duration, delay, timingFunction: getTimingFunction(), timeStamp: 0 }; onBegin(eventData); } if (timeStampRef.current < duration && isAnimatingRef.current) { elementRef.current.style.animation = `${effect} ${duration}ms ${getTimingFunction()}`; if (onProgress) { const eventData = { element: elementRef.current, effect, duration, delay, timingFunction: getTimingFunction(), timeStamp: timeStampRef.current }; onProgress(eventData); } animationIdRef.current = requestAnimationFrame(animationStep); } else { if (elementRef.current) { elementRef.current.style.animation = ''; isAnimatingRef.current = false; } if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = 0; } if (onEnd && elementRef.current) { const eventData = { element: elementRef.current, effect, duration, delay, timingFunction: getTimingFunction(), timeStamp: timeStampRef.current }; onEnd(eventData); } } } catch (e) { if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = 0; } isAnimatingRef.current = false; if (onFail) { onFail(e); } } }, [duration, delay, effect, onBegin, onEnd, onFail, onProgress, getTimingFunction]); const startAnimation = useCallback(() => { if (!elementRef.current) { return undefined; } if (!validateAnimationType()) { if (onFail) { onFail({ element: getActualElement(elementRef), effect, duration, delay, timingFunction }); } return undefined; } elementRef.current = getActualElement(elementRef); timeStampRef.current = 0; prevTimeStampRef.current = 0; isAnimatingRef.current = true; reflow(elementRef.current); animationIdRef.current = requestAnimationFrame(animationStep); }, [animationStep]); useEffect(() => { if (!children) { return undefined; } if (!animate || !appear) { if (onBegin) { const eventData = { element: getActualElement(elementRef), effect, duration, delay, timingFunction }; onBegin(eventData); } if (onEnd) { const eventData = { element: getActualElement(elementRef), effect, duration, delay, timingFunction }; onEnd(eventData); } return undefined; } let timeoutId; if (delay > 0) { timeoutId = window.setTimeout(() => { startAnimation(); }, delay); } else { startAnimation(); } return () => { if (timeoutId) { window.clearTimeout(timeoutId); } if (elementRef.current) { elementRef.current = getActualElement(elementRef); elementRef.current.style.animation = ''; isAnimatingRef.current = false; if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = 0; } } }; }, [children, animate, delay, effect, duration, timingFunction, onBegin, onEnd, startAnimation, stopAnimation]); useImperativeHandle(ref, () => ({ element: getActualElement(elementRef), stop: stopAnimation })); return cloneElement(children, { ref: combinedRef, className: [ children.props.className, className ].filter(Boolean).join(' ') || undefined, style: { ...children.props.style, ...style }, ...rest }); }); export default Animate; /** * Fade component that automatically handles in/out transitions. * Uses FadeIn when `in=true` and FadeOut when `in=false`. * * @example * ```tsx * import { Fade } from '@syncfusion/react-base'; * * <Fade in={isVisible} duration={500}> * <div>Content to fade</div> * </Fade> * ``` */ export const Fade = forwardRef((props, ref) => { const { in: inProp = true, appear = true, duration = 400, timingFunction = 'ease', delay = 0, onEnd, ...rest } = props; const [hidden, setHidden] = useState(() => { return !(inProp); }); const isExiting = useMemo(() => !inProp && !hidden, [inProp, hidden]); const prevInProp = useRef(inProp); useEffect(() => { if (prevInProp.current !== inProp) { if (inProp && hidden) { setHidden(false); } prevInProp.current = inProp; } }, [inProp, hidden]); const handleAnimationEnd = useCallback((args) => { if (!inProp) { setHidden(true); } if (onEnd) { onEnd(args); } }, [inProp, onEnd]); if (hidden) { return null; } const effect = isExiting ? 'FadeOut' : 'FadeIn'; return (_jsx(Animate, { ref: ref, appear: appear, effect: effect, duration: duration, timingFunction: timingFunction, delay: delay, onEnd: handleAnimationEnd, componentType: "Fade", style: { visibility: isExiting ? 'hidden' : 'visible', transition: `visibility ${Math.max(1, Math.floor(duration * 0.95))}ms ${timingFunction} ${delay}ms` }, ...rest })); }); /** * Zoom component that automatically handles in/out transitions. * Uses ZoomIn when `in=true` and ZoomOut when `in=false`. * * @example * ```tsx * import { Zoom } from '@syncfusion/react-base'; * * <Zoom in={isVisible} duration={600}> * <div>Content to zoom</div> * </Zoom> * ``` */ export const Zoom = forwardRef((props, ref) => { const { in: inProp = true, appear = true, duration = 400, timingFunction = 'ease', delay = 0, onEnd, ...rest } = props; const [hidden, setHidden] = useState(() => { return !(inProp); }); const isExiting = useMemo(() => !inProp && !hidden, [inProp, hidden]); const prevInProp = useRef(inProp); useEffect(() => { if (prevInProp.current !== inProp) { if (inProp && hidden) { setHidden(false); } prevInProp.current = inProp; } }, [inProp, hidden]); const handleAnimationEnd = useCallback((args) => { if (!inProp) { requestAnimationFrame(() => { setHidden(true); }); } if (onEnd) { onEnd(args); } }, [inProp, onEnd]); if (hidden) { return null; } const effect = isExiting ? 'ZoomOut' : 'ZoomIn'; return (_jsx(Animate, { ref: ref, appear: appear, effect: effect, duration: duration, timingFunction: timingFunction, delay: delay, onEnd: handleAnimationEnd, componentType: "Zoom", style: { visibility: isExiting ? 'hidden' : 'visible', transition: `visibility ${Math.max(1, Math.floor(duration * 0.95))}ms ${timingFunction} ${delay}ms` }, ...rest })); }); /** * Slide component that automatically handles in/out transitions with direction control. * Uses Slide{Direction}In when `in=true` and Slide{Direction}Out when `in=false`. * * @example * ```tsx * import { Slide } from '@syncfusion/react-base'; * * <Slide in={isVisible} direction="Left" duration={800}> * <div>Content to slide</div> * </Slide> * ``` */ export const Slide = forwardRef((props, ref) => { const { in: inProp = true, appear = true, direction = 'Top', duration = 400, timingFunction = 'ease', delay = 0, onEnd, ...rest } = props; const [hidden, setHidden] = useState(() => { return !(inProp); }); const isExiting = useMemo(() => !inProp && !hidden, [inProp, hidden]); const prevInProp = useRef(inProp); useEffect(() => { if (prevInProp.current !== inProp) { if (inProp && hidden) { setHidden(false); } prevInProp.current = inProp; } }, [inProp, hidden]); const handleAnimationEnd = useCallback((args) => { if (!inProp) { setHidden(true); } if (onEnd) { onEnd(args); } }, [inProp, onEnd]); if (hidden) { return null; } const effect = isExiting ? `Slide${direction}Out` : `Slide${direction}In`; return (_jsx(Animate, { ref: ref, appear: appear, effect: effect, duration: duration, timingFunction: timingFunction, delay: delay, onEnd: handleAnimationEnd, componentType: "Slide", style: { visibility: isExiting ? 'hidden' : 'visible', transition: `visibility ${Math.max(1, Math.floor(duration * 0.95))}ms ${timingFunction} ${delay}ms` }, ...rest })); }); /** * Flip component that automatically handles in/out transitions with direction control. * Uses Flip{Direction}In when `in=true` and Flip{Direction}Out when `in=false`. * * @example * ```tsx * import { Flip } from '@syncfusion/react-base'; * * <Flip in={isVisible} direction="YRight" duration={1200}> * <div>Content to flip</div> * </Flip> * ``` */ export const Flip = forwardRef((props, ref) => { const { in: inProp = true, appear = true, direction = 'XUp', duration = 400, timingFunction = 'ease', delay = 0, onEnd, ...rest } = props; const [hidden, setHidden] = useState(() => { return !(inProp); }); const isExiting = useMemo(() => !inProp && !hidden, [inProp, hidden]); const prevInProp = useRef(inProp); useEffect(() => { if (prevInProp.current !== inProp) { if (inProp && hidden) { setHidden(false); } prevInProp.current = inProp; } }, [inProp, hidden]); const handleAnimationEnd = useCallback((args) => { if (!inProp) { setHidden(true); } if (onEnd) { onEnd(args); } }, [inProp, onEnd]); if (hidden) { return null; } const effect = isExiting ? `Flip${direction}Out` : `Flip${direction}In`; return (_jsx(Animate, { ref: ref, appear: appear, effect: effect, duration: duration, timingFunction: timingFunction, delay: delay, onEnd: handleAnimationEnd, componentType: "Flip", style: { visibility: isExiting ? 'hidden' : 'visible', transition: `visibility ${Math.max(1, Math.floor(duration * 0.95))}ms ${timingFunction} ${delay}ms` }, ...rest })); });