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 = {
|
export const PrimaryLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...PrimaryDefault.args,
|
...PrimaryDefault.args,
|
||||||
@@ -106,6 +113,13 @@ export const PrimaryInvertedDisabled: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PrimaryInvertedLoading: Story = {
|
||||||
|
args: {
|
||||||
|
...PrimaryInvertedDefault.args,
|
||||||
|
isPending: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const PrimaryInvertedLarge: Story = {
|
export const PrimaryInvertedLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...PrimaryInvertedDefault.args,
|
...PrimaryInvertedDefault.args,
|
||||||
@@ -181,6 +195,13 @@ export const SecondaryInvertedDisabled: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SecondaryInvertedLoading: Story = {
|
||||||
|
args: {
|
||||||
|
...SecondaryInvertedDefault.args,
|
||||||
|
isPending: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const SecondaryInvertedLarge: Story = {
|
export const SecondaryInvertedLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...SecondaryInvertedDefault.args,
|
...SecondaryInvertedDefault.args,
|
||||||
@@ -218,6 +239,12 @@ export const TertiaryDisabled: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TertiaryLoading: Story = {
|
||||||
|
args: {
|
||||||
|
...TertiaryDefault.args,
|
||||||
|
isPending: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
export const TertiaryLarge: Story = {
|
export const TertiaryLarge: Story = {
|
||||||
args: {
|
args: {
|
||||||
...TertiaryDefault.args,
|
...TertiaryDefault.args,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Button as ButtonRAC } from 'react-aria-components'
|
|||||||
import { variants } from './variants'
|
import { variants } from './variants'
|
||||||
|
|
||||||
import type { ButtonProps } from './types'
|
import type { ButtonProps } from './types'
|
||||||
|
import { Spinner } from '../Spinner'
|
||||||
|
import styles from './button.module.css'
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
variant,
|
variant,
|
||||||
@@ -12,7 +14,8 @@ export function Button({
|
|||||||
|
|
||||||
typography,
|
typography,
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
|
isPending,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const classNames = variants({
|
const classNames = variants({
|
||||||
@@ -20,10 +23,24 @@ export function Button({
|
|||||||
color,
|
color,
|
||||||
size,
|
size,
|
||||||
wrapping,
|
wrapping,
|
||||||
|
|
||||||
typography,
|
typography,
|
||||||
className,
|
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 {
|
.variant-text.color-inverted:disabled {
|
||||||
color: var(--Component-Button-Brand-Secondary-On-fill-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