diff --git a/packages/components/avatar/src/use-avatar-group.ts b/packages/components/avatar/src/use-avatar-group.ts index b29951fe0..8de35a612 100644 --- a/packages/components/avatar/src/use-avatar-group.ts +++ b/packages/components/avatar/src/use-avatar-group.ts @@ -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; diff --git a/packages/components/avatar/src/use-avatar.ts b/packages/components/avatar/src/use-avatar.ts index 251f02328..1791486cc 100644 --- a/packages/components/avatar/src/use-avatar.ts +++ b/packages/components/avatar/src/use-avatar.ts @@ -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 ""; diff --git a/packages/components/button/package.json b/packages/components/button/package.json index cbb529a36..3ad4e3f86 100644 --- a/packages/components/button/package.json +++ b/packages/components/button/package.json @@ -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", diff --git a/packages/components/button/src/use-button.ts b/packages/components/button/src/use-button.ts index d67844e7e..63265639f 100644 --- a/packages/components/button/src/use-button.ts +++ b/packages/components/button/src/use-button.ts @@ -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); diff --git a/packages/components/button/stories/button.stories.tsx b/packages/components/button/stories/button.stories.tsx index 2673a7365..9ec7d7f44 100644 --- a/packages/components/button/stories/button.stories.tsx +++ b/packages/components/button/stories/button.stories.tsx @@ -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: , }; -// export const Loadings = () => ( -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// ); +export const IsLoading = Template.bind({}); +IsLoading.args = { + ...defaultProps, + color: "primary", + isDisabled: true, + children: ( + <> + + Button + + ), +}; diff --git a/packages/components/link/src/use-link.ts b/packages/components/link/src/use-link.ts index e88357447..fbcc15e21 100644 --- a/packages/components/link/src/use-link.ts +++ b/packages/components/link/src/use-link.ts @@ -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}; } diff --git a/packages/components/loading/src/index.ts b/packages/components/loading/src/index.ts deleted file mode 100644 index b677ea330..000000000 --- a/packages/components/loading/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// export types -export type {LoadingProps} from "./loading"; - -// export component -export {default as Loading} from "./loading"; diff --git a/packages/components/loading/src/loading.animations.ts b/packages/components/loading/src/loading.animations.ts deleted file mode 100644 index c46094b49..000000000 --- a/packages/components/loading/src/loading.animations.ts +++ /dev/null @@ -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)", - }, -}); diff --git a/packages/components/loading/src/loading.styles.ts b/packages/components/loading/src/loading.styles.ts deleted file mode 100644 index a63c5808b..000000000 --- a/packages/components/loading/src/loading.styles.ts +++ /dev/null @@ -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", - }, - }, - }, -}); diff --git a/packages/components/loading/src/loading.tsx b/packages/components/loading/src/loading.tsx deleted file mode 100644 index a6908a501..000000000 --- a/packages/components/loading/src/loading.tsx +++ /dev/null @@ -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((props, ref) => { - const { - css, - containerCss, - gradientCSS, - children, - size, - type, - ariaLabel, - loadingColor, - labelColor, - className, - ...otherProps - } = useLoading(props); - - const domRef = useDOMRef(ref); - - return ( - - {type === "spinner" ? ( - - ) : ( - - - - - - )} - {children && ( - - {children} - - )} - - ); -}); - -if (__DEV__) { - Loading.displayName = "NextUI.Loading"; -} - -Loading.toString = () => ".nextui-loading"; - -export default Loading; diff --git a/packages/components/loading/src/use-loading.ts b/packages/components/loading/src/use-loading.ts deleted file mode 100644 index 6bc35e912..000000000 --- a/packages/components/loading/src/use-loading.ts +++ /dev/null @@ -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; diff --git a/packages/components/loading/src/variants/spinner.tsx b/packages/components/loading/src/variants/spinner.tsx deleted file mode 100644 index c13f91cef..000000000 --- a/packages/components/loading/src/variants/spinner.tsx +++ /dev/null @@ -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 {} - -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((props, ref) => { - const {children, size, className, ...otherProps} = props; - - const domRef = useDOMRef(ref); - - return ( - - {[...new Array(12)].map((_, index) => ( - - ))} - {children} - - ); -}); - -if (__DEV__) { - Spinner.displayName = "NextUI.Spinner"; -} - -Spinner.toString = () => ".nextui-spinner"; - -export default Spinner; diff --git a/packages/components/loading/stories/loading.stories.tsx b/packages/components/loading/stories/loading.stories.tsx deleted file mode 100644 index b2b54c62c..000000000 --- a/packages/components/loading/stories/loading.stories.tsx +++ /dev/null @@ -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 = () => ; - -export const Text = () => ( - <> - Loading - - Loading - - Loading - - Loading - - Loading - - -); - -export const Colors = () => ( - <> - Primary - - Secondary - - Success - - Warning - - Error - - -); - -export const TextColors = () => ( - <> - Primary - - Secondary - - Success - - Warning - - Error - - -); - -export const Sizes = () => ( - <> - mini - - small - - medium - - large - - xlarge - - -); - -export const Types = () => ( - - - default - - - spinner - - - - points - - - - - points-opacity - - - - - gradient - - - -); diff --git a/packages/components/loading/README.md b/packages/components/spinner/README.md similarity index 100% rename from packages/components/loading/README.md rename to packages/components/spinner/README.md diff --git a/packages/components/loading/__tests__/loading.test.tsx b/packages/components/spinner/__tests__/spinner.test.tsx similarity index 52% rename from packages/components/loading/__tests__/loading.test.tsx rename to packages/components/spinner/__tests__/spinner.test.tsx index 78f7269ae..e0db803c0 100644 --- a/packages/components/loading/__tests__/loading.test.tsx +++ b/packages/components/spinner/__tests__/spinner.test.tsx @@ -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(); + const wrapper = render(); expect(() => wrapper.unmount()).not.toThrow(); }); @@ -13,36 +13,30 @@ describe("Loading", () => { it("ref should be forwarded", () => { const ref = React.createRef(); - render(); + render(); expect(ref.current).not.toBeNull(); }); it("should render with default aria-label", () => { - const {getByLabelText} = render(); + const {getByLabelText} = render(); expect(getByLabelText("Loading")).toBeInTheDocument(); }); - it("should render with default aria-label for spinner", () => { - const {getByLabelText} = render(); + it("should replace the default aria-label when a label is passed", () => { + const {getByLabelText} = render(); - expect(getByLabelText("Loading")).toBeInTheDocument(); - }); - - it("should work with text in spinner type", () => { - const {getByText} = render(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(Custom label); + const {getByLabelText} = render(Custom label); expect(getByLabelText("Custom label")).toBeInTheDocument(); }); it("should replace the default aria-label if aria-label is passed", () => { - const {getByLabelText} = render(); + const {getByLabelText} = render(); expect(getByLabelText("Custom label")).toBeInTheDocument(); }); diff --git a/packages/components/loading/clean-package.config.json b/packages/components/spinner/clean-package.config.json similarity index 100% rename from packages/components/loading/clean-package.config.json rename to packages/components/spinner/clean-package.config.json diff --git a/packages/components/loading/package.json b/packages/components/spinner/package.json similarity index 91% rename from packages/components/loading/package.json rename to packages/components/spinner/package.json index dccf96f2e..03b3a39b0 100644 --- a/packages/components/loading/package.json +++ b/packages/components/spinner/package.json @@ -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 ", "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:*" }, diff --git a/packages/components/spinner/src/index.ts b/packages/components/spinner/src/index.ts new file mode 100644 index 000000000..c0d9ef0b5 --- /dev/null +++ b/packages/components/spinner/src/index.ts @@ -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"; diff --git a/packages/components/spinner/src/spinner.tsx b/packages/components/spinner/src/spinner.tsx new file mode 100644 index 000000000..72f53ad8c --- /dev/null +++ b/packages/components/spinner/src/spinner.tsx @@ -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 {} + +const Spinner = forwardRef((props, ref) => { + const {domRef, slots, styles, label, getSpinnerProps} = useSpinner({ref, ...props}); + + return ( +
+ + + {label && {label}} +
+ ); +}); + +if (__DEV__) { + Spinner.displayName = "NextUI.Loading"; +} + +export default Spinner; diff --git a/packages/components/spinner/src/use-spinner.ts b/packages/components/spinner/src/use-spinner.ts new file mode 100644 index 000000000..d7f9f0a24 --- /dev/null +++ b/packages/components/spinner/src/use-spinner.ts @@ -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; + /** + * 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 + * + * ``` + */ + styles?: SlotsToClasses; +} + +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; diff --git a/packages/components/spinner/stories/spinner.stories.tsx b/packages/components/spinner/stories/spinner.stories.tsx new file mode 100644 index 000000000..297d5a439 --- /dev/null +++ b/packages/components/spinner/stories/spinner.stories.tsx @@ -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) => ( +
+ +
+ ), + ], +} as ComponentMeta; + +const defaultProps = { + ...spinner.defaultVariants, +}; + +const Template: ComponentStory = (args: SpinnerProps) => ; + +export const Default = Template.bind({}); +Default.args = { + ...defaultProps, +}; + +export const WithLabel = Template.bind({}); +WithLabel.args = { + ...defaultProps, + label: "Loading...", +}; diff --git a/packages/components/loading/tsconfig.json b/packages/components/spinner/tsconfig.json similarity index 100% rename from packages/components/loading/tsconfig.json rename to packages/components/spinner/tsconfig.json diff --git a/packages/components/storybook/.storybook/main.js b/packages/components/storybook/.storybook/main.js index 30d604f0f..c92bf9a6b 100644 --- a/packages/components/storybook/.storybook/main.js +++ b/packages/components/storybook/.storybook/main.js @@ -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: [ diff --git a/packages/components/user/src/use-user.ts b/packages/components/user/src/use-user.ts index 16e43fd7c..7e3d89030 100644 --- a/packages/components/user/src/use-user.ts +++ b/packages/components/user/src/use-user.ts @@ -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); diff --git a/packages/core/react/package.json b/packages/core/react/package.json index 51d392b0b..4342f3601 100644 --- a/packages/core/react/package.json +++ b/packages/core/react/package.json @@ -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:*", diff --git a/packages/core/theme/src/components/button/index.ts b/packages/core/theme/src/components/button/index.ts index 3178a6d0f..06fb34e50 100644 --- a/packages/core/theme/src/components/button/index.ts +++ b/packages/core/theme/src/components/button/index.ts @@ -26,7 +26,7 @@ const button = tv({ "antialiased", "active:scale-95", "overflow-hidden", - "gap-2", + "gap-3", ], variants: { variant: { diff --git a/packages/core/theme/src/components/index.ts b/packages/core/theme/src/components/index.ts index d914b26f6..15b1c8fc6 100644 --- a/packages/core/theme/src/components/index.ts +++ b/packages/core/theme/src/components/index.ts @@ -4,3 +4,4 @@ export * from "./avatar-group"; export * from "./user"; export * from "./button"; export * from "./drip"; +export * from "./spinner"; diff --git a/packages/core/theme/src/components/spinner/index.ts b/packages/core/theme/src/components/spinner/index.ts new file mode 100644 index 000000000..01667a5bb --- /dev/null +++ b/packages/core/theme/src/components/spinner/index.ts @@ -0,0 +1,138 @@ +import {tv, type VariantProps} from "tailwind-variants"; + +/** + * Spinner wrapper **Tailwind Variants** component + * + * const {base, line1, line2, label } = spinner({...}) + * + * @example + *
+ * + * + * + *
+ */ +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; +export type SpinnerSlots = keyof ReturnType; + +export {spinner}; diff --git a/packages/core/theme/src/plugin.js b/packages/core/theme/src/plugin.js index 430f55c4c..cccb548ca 100644 --- a/packages/core/theme/src/plugin.js +++ b/packages/core/theme/src/plugin.js @@ -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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dfa09850..985b65535 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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