mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: add spinner component (#16014)
This component is necessary to move forward with the "[Define a global button style](https://github.com/coder/coder/issues/14978)" issue, as it requires buttons to support loading states with spinners. The `Spinner` component is heavily inspired by the [Radix UI Spinner component](https://www.radix-ui.com/themes/docs/components/spinner) and was developed with the help of [v0](https://v0.dev/). **Preview**  --------- Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { Spinner } from "components/deprecated/Spinner/Spinner";
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
|
||||
interface LoaderProps extends HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
const meta: Meta<typeof Spinner> = {
|
||||
title: "components/Spinner",
|
||||
component: Spinner,
|
||||
args: {
|
||||
children: <PlusIcon className="size-icon-lg" />,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Spinner>;
|
||||
|
||||
export const Idle: Story = {};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: { loading: true },
|
||||
};
|
||||
@@ -1,22 +1,77 @@
|
||||
import CircularProgress, {
|
||||
type CircularProgressProps,
|
||||
} from "@mui/material/CircularProgress";
|
||||
import isChromatic from "chromatic/isChromatic";
|
||||
import type { FC } from "react";
|
||||
|
||||
/**
|
||||
* Spinner component used to indicate loading states. This component abstracts
|
||||
* the MUI CircularProgress to provide better control over its rendering,
|
||||
* especially in snapshot tests with Chromatic.
|
||||
* This component was inspired by
|
||||
* https://www.radix-ui.com/themes/docs/components/spinner and developed using
|
||||
* https://v0.dev/ help.
|
||||
*/
|
||||
export const Spinner: FC<CircularProgressProps> = (props) => {
|
||||
/**
|
||||
* During Chromatic snapshots, we render the spinner as determinate to make it
|
||||
* static without animations, using a deterministic value (75%).
|
||||
*/
|
||||
if (isChromatic()) {
|
||||
props.variant = "determinate";
|
||||
props.value = 75;
|
||||
|
||||
import isChromatic from "chromatic/isChromatic";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
const leaves = 8;
|
||||
|
||||
const spinnerVariants = cva("", {
|
||||
variants: {
|
||||
size: {
|
||||
lg: "size-icon-lg",
|
||||
sm: "size-icon-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "lg",
|
||||
},
|
||||
});
|
||||
|
||||
type SpinnerProps = React.SVGProps<SVGSVGElement> &
|
||||
VariantProps<typeof spinnerVariants> & {
|
||||
children?: ReactNode;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function Spinner({
|
||||
className,
|
||||
size,
|
||||
loading,
|
||||
children,
|
||||
...props
|
||||
}: SpinnerProps) {
|
||||
if (!loading) {
|
||||
return children;
|
||||
}
|
||||
return <CircularProgress {...props} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn(spinnerVariants({ size, className }))}
|
||||
{...props}
|
||||
>
|
||||
<title>Loading spinner</title>
|
||||
{[...Array(leaves)].map((_, i) => {
|
||||
const rotation = i * (360 / leaves);
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={i}
|
||||
x="10.9"
|
||||
y="2"
|
||||
width="2"
|
||||
height="5.5"
|
||||
rx="1"
|
||||
// 0.8 = leaves * 0.1
|
||||
className={
|
||||
isChromatic() ? "" : "animate-[loading_0.8s_ease-in-out_infinite]"
|
||||
}
|
||||
style={{
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
transformOrigin: "center",
|
||||
animationDelay: `${-i * 0.1}s`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import CircularProgress, {
|
||||
type CircularProgressProps,
|
||||
} from "@mui/material/CircularProgress";
|
||||
import isChromatic from "chromatic/isChromatic";
|
||||
import type { FC } from "react";
|
||||
|
||||
/**
|
||||
* Spinner component used to indicate loading states. This component abstracts
|
||||
* the MUI CircularProgress to provide better control over its rendering,
|
||||
* especially in snapshot tests with Chromatic.
|
||||
*
|
||||
* @deprecated prefer `components.Spinner`
|
||||
*/
|
||||
export const Spinner: FC<CircularProgressProps> = (props) => {
|
||||
/**
|
||||
* During Chromatic snapshots, we render the spinner as determinate to make it
|
||||
* static without animations, using a deterministic value (75%).
|
||||
*/
|
||||
if (isChromatic()) {
|
||||
props.variant = "determinate";
|
||||
props.value = 75;
|
||||
}
|
||||
return <CircularProgress {...props} />;
|
||||
};
|
||||
@@ -58,6 +58,15 @@ module.exports = {
|
||||
5: "hsl(var(--chart-5))",
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
loading: {
|
||||
"0%": { opacity: 0.85 },
|
||||
"25%": { opacity: 0.7 },
|
||||
"50%": { opacity: 0.4 },
|
||||
"75%": { opacity: 0.3 },
|
||||
"100%": { opacity: 0.2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
|
||||
Reference in New Issue
Block a user