UNPKG

@syncfusion/react-popups

Version:

A package of Pure React popup components such as Tooltip that is used to display information or messages in separate pop-ups.

256 lines (255 loc) 11.2 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { preRender } from '@syncfusion/react-base'; import { useState, useEffect, useRef, useImperativeHandle, forwardRef, useMemo } from 'react'; import * as ReactDOM from 'react-dom'; const DEFT_WIDTHS = { Material3: 30, Fluent2: 30, Bootstrap5: 36, Tailwind3: 30 }; const CLS_MAPPINGS = { Material3: 'sf-spin-material3', Fluent2: 'sf-spin-fluent2', Bootstrap5: 'sf-spin-bootstrap5', Tailwind3: 'sf-spin-tailwind3' }; const CLS_SPINWRAP = 'sf-spinner-pane'; const CLS_SPININWRAP = 'sf-spinner-inner'; const CLS_SPINCIRCLE = 'sf-path-circle'; const CLS_SPINARC = 'sf-path-arc'; const CLS_SPINLABEL = 'sf-spin-label'; const CLS_SPINTEMPLATE = 'sf-spin-template'; /** * Defines the available design systems for spinner appearance. * Each option represents a different visual style based on popular UI frameworks. */ export var SpinnerType; (function (SpinnerType) { /** * Material Design 3 spinner style with circular animation following * Google's Material Design guidelines. */ SpinnerType["Material3"] = "Material3"; /** * Bootstrap 5 spinner style following the Bootstrap framework's * visual design patterns. */ SpinnerType["Bootstrap5"] = "Bootstrap5"; /** * Fluent Design 2 spinner style following Microsoft's Fluent design * system guidelines. */ SpinnerType["Fluent2"] = "Fluent2"; /** * Tailwind CSS 3 spinner style following the Tailwind design * aesthetic and principles. */ SpinnerType["Tailwind3"] = "Tailwind3"; })(SpinnerType || (SpinnerType = {})); const globalTemplate = null; const globalCssClass = null; const globalType = null; const spinnerInstances = []; /** * A versatile Spinner component that provides visual feedback for loading states. * * The Spinner supports multiple design systems through the SpinnerType enum * and can be customized with various properties for size, color, and behavior. * * ```typescript * <Spinner * type={SpinnerType.Material3} * visible={true} * /> * ``` */ export const Spinner = forwardRef((props, ref) => { const animationFrameRef = useRef(null); const { className = '', label, width, visible = false, template, target, type: propType, ...restProps } = props; const [show, setIsVisible] = useState(visible); const type = propType || globalType || null; const spinnerRef = useRef(null); const targetRef = useRef(null); useImperativeHandle(ref, () => ({ element: spinnerRef.current })); useEffect(() => { spinnerInstances.push(ref); return () => { const index = spinnerInstances.indexOf(ref); if (index > -1) { spinnerInstances.splice(index, 1); } }; }, [ref]); useEffect(() => { preRender('spinner'); }, []); useEffect(() => { setIsVisible(visible); }, [visible]); useEffect(() => { if (target) { targetRef.current = typeof target === 'string' ? document.querySelector(target) : target; } }, [target]); const calculateRadius = useMemo(() => { const baseWidth = DEFT_WIDTHS[type || 'Material3']; const parsedWidth = width !== undefined ? parseFloat(width.toString()) : baseWidth; return parsedWidth / (2); }, [width, type]); const getSpinnerClassNames = () => { return [ CLS_SPINWRAP, className || globalCssClass, show ? 'sf-spin-show' : 'sf-spin-hide', template || globalTemplate ? CLS_SPINTEMPLATE : '' ].filter(Boolean).join(' '); }; const useAnimatedRotation = (deps = []) => { const [rotation, setRotation] = useState(0); useEffect(() => { if (!show) { return; } const interval = setInterval(() => { setRotation((prev) => (prev + 45) % 360); }, 100); return () => clearInterval(interval); }, [show, ...deps]); return rotation; }; const randomGenerator = () => { const combine = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; return Array.from({ length: 5 }, () => combine.charAt(Math.floor(Math.random() * combine.length))).join(''); }; const getStrokeSize = (diameter) => { return (10 / 100) * diameter; }; const drawArc = (diameter, strokeSize) => { const radius = diameter / 2; const offset = strokeSize / 2; return `M${radius},${offset}A${radius - offset},${radius - offset} 0 1 1 ${offset},${radius}`; }; const defineCircle = (x, y, radius) => { return `M ${x} ${y} m ${-radius} 0 a ${radius} ${radius} 0 1 0 ${radius * 2} 0 a ${radius} ${radius} 0 1 0 ${-radius * 2} 0`; }; const defineArc = (x, y, radius, startArc, endArc) => { const start = defineArcPoints(x, y, radius, endArc); const end = defineArcPoints(x, y, radius, startArc); return `M ${start.x} ${start.y} A ${radius} ${radius} 0 0 0 ${end.x} ${end.y}`; }; const defineArcPoints = (centerX, centerY, radius, angle) => { const radians = (angle - 90) * Math.PI / 180.0; return { x: centerX + (radius * Math.cos(radians)), y: centerY + (radius * Math.sin(radians)) }; }; const renderBootstrapLikeSpinner = (spinnerType) => { const uniqueID = useRef(randomGenerator()).current; const rotation = useAnimatedRotation(); const className = spinnerType; return show ? (_jsx("svg", { id: uniqueID, className: CLS_MAPPINGS[className], viewBox: `0 0 ${calculateRadius * 2} ${calculateRadius * 2}`, style: { width: `${calculateRadius * 2}px`, height: `${calculateRadius * 2}px`, transform: `rotate(${rotation}deg)`, transition: 'transform 0.1s linear' }, children: _jsx("path", { className: CLS_SPINCIRCLE, d: drawArc(calculateRadius * 2, getStrokeSize(calculateRadius * 2)), strokeWidth: getStrokeSize(calculateRadius * 2), fill: "none" }) })) : null; }; const getDashOffset = (diameter, strokeSize, value, max) => { return (diameter - strokeSize) * Math.PI * ((3 * (max) / 100) - (value / 100)); }; const easeAnimation = (current, start, change, duration) => { const timestamp = (current /= duration) * current; const timecount = timestamp * current; return start + change * (6 * timecount * timestamp + -15 * timestamp * timestamp + 10 * timecount); }; const renderMaterialLikeSpinner = (spinnerType, radius) => { const uniqueID = useRef(randomGenerator()).current; const diameter = radius * 2; const strokeSize = getStrokeSize(diameter); const [offset, setOffset] = useState(getDashOffset(diameter, strokeSize, 1, 75)); const [rotation, setRotation] = useState(0); const startTimeRef = useRef(null); const rotationCountRef = useRef(0); useEffect(() => { if (!show) { return; } const animate = (timestamp) => { if (!startTimeRef.current) { startTimeRef.current = timestamp; } const elapsed = timestamp - startTimeRef.current; const duration = 1333; const progress = (elapsed % duration) / duration; const easedProgress = easeAnimation(progress, 0, 1, 1); const start = 1; const end = 149; const max = 75; const currentValue = start + (end - start) * easedProgress; setOffset(getDashOffset(diameter, strokeSize, currentValue, max)); if (elapsed >= duration) { rotationCountRef.current += 1; setRotation(rotationCountRef.current * -90); startTimeRef.current = timestamp; } animationFrameRef.current = requestAnimationFrame(animate); }; animationFrameRef.current = requestAnimationFrame(animate); return () => { if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); } }; }, [show, diameter, strokeSize]); let className; switch (spinnerType) { case 'Tailwind3': className = CLS_MAPPINGS['Tailwind3']; break; default: className = CLS_MAPPINGS['Material3']; } return show ? (_jsx("svg", { id: uniqueID, className: className, viewBox: `0 0 ${diameter} ${diameter}`, style: { width: `${diameter}px`, height: `${diameter}px`, transformOrigin: `${diameter / 2}px ${diameter / 2}px` }, children: _jsx("path", { className: CLS_SPINCIRCLE, d: drawArc(diameter, strokeSize), strokeWidth: strokeSize, strokeDasharray: ((diameter - strokeSize) * Math.PI * 0.75).toString(), strokeDashoffset: offset, transform: `rotate(${rotation} ${diameter / 2} ${diameter / 2})`, fill: "none" }) })) : null; }; const renderCommonSpinner = (spinnerType) => { const uniqueID = useRef(randomGenerator()).current; const className = spinnerType; return (_jsxs("svg", { id: uniqueID, className: CLS_MAPPINGS[className], viewBox: `0 0 ${calculateRadius * 2} ${calculateRadius * 2}`, style: { width: `${calculateRadius * 2}px`, height: `${calculateRadius * 2}px`, transformOrigin: 'center' }, children: [_jsx("path", { className: CLS_SPINCIRCLE, d: defineCircle(calculateRadius, calculateRadius, calculateRadius), fill: "none" }), _jsx("path", { className: CLS_SPINARC, d: defineArc(calculateRadius, calculateRadius, calculateRadius, 315, 45), fill: "none" })] })); }; const renderSpinnerContent = () => { const effectiveTemplate = template || globalTemplate; if (effectiveTemplate) { return _jsx("div", { dangerouslySetInnerHTML: { __html: effectiveTemplate } }); } const spinnerType = type || SpinnerType.Material3; const radius = calculateRadius; switch (spinnerType) { case 'Bootstrap5': return renderBootstrapLikeSpinner(spinnerType); case 'Fluent2': return renderCommonSpinner(spinnerType); case 'Material3': case 'Tailwind3': default: return renderMaterialLikeSpinner(spinnerType, radius); } }; const spinnerContent = (_jsx("div", { ref: spinnerRef, className: getSpinnerClassNames(), ...restProps, children: _jsxs("div", { className: CLS_SPININWRAP, "aria-disabled": "true", children: [renderSpinnerContent(), label && _jsx("div", { className: CLS_SPINLABEL, children: label })] }) })); if (targetRef.current) { return ReactDOM.createPortal(spinnerContent, targetRef.current); } return spinnerContent; }); export default Spinner;