@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
JavaScript
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;