Skip to content

Commit

Permalink
feat(spinner): new spinner variants (#4555)
Browse files Browse the repository at this point in the history
* refactor(spinner): add default variant

* feature(spinner): add gradient variant

* feature(spinner): add dots variant

* feature(spinner): add dots-blink variant

* feature(spinner): add spinner-bars

* chore(spinner): add variants storybook

* chore: adding variants to docs

* chore: simplyfying the styles and modifying docs

* chore: nits

* chore: updating the dots and dots-blink animation

* chore: nits

* chore: adding Marcus' suggestions

* chore: adding Marcus's suggestions

* chore: adding junior's suggestions

---------

Co-authored-by: Maharshi Alpesh <[email protected]>
  • Loading branch information
Peterl561 and macci001 authored Jan 30, 2025
1 parent a66476d commit 1965b84
Show file tree
Hide file tree
Showing 18 changed files with 324 additions and 32 deletions.
6 changes: 6 additions & 0 deletions .changeset/clever-pets-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@heroui/spinner": patch
"@heroui/theme": patch
---

Adding variants to the Spinner Component.
2 changes: 2 additions & 0 deletions apps/docs/content/components/spinner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import sizes from "./sizes";
import colors from "./colors";
import label from "./label";
import labelColors from "./label-colors";
import variants from "./variants";

export const spinnerContent = {
usage,
sizes,
colors,
label,
labelColors,
variants,
};
13 changes: 13 additions & 0 deletions apps/docs/content/components/spinner/variants.raw.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Spinner} from "@heroui/react";

export default function App() {
return (
<div className="flex flex-wrap items-end gap-8">
<Spinner classNames={{label: "text-primary-400 mt-4"}} label="default" variant="default" />
<Spinner classNames={{label: "text-primary-400 mt-4"}} label="gradient" variant="gradient" />
<Spinner classNames={{label: "text-primary-400 mt-4"}} label="spinner" variant="spinner" />
<Spinner classNames={{label: "text-primary-400 mt-4"}} label="wave" variant="wave" />
<Spinner classNames={{label: "text-primary-400 mt-4"}} label="dots" variant="dots" />
</div>
);
}
9 changes: 9 additions & 0 deletions apps/docs/content/components/spinner/variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./variants.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
9 changes: 9 additions & 0 deletions apps/docs/content/docs/api-references/heroui-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ The available options are:
- **Type**: `"user" | "always" | "never"`
- **Default**: `"never"`

`spinnerVariant`

- **Description**: The default variant of the spinner.
- **Type**: `string` | `undefined`
- **Possible Values**: `default` | `gradient` | `wave` | `dots` | `spinner`
- **Default**: `default`

<Spacer y={2}/>

---

## Types
Expand Down
18 changes: 15 additions & 3 deletions apps/docs/content/docs/components/spinner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,18 @@ Spinner express an unspecified wait time or display the length of a process.

<CodeDemo title="Label colors" files={spinnerContent.labelColors} />

### Variants

<CodeDemo title="Variants" files={spinnerContent.variants} />

## Slots

- **base**: The base slot of the spinner, it wraps the circles and the label.
- **wrapper**: The wrapper of the circles.
- **circle1**: The first circle of the spinner.
- **circle2**: The second circle of the spinner.
- **circle1**: The first circle of the spinner component. (Effective only when variant is `default` or `gradient`)
- **circle2**: The second circle of the spinner component. (Effective only when variant is `default` or `gradient`)
- **dots**: Dots of the spinner component. (Effective only when variant is `wave` or `dots`)
- **spinnerBars**: Bars of the spinner component. (Effective only when variant is `spinner`)
- **label**: The label content.

