@syncfusion/react-base
Version:
A common package of core React base, methods and class definitions
442 lines (441 loc) • 16.2 kB
JavaScript
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 }));
});