feat(SW-1255): Add loading state to button component
This commit is contained in:
committed by
Simon Emanuelsson
parent
80ccdc0e44
commit
89468bc37f
@@ -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,
|
||||
|
||||
@@ -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 <ButtonRAC {...props} className={classNames} />
|
||||
return (
|
||||
<ButtonRAC {...props} className={classNames} isPending={isPending}>
|
||||
{({ isPending }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{isPending && (
|
||||
<div className={styles.spinnerWrapper}>
|
||||
<Spinner size="Small" color="CurrentColor" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</ButtonRAC>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'react-material-symbols/rounded'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Spinner } from './Spinner'
|
||||
|
||||
const meta: Meta<typeof Spinner> = {
|
||||
title: 'Components/Spinner',
|
||||
component: Spinner,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Spinner>
|
||||
|
||||
function Wrapper({
|
||||
children,
|
||||
backgroundColor,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
backgroundColor?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '200px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
color: 'Accent',
|
||||
size: 'Medium',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Wrapper>
|
||||
<Story />
|
||||
</Wrapper>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Inverted: Story = {
|
||||
args: {
|
||||
color: 'Inverted',
|
||||
size: 'Medium',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Wrapper backgroundColor="var(--Icon-Interactive-Default)">
|
||||
<Story />
|
||||
</Wrapper>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: 'Small',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Wrapper>
|
||||
<Story />
|
||||
</Wrapper>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
size: 'Medium',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Wrapper>
|
||||
<Story />
|
||||
</Wrapper>
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: 'Large',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Wrapper>
|
||||
<Story />
|
||||
</Wrapper>
|
||||
),
|
||||
],
|
||||
}
|
||||
21
packages/design-system/lib/components/Spinner/Spinner.tsx
Normal file
21
packages/design-system/lib/components/Spinner/Spinner.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { VariantProps } from 'class-variance-authority'
|
||||
import styles from './spinner.module.css'
|
||||
|
||||
import { variants } from './variants'
|
||||
|
||||
type SpinnerProps = VariantProps<typeof variants>
|
||||
|
||||
export function Spinner({ color, size }: SpinnerProps) {
|
||||
const classNames = variants({
|
||||
color,
|
||||
size,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className={styles.dot} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
packages/design-system/lib/components/Spinner/index.ts
Normal file
1
packages/design-system/lib/components/Spinner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Spinner } from './Spinner'
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
24
packages/design-system/lib/components/Spinner/variants.ts
Normal file
24
packages/design-system/lib/components/Spinner/variants.ts
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user