<Spacer y={4} />
Expand Down Expand Up @@ -94,6 +100,12 @@ Spinner express an unspecified wait time or display the length of a process.
description: "The color of the spinner circles.",
default: "primary"
},
{
attribute: "variant",
type: "default | gradient | wave | dots | spinner",
description: "The variant of the spinner",
default: "default"
},
{
attribute: "labelColor",
type: "default | primary | secondary | success | warning | danger",
Expand All @@ -102,7 +114,7 @@ Spinner express an unspecified wait time or display the length of a process.
},
{
attribute: "classNames",
type: "Partial<Record<\"base\"\"wrapper\"\"circle1\"\"circle2\"\"label\", string>>",
type: "Partial<Record<'base' | 'wrapper' | 'circle1' | 'circle2' | 'dots' | 'spinnerBars' | 'label', string>>",
description: "Allows to set custom class names for the spinner slots.",
default: "-"
}
Expand Down
5 changes: 3 additions & 2 deletions packages/components/spinner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0",
"@heroui/theme": ">=2.4.0"
"@heroui/theme": ">=2.4.7"
},
"dependencies": {
"@heroui/system-rsc": "workspace:*",
"@heroui/shared-utils": "workspace:*",
"@heroui/react-utils": "workspace:*"
"@heroui/react-utils": "workspace:*",
"@heroui/system": "workspace:*"
},
"devDependencies": {
"@heroui/theme": "workspace:*",
Expand Down
44 changes: 43 additions & 1 deletion packages/components/spinner/src/spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,49 @@ import {UseSpinnerProps, useSpinner} from "./use-spinner";
export interface SpinnerProps extends UseSpinnerProps {}

const Spinner = forwardRef<"div", SpinnerProps>((props, ref) => {
const {slots, classNames, label, getSpinnerProps} = useSpinner({...props});
const {slots, classNames, label, variant, getSpinnerProps} = useSpinner({...props});

if (variant === "wave" || variant === "dots") {
return (
<div ref={ref} {...getSpinnerProps()}>
<div className={slots.wrapper({class: classNames?.wrapper})}>
{[...new Array(3)].map((_, index) => (
<i
key={`dot-${index}`}
className={slots.dots({class: classNames?.dots})}
style={
{
"--dot-index": index,
} as React.CSSProperties
}
/>
))}
</div>
{label && <span className={slots.label({class: classNames?.label})}>{label}</span>}
</div>
);
}

if (variant === "spinner") {
return (
<div ref={ref} {...getSpinnerProps()}>
<div className={slots.wrapper({class: classNames?.wrapper})}>
{[...new Array(12)].map((_, index) => (
<i
key={`star-${index}`}
className={slots.spinnerBars({class: classNames?.spinnerBars})}
style={
{
"--bar-index": index,
} as React.CSSProperties
}
/>
))}
</div>
{label && <span className={slots.label({class: classNames?.label})}>{label}</span>}
</div>
);
}

return (
<div ref={ref} {...getSpinnerProps()}>
Expand Down
6 changes: 5 additions & 1 deletion packages/components/spinner/src/use-spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {mapPropsVariants} from "@heroui/system-rsc";
import {spinner} from "@heroui/theme";
import {clsx, objectToDeps} from "@heroui/shared-utils";
import {useMemo, useCallback, Ref} from "react";
import {useProviderContext} from "@heroui/system";

interface Props extends HTMLHeroUIProps<"div"> {
/**
Expand Down Expand Up @@ -38,6 +39,9 @@ export type UseSpinnerProps = Props & SpinnerVariantProps;
export function useSpinner(originalProps: UseSpinnerProps) {
const [props, variantProps] = mapPropsVariants(originalProps, spinner.variantKeys);

const globalContext = useProviderContext();
const variant = originalProps?.variant ?? globalContext?.spinnerVariant ?? "default";

const {children, className, classNames, label: labelProp, ...otherProps} = props;

const slots = useMemo(() => spinner({...variantProps}), [objectToDeps(variantProps)]);
Expand Down Expand Up @@ -65,7 +69,7 @@ export function useSpinner(originalProps: UseSpinnerProps) {
[ariaLabel, slots, baseStyles, otherProps],
);

return {label, slots, classNames, getSpinnerProps};
return {label, slots, classNames, variant, getSpinnerProps};
}

export type UseSpinnerReturn = ReturnType<typeof useSpinner>;
31 changes: 30 additions & 1 deletion packages/components/spinner/stories/spinner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import {Meta} from "@storybook/react";
import {spinner} from "@heroui/theme";

import {Spinner} from "../src";
import {Spinner, SpinnerProps} from "../src";

export default {
title: "Components/Spinner",
Expand All @@ -26,6 +26,12 @@ export default {
},
options: ["sm", "md", "lg"],
},
variant: {
control: {
type: "select",
},
options: ["default", "gradient", "spinner", "wave", "dots"],
},
},
decorators: [
(Story) => (
Expand All @@ -40,6 +46,18 @@ const defaultProps = {
...spinner.defaultVariants,
};

const VariantsTemplate = (args: SpinnerProps) => {
return (
<div className="flex flex-wrap items-end gap-8 py-4">
<Spinner {...args} label="default" variant="default" />
<Spinner {...args} label="gradient" variant="gradient" />
<Spinner {...args} label="spinner" variant="spinner" />
<Spinner {...args} label="wave" variant="wave" />
<Spinner {...args} label="dots" variant="dots" />
</div>
);
};

export const Default = {
args: {
...defaultProps,
Expand All @@ -52,3 +70,14 @@ export const WithLabel = {
label: "Loading...",
},
};

export const Variants = {
args: {
...defaultProps,
classNames: {
label: "text-primary-400 mt-4",
},
},

render: VariantsTemplate,
};
7 changes: 6 additions & 1 deletion packages/core/system/src/provider-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {SupportedCalendars} from "./types";
import type {SpinnerVariants, SupportedCalendars} from "./types";
import type {Calendar} from "@internationalized/date";
import type {DateValue} from "@react-types/datepicker";

Expand Down Expand Up @@ -87,6 +87,11 @@ export type ProviderContextProps = {
* @default all calendars
*/
createCalendar?: (calendar: SupportedCalendars) => Calendar | null;
/**
* The default variant of the spinner.
* @default default
*/
spinnerVariant?: SpinnerVariants;
};

export const [ProviderContext, useProviderContext] = createContext<ProviderContextProps>({
Expand Down
3 changes: 3 additions & 0 deletions packages/core/system/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const HeroUIProvider: React.FC<HeroUIProviderProps> = ({
// then they will be set in `use-date-input.ts` or `use-calendar-base.ts`
defaultDates,
createCalendar,
spinnerVariant,
...otherProps
}) => {
let contents = children;
Expand All @@ -87,6 +88,7 @@ export const HeroUIProvider: React.FC<HeroUIProviderProps> = ({
disableRipple,
validationBehavior,
labelPlacement,
spinnerVariant,
};
}, [
createCalendar,
Expand All @@ -96,6 +98,7 @@ export const HeroUIProvider: React.FC<HeroUIProviderProps> = ({
disableRipple,
validationBehavior,
labelPlacement,
spinnerVariant,
]);

return (
Expand Down
5 changes: 5 additions & 0 deletions packages/core/system/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ export type SupportedCalendars =
| "persian"
| "roc"
| "gregory";

/**
* Spinner Variants
*/
export type SpinnerVariants = "default" | "gradient" | "wave" | "dots" | "spinner";
33 changes: 33 additions & 0 deletions packages/core/theme/src/animations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export const animations = {
"drip-expand": "drip-expand 420ms linear",
"spinner-ease-spin": "spinner-spin 0.8s ease infinite",
"spinner-linear-spin": "spinner-spin 0.8s linear infinite",
sway: "sway 750ms ease infinite",
blink: "blink 1.4s infinite both",
"fade-out": "fade-out 1.2s linear 0s infinite normal none running",
"appearance-in": "appearance-in 250ms ease-out normal both",
"appearance-out": "appearance-out 60ms ease-in normal both",
"indeterminate-bar":
Expand Down Expand Up @@ -67,5 +70,35 @@ export const animations = {
transform: "translateX(100%) scaleX(1)",
},
},
sway: {
"0%": {
transform: "translate(0px, 0px)",
},
"50%": {
transform: "translate(0px, -150%)",
},
"100%": {
transform: "translate(0px, 0px)",
},
},
blink: {
"0%": {
opacity: "0.2",
},
"20%": {
opacity: "1",
},
"100%": {
opacity: "0.2",
},
},
"fade-out": {
"0%": {
opacity: "1",
},
"100%": {
opacity: "0.15",
},
},
},
};
Loading

0 comments on commit 1965b84

Please sign in to comment.