feat(spinner): needless spinners removed, main spinner migrated to tw

This commit is contained in:
Junior Garcia 2023-02-19 15:53:52 -03:00
parent 61910a84db
commit dd30f6c34d
30 changed files with 431 additions and 785 deletions

View File

@ -4,7 +4,7 @@ import {avatarGroup} from "@nextui-org/theme";
import {HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {ReactRef, clsx, getValidChildren, compact} from "@nextui-org/shared-utils";
import {cloneElement} from "react";
import {cloneElement, useMemo} from "react";
import {AvatarProps} from "./index";
@ -91,7 +91,7 @@ export function useAvatarGroup(props: UseAvatarGroupProps) {
isDisabled,
};
const styles = avatarGroup({className, isGrid});
const styles = useMemo(() => avatarGroup({className, isGrid}), [className, isGrid]);
const validChildren = getValidChildren(children);
const childrenWithinMax = max ? validChildren.slice(0, max) : validChildren;

View File

@ -133,16 +133,20 @@ export function useAvatar(props: UseAvatarProps) {
*/
const showFallback = (!src || !isImgLoaded) && showFallbackProp;
const slots = avatar({
color,
radius,
size,
isBordered,
isFocusVisible,
isDisabled,
isInGroup,
isInGridGroup: groupContext?.isGrid ?? false,
});
const slots = useMemo(
() =>
avatar({
color,
radius,
size,
isBordered,
isFocusVisible,
isDisabled,
isInGroup,
isInGridGroup: groupContext?.isGrid ?? false,
}),
[color, radius, size, isBordered, isFocusVisible, isDisabled, isInGroup, groupContext?.isGrid],
);
const buttonStyles = useMemo(() => {
if (as !== "button") return "";

View File

@ -42,13 +42,14 @@
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/drip": "workspace:*",
"@nextui-org/spinner": "workspace:*",
"@react-aria/button": "^3.6.2",
"@react-aria/interactions": "^3.12.0",
"@react-aria/utils": "^3.14.0",
"@react-aria/focus": "^3.9.0"
},
"devDependencies": {
"@nextui-org/loading": "workspace:*",
"@nextui-org/spinner": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@react-types/shared": "^3.15.0",
"@react-types/button": "^3.6.2",

View File

@ -13,7 +13,7 @@ import {useDrip} from "@nextui-org/drip";
import {useDOMRef} from "@nextui-org/dom-utils";
import {warn, clsx} from "@nextui-org/shared-utils";
import {button} from "@nextui-org/theme";
import {isValidElement, cloneElement} from "react";
import {isValidElement, cloneElement, useMemo} from "react";
import {useButtonGroupContext} from "./button-group-context";
@ -81,17 +81,31 @@ export function useButton(props: UseButtonProps) {
autoFocus,
});
const styles = button({
size,
color,
variant,
radius,
fullWidth,
isDisabled,
isFocusVisible,
disableAnimation,
className,
});
const styles = useMemo(
() =>
button({
size,
color,
variant,
radius,
fullWidth,
isDisabled,
isFocusVisible,
disableAnimation,
className,
}),
[
size,
color,
variant,
radius,
fullWidth,
isDisabled,
isFocusVisible,
disableAnimation,
className,
],
);
const {onClick: onDripClickHandler, ...dripBindings} = useDrip(false, domRef);

View File

@ -1,8 +1,8 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {button} from "@nextui-org/theme";
// import {Loading} from "@nextui-org/loading";
import {Notification, Camera} from "@nextui-org/shared-icons";
import {Spinner} from "@nextui-org/spinner";
import {Button, ButtonProps} from "../src";
@ -70,6 +70,12 @@ IsDisabled.args = {
isDisabled: true,
};
export const DisableRipple = Template.bind({});
DisableRipple.args = {
...defaultProps,
disableRipple: true,
};
export const WithIcons = Template.bind({});
WithIcons.args = {
...defaultProps,
@ -77,32 +83,15 @@ WithIcons.args = {
rightIcon: <Camera />,
};
// export const Loadings = () => (
// <Grid.Container gap={2}>
// <Grid>
// <Button auto disabled color="primary" css={{px: "$13"}}>
// <Loading color="currentColor" size="sm" />
// </Button>
// </Grid>
// <Grid>
// <Button auto disabled color="secondary" css={{px: "$13"}}>
// <Loading color="currentColor" size="sm" type="spinner" />
// </Button>
// </Grid>
// <Grid>
// <Button auto disabled color="success" css={{px: "$13"}}>
// <Loading color="currentColor" size="sm" type="points" />
// </Button>
// </Grid>
// <Grid>
// <Button auto disabled color="warning" css={{px: "$13"}}>
// <Loading color="currentColor" size="sm" type="points-opacity" />
// </Button>
// </Grid>
// <Grid>
// <Button auto disabled color="error" css={{px: "$13"}}>
// <Loading color="currentColor" size="sm" type="spinner" />
// </Button>
// </Grid>
// </Grid.Container>
// );
export const IsLoading = Template.bind({});
IsLoading.args = {
...defaultProps,
color: "primary",
isDisabled: true,
children: (
<>
<Spinner color="white" size="sm" />
Button
</>
),
};

View File

@ -6,6 +6,7 @@ import {useLink as useAriaLink} from "@react-aria/link";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {ReactRef} from "@nextui-org/shared-utils";
import {useMemo} from "react";
interface Props extends HTMLNextUIProps<"a">, LinkVariantProps {
/**
@ -51,10 +52,14 @@ export function useLink(originalProps: UseLinkProps) {
otherProps.role = "link";
}
const styles = link({
...variantProps,
className,
});
const styles = useMemo(
() =>
link({
...variantProps,
className,
}),
[variantProps, className],
);
return {Component, as, styles, domRef, linkProps, showAnchorIcon, ...otherProps};
}

View File

@ -1,5 +0,0 @@
// export types
export type {LoadingProps} from "./loading";
// export component
export {default as Loading} from "./loading";

View File

@ -1,34 +0,0 @@
import {keyframes} from "@nextui-org/system";
export const blink = keyframes({
"0%": {
opacity: "0.2",
},
"20%": {
opacity: 1,
},
"100%": {
opacity: "0.2",
},
});
export const rotate = keyframes({
"0%": {
transform: "rotate(0deg)",
},
"100%": {
transform: "rotate(360deg)",
},
});
export const points = keyframes({
"0%": {
transform: "translate(0px, 0px)",
},
"50%": {
transform: "translate(0, calc(-$$loadingSize * 1.4))",
},
"100%": {
transform: "translate(0px, 0px)",
},
});

View File

@ -1,263 +0,0 @@
import {styled} from "@nextui-org/system";
import {rotate, points, blink} from "./loading.animations";
export const StyledLoadingContainer = styled("div", {
d: "flex",
fd: "column",
jc: "center",
ai: "center",
position: "relative",
});
export const StyledLoading = styled("span", {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
size: "100%",
dflex: "center",
bgColor: "transparent",
us: "none",
variants: {
size: {
xs: {
$$loadingSize: "$space$8",
$$loadingBorder: "$space$1",
},
sm: {
$$loadingSize: "$space$10",
$$loadingBorder: "$space$1",
},
md: {
$$loadingSize: "$space$12",
$$loadingBorder: "calc($space$1 * 1.5)",
},
lg: {
$$loadingSize: "$space$15",
$$loadingBorder: "$space$2",
},
xl: {
$$loadingSize: "$space$18",
$$loadingBorder: "$space$3",
},
},
type: {
default: {
d: "flex",
br: "$rounded",
position: "relative",
size: "$$loadingSize",
i: {
top: "0px",
size: "100%",
position: "absolute",
br: "inherit",
},
"._1": {
border: "$$loadingBorder solid $$loadingColor",
borderTop: "$$loadingBorder solid transparent",
borderLeft: "$$loadingBorder solid transparent",
borderRight: "$$loadingBorder solid transparent",
animation: `${rotate} 0.8s ease infinite`,
},
"._2": {
border: "$$loadingBorder dotted $$loadingColor",
borderTop: "$$loadingBorder solid transparent",
borderLeft: "$$loadingBorder solid transparent",
borderRight: "$$loadingBorder solid transparent",
animation: `${rotate} 0.8s linear infinite`,
opacity: 0.5,
},
"._3": {
display: "none",
},
},
points: {
d: "flex",
position: "relative",
transform: "translate(0, calc($$loadingSize * 0.6))",
i: {
size: "$$loadingSize",
margin: "0 3px",
bg: "$$loadingColor",
},
"._1": {
br: "$rounded",
animation: `${points} 0.75s ease infinite`,
},
"._2": {
br: "$rounded",
animation: `${points} 0.75s ease infinite 0.25s`,
},
"._3": {
br: "$rounded",
animation: `${points} 0.75s ease infinite 0.5s`,
},
},
"points-opacity": {
d: "flex",
position: "relative",
i: {
display: "inline-block",
size: "$$loadingSize",
br: "$rounded",
bg: "$$loadingColor",
margin: "0 1px",
animation: `${blink} 1.4s infinite both`,
},
"._2": {
animationDelay: "0.2s",
},
"._3": {
animationDelay: "0.4s",
},
},
spinner: {},
gradient: {
display: "flex",
position: "relative",
size: "$$loadingSize",
"._1": {
position: "absolute",
size: "100%",
border: "0px",
animation: `${rotate} 1s linear infinite`,
top: "0px",
br: "$rounded",
bg: "linear-gradient(0deg, $background 33%,$$loadingColor 100%)",
},
"._2": {
top: "2px",
position: "absolute",
size: "calc(100% - 4px)",
border: "0px",
bg: "$background",
br: "$rounded",
},
"._3": {
display: "none",
},
},
},
},
compoundVariants: [
// points-opacity & xs size
{
size: "xs",
type: "points-opacity",
css: {
$$loadingSize: "$space$1",
},
},
// points-opacity & sm size
{
size: "sm",
type: "points-opacity",
css: {
$$loadingSize: "$space$2",
},
},
// points-opacity & md size
{
size: "md",
type: "points-opacity",
css: {
$$loadingSize: "$space$3",
},
},
// points-opacity & lg size
{
size: "lg",
type: "points-opacity",
css: {
$$loadingSize: "$space$4",
},
},
// points-opacity & xl size
{
size: "xl",
type: "points-opacity",
css: {
$$loadingSize: "$space$5",
},
},
// points & xs size
{
size: "xs",
type: "points",
css: {
$$loadingSize: "$space$1",
},
},
// points & sm size
{
size: "sm",
type: "points",
css: {
$$loadingSize: "$space$2",
},
},
// points & md size
{
size: "md",
type: "points",
css: {
$$loadingSize: "$space$3",
},
},
// points & lg size
{
size: "lg",
type: "points",
css: {
$$loadingSize: "$space$4",
},
},
// points & xl size
{
size: "xl",
type: "points",
css: {
$$loadingSize: "$space$5",
},
},
],
defaultVariants: {
type: "default",
},
});
export const StyledLoadingLabel = styled("label", {
mt: "$1",
color: "$$labelColor",
fontSize: "$$loadingSize",
"*": {
margin: 0,
},
variants: {
size: {
xs: {
fontSize: "$space$5",
marginTop: "$2",
},
sm: {
fontSize: "$space$6",
marginTop: "$3",
},
md: {
fontSize: "$base",
marginTop: "$4",
},
lg: {
fontSize: "$space$10",
marginTop: "$4",
},
xl: {
fontSize: "$space$11",
marginTop: "$5",
},
},
},
});

View File

@ -1,72 +0,0 @@
import {forwardRef} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {StyledLoadingContainer, StyledLoading, StyledLoadingLabel} from "./loading.styles";
import {UseLoadingProps, useLoading} from "./use-loading";
import Spinner from "./variants/spinner";
export interface LoadingProps extends UseLoadingProps {}
const Loading = forwardRef<LoadingProps, "div">((props, ref) => {
const {
css,
containerCss,
gradientCSS,
children,
size,
type,
ariaLabel,
loadingColor,
labelColor,
className,
...otherProps
} = useLoading(props);
const domRef = useDOMRef(ref);
return (
<StyledLoadingContainer
ref={domRef}
className={clsx("nextui-loading-container", className)}
css={{
$$loadingColor: loadingColor,
$$labelColor: labelColor,
...containerCss,
}}
{...otherProps}
>
{type === "spinner" ? (
<Spinner aria-label={ariaLabel} css={css} size={size} />
) : (
<StyledLoading
aria-label={ariaLabel}
className={clsx("nextui-loading", `nextui-loading-${type}`)}
css={{
...css,
...gradientCSS,
}}
size={size}
type={type}
>
<i className="_1" />
<i className="_2" />
<i className="_3" />
</StyledLoading>
)}
{children && (
<StyledLoadingLabel className="nextui-loading-label" size={size}>
{children}
</StyledLoadingLabel>
)}
</StyledLoadingContainer>
);
});
if (__DEV__) {
Loading.displayName = "NextUI.Loading";
}
Loading.toString = () => ".nextui-loading";
export default Loading;

View File

@ -1,71 +0,0 @@
import {useMemo} from "react";
import {getCSSColor, NormalSizes} from "@nextui-org/shared-utils";
import {CSS, HTMLNextUIProps} from "@nextui-org/system";
export interface UseLoadingProps extends HTMLNextUIProps<"div"> {
/**
* Loading size.
* @default "md"
*/
size?: NormalSizes;
/**
* Loading color.
* @default "primary"
*/
color?: CSS["color"];
/**
* Loading label color.
* @default "default"
*/
textColor?: CSS["color"];
/**
* Loading type.
* @default "default"
*/
//TODO: rename to "variant"
type?: "default" | "points" | "points-opacity" | "gradient" | "spinner";
/**
* Sets a background for the "gradient" loading variant.
*/
gradientBackground?: CSS["color"];
/**
* Override default container styles
*/
containerCss?: CSS;
}
export function useLoading(props: UseLoadingProps) {
const {
children,
size = "md",
color = "primary",
type = "default",
textColor = "default",
gradientBackground,
...otherProps
} = props;
const gradientCSS = useMemo(() => {
return type === "gradient" ? {"._2": {bg: gradientBackground}} : {};
}, [type, gradientBackground]);
const ariaLabel = useMemo(() => {
if (children && typeof children === "string") {
return children;
}
return !otherProps["aria-label"] ? "Loading" : "";
}, [children, otherProps["aria-label"]]);
const loadingColor = useMemo(() => {
return getCSSColor(color as string);
}, [color]);
const labelColor = useMemo(() => {
return getCSSColor(textColor as string);
}, [textColor]);
return {children, size, type, loadingColor, labelColor, ariaLabel, gradientCSS, ...otherProps};
}
export type UseLoadingReturn = ReturnType<typeof useLoading>;

View File

@ -1,129 +0,0 @@
import {styled, keyframes, forwardRef} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, __DEV__} from "@nextui-org/shared-utils";
import {UseLoadingProps} from "../use-loading";
export interface SpinnerProps
extends Omit<UseLoadingProps, "type" | "gradientBackground" | "containerCss"> {}
export const spinnerAnimation = keyframes({
"0%": {
opacity: 1,
},
"100%": {
opacity: 0.15,
},
});
export const StyledSpinner = styled("div", {
d: "flex",
fd: "column",
jc: "center",
ai: "center",
position: "relative",
variants: {
size: {
xs: {
size: "$6",
},
sm: {
size: "$8",
},
md: {
size: "$9",
},
lg: {
size: "$11",
},
xl: {
size: "$12",
},
},
},
});
export const StyledSpinnerSpan = styled("span", {
bg: "$$loadingColor",
position: "absolute",
width: "34%",
height: "8%",
br: "$lg",
animation: `${spinnerAnimation} 1.2s linear 0s infinite normal none running`,
"&:nth-child(1)": {
animationDelay: "-1.2s",
transform: "rotate(0deg) translate(146%)",
},
"&:nth-child(2)": {
animationDelay: "-1.1s",
transform: "rotate(30deg) translate(146%)",
},
"&:nth-child(3)": {
animationDelay: "-1s",
transform: "rotate(60deg) translate(146%)",
},
"&:nth-child(4)": {
animationDelay: "-0.9s",
transform: "rotate(90deg) translate(146%)",
},
"&:nth-child(5)": {
animationDelay: "-0.8s",
transform: "rotate(120deg) translate(146%)",
},
"&:nth-child(6)": {
animationDelay: "-0.7s",
transform: "rotate(150deg) translate(146%)",
},
"&:nth-child(7)": {
animationDelay: "-0.6s",
transform: "rotate(180deg) translate(146%)",
},
"&:nth-child(8)": {
animationDelay: "-0.5s",
transform: "rotate(210deg) translate(146%)",
},
"&:nth-child(9)": {
animationDelay: "-0.4s",
transform: "rotate(240deg) translate(146%)",
},
"&:nth-child(10)": {
animationDelay: "-0.3s",
transform: "rotate(270deg) translate(146%)",
},
"&:nth-child(11)": {
animationDelay: "-0.2s",
transform: "rotate(300deg) translate(146%)",
},
"&:nth-child(12)": {
animationDelay: "-0.1s",
transform: "rotate(330deg) translate(146%)",
},
});
const Spinner = forwardRef<SpinnerProps, "div">((props, ref) => {
const {children, size, className, ...otherProps} = props;
const domRef = useDOMRef(ref);
return (
<StyledSpinner
ref={domRef}
className={clsx("nextui-spinner", className)}
size={size}
{...otherProps}
>
{[...new Array(12)].map((_, index) => (
<StyledSpinnerSpan key={`nextui-spinner-${index}`} />
))}
{children}
</StyledSpinner>
);
});
if (__DEV__) {
Spinner.displayName = "NextUI.Spinner";
}
Spinner.toString = () => ".nextui-spinner";
export default Spinner;

View File

@ -1,99 +0,0 @@
import React from "react";
import {Meta} from "@storybook/react";
import {Spacer} from "@nextui-org/spacer";
import {Grid} from "@nextui-org/grid";
import {Loading} from "../src";
export default {
title: "Feedback/Loading",
component: Loading,
} as Meta;
export const Default = () => <Loading />;
export const Text = () => (
<>
<Loading size="xs">Loading</Loading>
<Spacer x={1} />
<Loading size="sm">Loading</Loading>
<Spacer x={1} />
<Loading size="md">Loading</Loading>
<Spacer x={1} />
<Loading size="lg">Loading</Loading>
<Spacer x={1} />
<Loading size="xl">Loading</Loading>
<Spacer x={1} />
</>
);
export const Colors = () => (
<>
<Loading color="primary">Primary</Loading>
<Spacer x={1} />
<Loading color="secondary">Secondary</Loading>
<Spacer x={1} />
<Loading color="success">Success</Loading>
<Spacer x={1} />
<Loading color="warning">Warning</Loading>
<Spacer x={1} />
<Loading color="error">Error</Loading>
<Spacer x={1} />
</>
);
export const TextColors = () => (
<>
<Loading textColor="primary">Primary</Loading>
<Spacer x={1} />
<Loading textColor="secondary">Secondary</Loading>
<Spacer x={1} />
<Loading textColor="success">Success</Loading>
<Spacer x={1} />
<Loading textColor="warning">Warning</Loading>
<Spacer x={1} />
<Loading textColor="error">Error</Loading>
<Spacer x={1} />
</>
);
export const Sizes = () => (
<>
<Loading size="xs">mini</Loading>
<Spacer y={2} />
<Loading size="sm">small</Loading>
<Spacer y={2} />
<Loading size="md">medium</Loading>
<Spacer y={2} />
<Loading size="lg">large</Loading>
<Spacer y={2} />
<Loading size="xl">xlarge</Loading>
<Spacer y={2} />
</>
);
export const Types = () => (
<Grid.Container gap={4} justify="center">
<Grid>
<Loading type="default">default</Loading>
</Grid>
<Grid>
<Loading type="spinner">spinner</Loading>
</Grid>
<Grid>
<Loading style={{marginLeft: "-0.5rem"}} type="points">
points
</Loading>
</Grid>
<Grid>
<Loading style={{marginLeft: "-0.5rem"}} type="points-opacity">
points-opacity
</Loading>
</Grid>
<Grid>
<Loading style={{marginLeft: "-0.5rem"}} type="gradient">
gradient
</Loading>
</Grid>
</Grid.Container>
);

View File

@ -1,11 +1,11 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {Loading} from "../src";
import {Spinner} from "../src";
describe("Loading", () => {
describe("Spinner", () => {
it("should render correctly", () => {
const wrapper = render(<Loading />);
const wrapper = render(<Spinner />);
expect(() => wrapper.unmount()).not.toThrow();
});
@ -13,36 +13,30 @@ describe("Loading", () => {
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Loading ref={ref} />);
render(<Spinner ref={ref} />);
expect(ref.current).not.toBeNull();
});
it("should render with default aria-label", () => {
const {getByLabelText} = render(<Loading />);
const {getByLabelText} = render(<Spinner />);
expect(getByLabelText("Loading")).toBeInTheDocument();
});
it("should render with default aria-label for spinner", () => {
const {getByLabelText} = render(<Loading type="spinner" />);
it("should replace the default aria-label when a label is passed", () => {
const {getByLabelText} = render(<Spinner label="Custom label" />);
expect(getByLabelText("Loading")).toBeInTheDocument();
});
it("should work with text in spinner type", () => {
const {getByText} = render(<Loading type="spinner">Loading</Loading>);
expect(getByText("Loading")).toBeInTheDocument();
expect(getByLabelText("Custom label")).toBeInTheDocument();
});
it("should replace the default aria-label when a children is passed", () => {
const {getByLabelText} = render(<Loading>Custom label</Loading>);
const {getByLabelText} = render(<Spinner>Custom label</Spinner>);
expect(getByLabelText("Custom label")).toBeInTheDocument();
});
it("should replace the default aria-label if aria-label is passed", () => {
const {getByLabelText} = render(<Loading aria-label="Custom label" />);
const {getByLabelText} = render(<Spinner aria-label="Custom label" />);
expect(getByLabelText("Custom label")).toBeInTheDocument();
});

View File

@ -1,9 +1,11 @@
{
"name": "@nextui-org/loading",
"name": "@nextui-org/spinner",
"version": "2.0.0-beta.1",
"description": "Loaders express an unspecified wait time or display the length of a process.",
"keywords": [
"loading"
"loading",
"spinner",
"progress"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
@ -38,6 +40,7 @@
},
"dependencies": {
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/dom-utils": "workspace:*"
},

View File

@ -0,0 +1,8 @@
// export types
export type {SpinnerProps} from "./spinner";
// export hooks
export {useSpinner} from "./use-spinner";
// export component
export {default as Spinner} from "./spinner";

View File

@ -0,0 +1,25 @@
import {forwardRef} from "@nextui-org/system";
import {__DEV__} from "@nextui-org/shared-utils";
// import {StyledLoadingContainer, StyledLoading, StyledLoadingLabel} from "./loading.styles";
import {UseSpinnerProps, useSpinner} from "./use-spinner";
export interface SpinnerProps extends Omit<UseSpinnerProps, "ref"> {}
const Spinner = forwardRef<SpinnerProps, "div">((props, ref) => {
const {domRef, slots, styles, label, getSpinnerProps} = useSpinner({ref, ...props});
return (
<div ref={domRef} {...getSpinnerProps()}>
<i className={slots.circle1({class: styles?.circle1})} />
<i className={slots.circle2({class: styles?.circle2})} />
{label && <span className={slots.label()}>{label}</span>}
</div>
);
});
if (__DEV__) {
Spinner.displayName = "NextUI.Loading";
}
export default Spinner;

View File

@ -0,0 +1,70 @@
import type {SpinnerVariantProps, SpinnerSlots, SlotsToClasses} from "@nextui-org/theme";
import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
import {spinner} from "@nextui-org/theme";
import {clsx, ReactRef} from "@nextui-org/shared-utils";
import {useDOMRef} from "@nextui-org/dom-utils";
import {useMemo, useCallback} from "react";
export interface UseSpinnerProps extends HTMLNextUIProps<"div", SpinnerVariantProps> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLDivElement | null>;
/**
* Spinner label, in case you passed it will be used as `aria-label`.
*/
label?: string;
/**
* Classname or List of classes to change the styles of the avatar.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Spinner styles={{
* base:"base-classes",
* line1: "circle1-classes",
* line2: "circle2-classes",
* label: "label-classes"
* }} />
* ```
*/
styles?: SlotsToClasses<SpinnerSlots>;
}
export function useSpinner(originalProps: UseSpinnerProps) {
const [props, variantProps] = mapPropsVariants(originalProps, spinner.variantKeys);
const {ref, children, className, styles, label: labelProp, ...otherProps} = props;
const domRef = useDOMRef(ref);
const slots = useMemo(() => spinner({...variantProps}), [variantProps]);
const baseStyles = clsx(styles?.base, className);
const label = labelProp || children;
const ariaLabel = useMemo(() => {
if (label && typeof label === "string") {
return label;
}
return !otherProps["aria-label"] ? "Loading" : "";
}, [children, label, otherProps["aria-label"]]);
const getSpinnerProps = useCallback(
() => ({
"aria-label": ariaLabel,
className: slots.base({
class: baseStyles,
}),
...otherProps,
}),
[ariaLabel, slots, baseStyles, otherProps],
);
return {domRef, label, slots, styles, getSpinnerProps};
}
export type UseSpinnerReturn = ReturnType<typeof useSpinner>;

View File

@ -0,0 +1,54 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {spinner} from "@nextui-org/theme";
import {Spinner, SpinnerProps} from "../src";
export default {
title: "Feedback/Spinner",
component: Spinner,
argTypes: {
color: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
labelColor: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
},
decorators: [
(Story) => (
<div className="ml-4">
<Story />
</div>
),
],
} as ComponentMeta<typeof Spinner>;
const defaultProps = {
...spinner.defaultVariants,
};
const Template: ComponentStory<typeof Spinner> = (args: SpinnerProps) => <Spinner {...args} />;
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};
export const WithLabel = Template.bind({});
WithLabel.args = {
...defaultProps,
label: "Loading...",
};

View File

@ -4,6 +4,7 @@ module.exports = {
"../../avatar/stories/*.stories.@(js|jsx|ts|tsx)",
"../../user/stories/*.stories.@(js|jsx|ts|tsx)",
"../../button/stories/*.stories.@(js|jsx|ts|tsx)",
"../../spinner/stories/*.stories.@(js|jsx|ts|tsx)",
],
staticDirs: ["../public"],
addons: [

View File

@ -76,7 +76,7 @@ export function useUser(props: UseUserProps) {
return isFocusable || as === "button";
}, [isFocusable, as]);
const slots = user({isFocusVisible});
const slots = useMemo(() => user({isFocusVisible}), [isFocusVisible]);
const baseStyles = clsx(styles?.base, className);

View File

@ -50,7 +50,7 @@
"@nextui-org/drip": "workspace:*",
"@nextui-org/image": "workspace:*",
"@nextui-org/link": "workspace:*",
"@nextui-org/loading": "workspace:*",
"@nextui-org/spinner": "workspace:*",
"@nextui-org/pagination": "workspace:*",
"@nextui-org/radio": "workspace:*",
"@nextui-org/snippet": "workspace:*",

View File

@ -26,7 +26,7 @@ const button = tv({
"antialiased",
"active:scale-95",
"overflow-hidden",
"gap-2",
"gap-3",
],
variants: {
variant: {

View File

@ -4,3 +4,4 @@ export * from "./avatar-group";
export * from "./user";
export * from "./button";
export * from "./drip";
export * from "./spinner";

View File

@ -0,0 +1,138 @@
import {tv, type VariantProps} from "tailwind-variants";
/**
* Spinner wrapper **Tailwind Variants** component
*
* const {base, line1, line2, label } = spinner({...})
*
* @example
* <div className={base())}>
* <i className={circle1()}/>
* <i className={circle2()}/>
* <span className={label()}/>
* </div>
*/
const spinner = tv({
slots: {
base: "flex flex-col items-center justify-center relative",
circle1: [
"absolute",
"w-full",
"h-full",
"rounded-full",
"animate-spinner-ease-spin",
"border-2",
"border-solid",
"border-t-transparent",
"border-l-transparent",
"border-r-transparent",
],
circle2: [
"absolute",
"w-full",
"h-full",
"rounded-full",
"opacity-75",
"animate-spinner-linear-spin",
"border-2",
"border-dotted",
"border-t-transparent",
"border-l-transparent",
"border-r-transparent",
],
label: "text-foreground dark:text-foreground-dark font-regular",
},
variants: {
size: {
xs: {
base: "w-4 h-4",
circle1: "border-2",
circle2: "border-2",
label: "translate-y-6 text-xs",
},
sm: {
base: "w-5 h-5",
circle1: "border-2",
circle2: "border-2",
label: "translate-y-6 text-xs",
},
md: {
base: "w-8 h-8",
circle1: "border-[3px]",
circle2: "border-[3px]",
label: "translate-y-8 text-sm",
},
lg: {
base: "w-10 h-10",
circle1: "border-[3px]",
circle2: "border-[3px]",
label: "translate-y-10 text-base",
},
xl: {
base: "w-12 h-12",
circle1: "border-4",
circle2: "border-4",
label: "translate-y-12 text-lg",
},
},
color: {
white: {
circle1: "border-b-white",
circle2: "border-b-white",
},
neutral: {
circle1: "border-b-neutral",
circle2: "border-b-neutral",
},
primary: {
circle1: "border-b-primary",
circle2: "border-b-primary",
},
secondary: {
circle1: "border-b-secondary",
circle2: "border-b-secondary",
},
success: {
circle1: "border-b-success",
circle2: "border-b-success",
},
warning: {
circle1: "border-b-warning",
circle2: "border-b-warning",
},
danger: {
circle1: "border-b-danger",
circle2: "border-b-danger",
},
},
labelColor: {
neutral: {
label: "text-neutral",
},
primary: {
label: "text-primary",
},
secondary: {
label: "text-secondary",
},
success: {
label: "text-success",
},
warning: {
label: "text-warning",
},
danger: {
label: "text-danger",
},
},
},
defaultVariants: {
size: "md",
color: "primary",
},
});
export type SpinnerVariantProps = VariantProps<typeof spinner>;
export type SpinnerSlots = keyof ReturnType<typeof spinner>;
export {spinner};

View File

@ -153,8 +153,18 @@ module.exports = plugin(
},
animation: {
"drip-expand": "drip-expand 350ms linear",
"spinner-ease-spin": "spinner-spin 0.8s ease infinite",
"spinner-linear-spin": "spinner-spin 0.8s linear infinite",
},
keyframes: {
"spinner-spin": {
"0%": {
transform: "rotate(0deg)",
},
"100%": {
transform: "rotate(360deg)",
},
},
"drip-expand": {
"0%": {
opacity: 0,

66
pnpm-lock.yaml generated
View File

@ -383,9 +383,9 @@ importers:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/drip': workspace:*
'@nextui-org/loading': workspace:*
'@nextui-org/shared-icons': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/spinner': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@react-aria/button': ^3.6.2
@ -400,6 +400,7 @@ importers:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/drip': link:../drip
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/spinner': link:../spinner
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
'@react-aria/button': 3.6.4_react@17.0.2
@ -407,7 +408,6 @@ importers:
'@react-aria/interactions': 3.13.1_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
devDependencies:
'@nextui-org/loading': link:../loading
'@nextui-org/shared-icons': link:../../utilities/shared-icons
'@react-types/button': 3.7.0_react@17.0.2
'@react-types/shared': 3.16.0_react@17.0.2
@ -593,21 +593,6 @@ importers:
clean-package: 2.1.1
react: 17.0.2
packages/components/loading:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
clean-package: 2.1.1
react: ^17.0.2
dependencies:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
devDependencies:
clean-package: 2.1.1
react: 17.0.2
packages/components/pagination:
specifiers:
'@nextui-org/dom-utils': workspace:*
@ -681,6 +666,23 @@ importers:
clean-package: 2.1.1
react: 17.0.2
packages/components/spinner:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/shared-utils': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
clean-package: 2.1.1
react: ^17.0.2
dependencies:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/shared-utils': link:../../utilities/shared-utils
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
devDependencies:
clean-package: 2.1.1
react: 17.0.2
packages/components/storybook:
specifiers:
'@babel/core': ^7.16.7
@ -770,10 +772,10 @@ importers:
'@nextui-org/drip': workspace:*
'@nextui-org/image': workspace:*
'@nextui-org/link': workspace:*
'@nextui-org/loading': workspace:*
'@nextui-org/pagination': workspace:*
'@nextui-org/radio': workspace:*
'@nextui-org/snippet': workspace:*
'@nextui-org/spinner': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@nextui-org/user': workspace:*
@ -790,10 +792,10 @@ importers:
'@nextui-org/drip': link:../../components/drip
'@nextui-org/image': link:../../components/image
'@nextui-org/link': link:../../components/link
'@nextui-org/loading': link:../../components/loading
'@nextui-org/pagination': link:../../components/pagination
'@nextui-org/radio': link:../../components/radio
'@nextui-org/snippet': link:../../components/snippet
'@nextui-org/spinner': link:../../components/spinner
'@nextui-org/system': link:../system
'@nextui-org/theme': link:../theme
'@nextui-org/user': link:../../components/user
@ -5340,7 +5342,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/toggle': 3.4.2_react@17.0.2
'@react-types/button': 3.7.0_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
@ -5370,7 +5372,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/label': 3.4.2_react@17.0.2
'@react-aria/toggle': 3.4.2_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/checkbox': 3.3.0_react@17.0.2
'@react-stately/toggle': 3.4.2_react@17.0.2
'@react-types/checkbox': 3.4.0_react@17.0.2
@ -5402,7 +5404,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/overlays': 3.11.0_sfoxds7t5ydpegc3knd667wn6m
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/overlays': 3.4.2_react@17.0.2
'@react-types/dialog': 3.4.5_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
@ -5431,7 +5433,7 @@ packages:
dependencies:
'@babel/runtime': 7.20.13
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
clsx: 1.2.1
react: 17.0.2
@ -5471,7 +5473,7 @@ packages:
'@internationalized/number': 3.1.2
'@internationalized/string': 3.0.1
'@react-aria/ssr': 3.3.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
dev: false
@ -5498,7 +5500,7 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.13
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
dev: false
@ -5519,7 +5521,7 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.13
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/label': 3.7.1_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
@ -5545,7 +5547,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/link': 3.3.6_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
@ -5582,7 +5584,7 @@ packages:
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/overlays': 3.11.0_sfoxds7t5ydpegc3knd667wn6m
'@react-aria/selection': 3.12.1_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/collections': 3.4.4_react@17.0.2
'@react-stately/menu': 3.4.2_react@17.0.2
'@react-stately/tree': 3.3.4_react@17.0.2
@ -5604,7 +5606,7 @@ packages:
'@react-aria/i18n': 3.6.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/ssr': 3.3.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-aria/visually-hidden': 3.5.0_react@17.0.2
'@react-stately/overlays': 3.4.2_react@17.0.2
'@react-types/button': 3.7.0_react@17.0.2
@ -5644,7 +5646,7 @@ packages:
'@react-aria/i18n': 3.6.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/label': 3.4.2_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/radio': 3.6.0_react@17.0.2
'@react-types/radio': 3.3.1_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
@ -5714,7 +5716,7 @@ packages:
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/live-announcer': 3.1.2
'@react-aria/selection': 3.12.1_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/table': 3.5.0_react@17.0.2
'@react-stately/virtualizer': 3.4.1_react@17.0.2
'@react-types/checkbox': 3.4.0_react@17.0.2
@ -5773,7 +5775,7 @@ packages:
dependencies:
'@babel/runtime': 7.20.13
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/utils': 3.14.0_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
clsx: 1.2.1
react: 17.0.2