Skip to content

Commit

Permalink
Add loading state for buttons (#1000)
Browse files Browse the repository at this point in the history
* Add loading state for buttons

* Update spinners to have colors matching button styles

* Make disabled loading button state not be too dim
  • Loading branch information
zephraph authored Jun 29, 2022
1 parent d2619df commit be328b3
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 18 deletions.
4 changes: 3 additions & 1 deletion app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ export default function CreateInstanceForm({
<TextField id="hostname" description="Will be generated if not provided" />

<Form.Actions>
<Form.Submit>{title}</Form.Submit>
<Form.Submit loading={createDisk.isLoading || createInstance.isLoading}>
{title}
</Form.Submit>
<Form.Cancel />
</Form.Actions>
</FullPageForm>
Expand Down
2 changes: 1 addition & 1 deletion libs/api-mocks/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export const handlers = [
time_run_state_updated: new Date().toISOString(),
}
db.instances.push(newInstance)
return res(json(newInstance, 201))
return res(json(newInstance, 201, 2000))
}
),

Expand Down
4 changes: 2 additions & 2 deletions libs/api-mocks/msw/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { compose, context } from 'msw'
*
* https://mswjs.io/docs/basics/response-transformer#custom-transformer
*/
export const json = <B>(body: B, status = 200): ResponseTransformer<B> =>
compose(context.status(status), context.json(body))
export const json = <B>(body: B, status = 200, delay = 0): ResponseTransformer<B> =>
compose(context.status(status), context.json(body), context.delay(delay))

export interface ResultsPage<I extends { id: string }> {
items: I[]
Expand Down
27 changes: 19 additions & 8 deletions libs/ui/lib/button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,25 @@ export const All = () => {
{colors.map((color) => (
<div key={color} className="mb-2 flex flex-row space-x-2">
{variants.map((variant) => (
<Button
key={variant}
variant={variant}
color={color}
className={`:${state}`}
>
{variant}
</Button>
<>
<Button
key={variant}
variant={variant}
color={color}
className={`:${state}`}
>
{variant}
</Button>
<Button
key={variant}
variant={variant}
color={color}
className={`:${state}`}
loading={true}
>
{variant}
</Button>
</>
))}
</div>
))}
Expand Down
13 changes: 10 additions & 3 deletions libs/ui/lib/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import cn from 'classnames'
import { forwardRef } from 'react'

import { Spinner } from '@oxide/ui'
import { assertUnreachable } from '@oxide/util'

import './button.css'
Expand Down Expand Up @@ -69,7 +70,10 @@ type ButtonStyleProps = {
color?: Color
}

export type ButtonProps = React.ComponentPropsWithRef<'button'> & ButtonStyleProps
export type ButtonProps = React.ComponentPropsWithRef<'button'> &
ButtonStyleProps & {
loading?: boolean
}

export const buttonStyle = ({
size = 'base',
Expand All @@ -89,15 +93,18 @@ export const buttonStyle = ({
// Use `forwardRef` so the ref points to the DOM element (not the React Component)
// so it can be focused using the DOM API (eg. this.buttonRef.current.focus())
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, size, variant, color, className, ...rest }, ref) => {
({ children, size, variant, color, className, loading, ...rest }, ref) => {
return (
<button
className={cn(buttonStyle({ size, variant, color }), className)}
ref={ref}
type="button"
{...rest}
>
{children}
<>
{loading && <Spinner className="absolute" />}
<span className={cn({ invisible: loading })}>{children}</span>
</>
</button>
)
}
Expand Down
12 changes: 12 additions & 0 deletions libs/ui/lib/button/button.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,31 @@
.btn-primary {
@apply text-accent bg-accent-secondary hover:bg-accent-secondary-hover disabled:text-accent-disabled disabled:bg-accent-secondary;
}
.btn-primary:disabled > .spinner {
@apply text-accent;
}

/* Using `-solid` since ghost version is used as default secondary button state */
.btn-secondary-solid {
@apply text-secondary bg-secondary hover:bg-secondary-hover disabled:text-quaternary disabled:bg-secondary;
}
.btn-secondary-solid:disabled > .spinner {
@apply text-secondary;
}

.btn-destructive {
@apply text-destructive bg-destructive-secondary hover:bg-destructive-secondary-hover disabled:text-destructive-disabled disabled:bg-destructive-secondary;
}
.btn-destructive:disabled > .spinner {
@apply text-destructive;
}

.btn-notice {
@apply text-notice bg-notice-secondary hover:bg-notice-secondary-hover disabled:text-notice-disabled disabled:bg-notice-secondary;
}
.btn-notice:disabled > .spinner {
@apply text-notice;
}

/**
* Ghost
Expand Down
13 changes: 10 additions & 3 deletions libs/ui/lib/spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export const Spinner = () => {
import cn from 'classnames'

interface SpinnerProps {
className?: string
}

export const Spinner = ({ className }: SpinnerProps) => {
return (
<svg
width={12}
Expand All @@ -7,13 +13,14 @@ export const Spinner = () => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="Spinner"
className="animate-spin text-accent fill-green-800"
className={cn('spinner animate-spin', className)}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 10.5C8.48528 10.5 10.5 8.48528 10.5 6C10.5 3.51472 8.48528 1.5 6 1.5C3.51472 1.5 1.5 3.51472 1.5 6C1.5 8.48528 3.51472 10.5 6 10.5ZM6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12Z"
fill="currentFill"
fill="currentColor"
fillOpacity={0.5}
/>
<path
fillRule="evenodd"
Expand Down

1 comment on commit be328b3

@vercel
Copy link

@vercel vercel bot commented on be328b3 Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.