From 89468bc37fe8cc9793c6abe25103d6ccdc436ae9 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 23 Apr 2025 10:03:33 +0200 Subject: [PATCH] feat(SW-1255): Add loading state to button component --- .../lib/components/Button/Button.stories.tsx | 27 +++++ .../lib/components/Button/Button.tsx | 23 +++- .../lib/components/Button/button.module.css | 6 + .../components/Spinner/Spinner.stories.tsx | 104 ++++++++++++++++++ .../lib/components/Spinner/Spinner.tsx | 21 ++++ .../lib/components/Spinner/index.ts | 1 + .../lib/components/Spinner/spinner.module.css | 92 ++++++++++++++++ .../lib/components/Spinner/variants.ts | 24 ++++ 8 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 packages/design-system/lib/components/Spinner/Spinner.stories.tsx create mode 100644 packages/design-system/lib/components/Spinner/Spinner.tsx create mode 100644 packages/design-system/lib/components/Spinner/index.ts create mode 100644 packages/design-system/lib/components/Spinner/spinner.module.css create mode 100644 packages/design-system/lib/components/Spinner/variants.ts diff --git a/packages/design-system/lib/components/Button/Button.stories.tsx b/packages/design-system/lib/components/Button/Button.stories.tsx index 3b6a40cfe..74924d1bf 100644 --- a/packages/design-system/lib/components/Button/Button.stories.tsx +++ b/packages/design-system/lib/components/Button/Button.stories.tsx @@ -68,6 +68,13 @@ export const PrimaryDisabled: Story = { }, } +export const PrimaryLoading: Story = { + args: { + ...PrimaryDefault.args, + isPending: true, + }, +} + export const PrimaryLarge: Story = { args: { ...PrimaryDefault.args, @@ -106,6 +113,13 @@ export const PrimaryInvertedDisabled: Story = { }, } +export const PrimaryInvertedLoading: Story = { + args: { + ...PrimaryInvertedDefault.args, + isPending: true, + }, +} + export const PrimaryInvertedLarge: Story = { args: { ...PrimaryInvertedDefault.args, @@ -181,6 +195,13 @@ export const SecondaryInvertedDisabled: Story = { }, } +export const SecondaryInvertedLoading: Story = { + args: { + ...SecondaryInvertedDefault.args, + isPending: true, + }, +} + export const SecondaryInvertedLarge: Story = { args: { ...SecondaryInvertedDefault.args, @@ -218,6 +239,12 @@ export const TertiaryDisabled: Story = { }, } +export const TertiaryLoading: Story = { + args: { + ...TertiaryDefault.args, + isPending: true, + }, +} export const TertiaryLarge: Story = { args: { ...TertiaryDefault.args, diff --git a/packages/design-system/lib/components/Button/Button.tsx b/packages/design-system/lib/components/Button/Button.tsx index 02c7a7938..773a32f4c 100644 --- a/packages/design-system/lib/components/Button/Button.tsx +++ b/packages/design-system/lib/components/Button/Button.tsx @@ -3,6 +3,8 @@ import { Button as ButtonRAC } from 'react-aria-components' import { variants } from './variants' import type { ButtonProps } from './types' +import { Spinner } from '../Spinner' +import styles from './button.module.css' export function Button({ variant, @@ -12,7 +14,8 @@ export function Button({ typography, className, - + children, + isPending, ...props }: ButtonProps) { const classNames = variants({ @@ -20,10 +23,24 @@ export function Button({ color, size, wrapping, - typography, className, }) - return + return ( + + {({ isPending }) => { + return ( + <> + {children} + {isPending && ( +
+ +
+ )} + + ) + }} +
+ ) } diff --git a/packages/design-system/lib/components/Button/button.module.css b/packages/design-system/lib/components/Button/button.module.css index 12227961e..e008f3c6d 100644 --- a/packages/design-system/lib/components/Button/button.module.css +++ b/packages/design-system/lib/components/Button/button.module.css @@ -167,3 +167,9 @@ .variant-text.color-inverted:disabled { color: var(--Component-Button-Brand-Secondary-On-fill-Disabled); } + +.spinnerWrapper { + display: flex; + align-items: center; + margin-left: var(--Space-x1); +} \ No newline at end of file diff --git a/packages/design-system/lib/components/Spinner/Spinner.stories.tsx b/packages/design-system/lib/components/Spinner/Spinner.stories.tsx new file mode 100644 index 000000000..bc09d0fa5 --- /dev/null +++ b/packages/design-system/lib/components/Spinner/Spinner.stories.tsx @@ -0,0 +1,104 @@ +import 'react-material-symbols/rounded' + +import type { Meta, StoryObj } from '@storybook/react' + +import { Spinner } from './Spinner' + +const meta: Meta = { + title: 'Components/Spinner', + component: Spinner, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +function Wrapper({ + children, + backgroundColor, +}: { + children: React.ReactNode + backgroundColor?: string +}) { + return ( +
+ {children} +
+ ) +} + +export const Default: Story = { + args: { + color: 'Accent', + size: 'Medium', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export const Inverted: Story = { + args: { + color: 'Inverted', + size: 'Medium', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export const Small: Story = { + args: { + size: 'Small', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export const Medium: Story = { + args: { + size: 'Medium', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export const Large: Story = { + args: { + size: 'Large', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} diff --git a/packages/design-system/lib/components/Spinner/Spinner.tsx b/packages/design-system/lib/components/Spinner/Spinner.tsx new file mode 100644 index 000000000..32dc1388a --- /dev/null +++ b/packages/design-system/lib/components/Spinner/Spinner.tsx @@ -0,0 +1,21 @@ +import { VariantProps } from 'class-variance-authority' +import styles from './spinner.module.css' + +import { variants } from './variants' + +type SpinnerProps = VariantProps + +export function Spinner({ color, size }: SpinnerProps) { + const classNames = variants({ + color, + size, + }) + + return ( +
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+ ) +} diff --git a/packages/design-system/lib/components/Spinner/index.ts b/packages/design-system/lib/components/Spinner/index.ts new file mode 100644 index 000000000..fbf16c1fa --- /dev/null +++ b/packages/design-system/lib/components/Spinner/index.ts @@ -0,0 +1 @@ +export { Spinner } from './Spinner' diff --git a/packages/design-system/lib/components/Spinner/spinner.module.css b/packages/design-system/lib/components/Spinner/spinner.module.css new file mode 100644 index 000000000..dd60a9f04 --- /dev/null +++ b/packages/design-system/lib/components/Spinner/spinner.module.css @@ -0,0 +1,92 @@ +.spinner { + display: inline-block; + position: relative; + + --size: 20px; + --dot-size: 3px; + + width: var(--size); + height: var(--size); +} + +.size-small { + --size: 20px; +} + +.size-medium { + --size: 30px; + --dot-size: 5px; +} + +.size-large { + --size: 40px; + --dot-size: 6px; +} + +.spinner .dot { + transform-origin: calc(var(--size) / 2) calc(var(--size) / 2); + animation: spinnerAnimation 0.8s linear infinite; +} + +.spinner .dot::after { + content: ' '; + display: block; + position: absolute; + top: calc(var(--dot-size) / 2); + left: var(--dot-size); + width: var(--dot-size); + height: var(--dot-size); + border-radius: 50%; + background-color: currentColor; +} + +.accent .dot::after { + background-color: var(--Icon-Interactive-Default); +} + +.inverted .dot::after { + background-color: var(--Icon-Inverted); +} + +.dot:nth-child(1) { + transform: rotate(0deg); + animation-delay: -0.7s; +} + +.dot:nth-child(2) { + transform: rotate(45deg); + animation-delay: -0.6s; +} +.dot:nth-child(3) { + transform: rotate(90deg); + animation-delay: -0.5s; +} +.dot:nth-child(4) { + transform: rotate(135deg); + animation-delay: -0.4s; +} +.dot:nth-child(5) { + transform: rotate(180deg); + animation-delay: -0.3s; +} +.dot:nth-child(6) { + transform: rotate(225deg); + animation-delay: -0.2s; +} +.dot:nth-child(7) { + transform: rotate(270deg); + animation-delay: -0.1s; +} +.dot:nth-child(8) { + transform: rotate(315deg); + animation-delay: 0s; +} + +@keyframes spinnerAnimation { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/packages/design-system/lib/components/Spinner/variants.ts b/packages/design-system/lib/components/Spinner/variants.ts new file mode 100644 index 000000000..87e2764e5 --- /dev/null +++ b/packages/design-system/lib/components/Spinner/variants.ts @@ -0,0 +1,24 @@ +import { cva } from 'class-variance-authority' + +import styles from './spinner.module.css' + +export const config = { + variants: { + color: { + Accent: styles.accent, + Inverted: styles.inverted, + CurrentColor: 'currentColor', + }, + size: { + Small: styles['size-small'], + Medium: styles['size-medium'], + Large: styles['size-large'], + }, + }, + defaultVariants: { + color: 'Accent', + size: 'Medium', + }, +} as const + +export const variants = cva(styles.spinner, config)