172 lines
4.2 KiB
TypeScript
172 lines
4.2 KiB
TypeScript
import { forwardRef } from "react";
|
|
import { Button, ButtonProps, CircularProgress } from "@mui/material";
|
|
import { alpha, keyframes, styled } from "@mui/material/styles";
|
|
import type { Theme } from "@mui/material/styles";
|
|
|
|
type AnimatedCircleButtonProps = ButtonProps & {
|
|
disableAnimation?: boolean;
|
|
loading?: boolean;
|
|
};
|
|
|
|
type StyledButtonProps = AnimatedCircleButtonProps & { theme: Theme };
|
|
|
|
const loadingPulse = keyframes`
|
|
0% {
|
|
transform: translate(-50%, -50%) scale(0.6);
|
|
opacity: 0.35;
|
|
}
|
|
50% {
|
|
transform: translate(-50%, -50%) scale(1.45);
|
|
opacity: 0.15;
|
|
}
|
|
100% {
|
|
transform: translate(-50%, -50%) scale(0.6);
|
|
opacity: 0;
|
|
}
|
|
`;
|
|
|
|
const StyledButton = styled(Button, {
|
|
shouldForwardProp: (prop) =>
|
|
prop !== "disableAnimation" && prop !== "loading",
|
|
})<AnimatedCircleButtonProps>((props: StyledButtonProps) => {
|
|
const {
|
|
theme,
|
|
disableAnimation = false,
|
|
color,
|
|
variant = "text",
|
|
disabled = false,
|
|
loading = false,
|
|
} = props;
|
|
|
|
const shouldAnimate = !disableAnimation && (!disabled || loading);
|
|
const pointerBlocked = loading;
|
|
|
|
const paletteMainMap: Record<string, string> = {
|
|
primary: theme.palette.primary.main,
|
|
secondary: theme.palette.secondary.main,
|
|
error: theme.palette.error.main,
|
|
warning: theme.palette.warning.main,
|
|
info: theme.palette.info.main,
|
|
success: theme.palette.success.main,
|
|
inherit: theme.palette.primary.main,
|
|
};
|
|
|
|
const paletteMain =
|
|
(color && paletteMainMap[String(color)]) ?? theme.palette.primary.main;
|
|
|
|
const pulseColor =
|
|
variant === "outlined" || variant === "text"
|
|
? alpha(paletteMain, 0.18)
|
|
: alpha(paletteMain, 0.3);
|
|
|
|
return {
|
|
position: "relative",
|
|
overflow: "hidden",
|
|
borderRadius: 5,
|
|
zIndex: 0,
|
|
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
pointerEvents: pointerBlocked ? "none" : undefined,
|
|
"&::after": shouldAnimate
|
|
? {
|
|
content: '""',
|
|
position: "absolute",
|
|
width: "12px",
|
|
height: "12px",
|
|
backgroundColor: pulseColor,
|
|
borderRadius: "50%",
|
|
top: "50%",
|
|
left: "50%",
|
|
pointerEvents: "none",
|
|
zIndex: 0,
|
|
...(loading
|
|
? {
|
|
opacity: 0.35,
|
|
transform: "translate(-50%, -50%) scale(0.6)",
|
|
animation: `${loadingPulse} 1.2s ease-in-out infinite`,
|
|
}
|
|
: {
|
|
opacity: 0,
|
|
transform: "translate(-50%, -50%) scale(0)",
|
|
transition: "transform 0.45s ease, opacity 0.45s ease",
|
|
}),
|
|
}
|
|
: {},
|
|
...(loading
|
|
? {}
|
|
: {
|
|
"&:hover": {
|
|
transform: "translateY(-1px)",
|
|
boxShadow: theme.shadows[4],
|
|
},
|
|
"&:hover::after": shouldAnimate
|
|
? {
|
|
transform: "translate(-50%, -50%) scale(15)",
|
|
opacity: 1,
|
|
}
|
|
: {},
|
|
"&:active": {
|
|
transform: "translateY(0)",
|
|
boxShadow: theme.shadows[2],
|
|
},
|
|
"&:active::after": shouldAnimate
|
|
? {
|
|
transform: "translate(-50%, -50%) scale(18)",
|
|
opacity: 0.4,
|
|
}
|
|
: {},
|
|
}),
|
|
"&.Mui-disabled": {
|
|
boxShadow: "none",
|
|
transform: "none",
|
|
...(loading && shouldAnimate
|
|
? {}
|
|
: {
|
|
"&::after": {
|
|
opacity: 0,
|
|
},
|
|
}),
|
|
},
|
|
...(disabled && {
|
|
boxShadow: "none",
|
|
transform: "none",
|
|
}),
|
|
"& > *": {
|
|
position: "relative",
|
|
zIndex: 1,
|
|
},
|
|
};
|
|
});
|
|
|
|
export const AnimatedCircleButton = forwardRef<
|
|
HTMLButtonElement,
|
|
AnimatedCircleButtonProps
|
|
>((props, ref) => {
|
|
const {
|
|
loading = false,
|
|
disabled,
|
|
children,
|
|
startIcon,
|
|
endIcon,
|
|
...rest
|
|
} = props;
|
|
|
|
const effectiveStartIcon = loading ? (
|
|
<CircularProgress size={16} color="inherit" />
|
|
) : (
|
|
startIcon
|
|
);
|
|
|
|
return (
|
|
<StyledButton
|
|
ref={ref}
|
|
loading={loading}
|
|
disabled={loading ? true : disabled}
|
|
startIcon={effectiveStartIcon}
|
|
endIcon={loading ? undefined : endIcon}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</StyledButton>
|
|
);
|
|
});
|