Skip to content

Commit

Permalink
Add a loading button
Browse files Browse the repository at this point in the history
  • Loading branch information
Vineet119 authored and joshwooding committed Aug 1, 2024
1 parent 0eb21ae commit 9285dfe
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 3 deletions.
44 changes: 43 additions & 1 deletion packages/core/src/button/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
border-width: var(--saltButton-borderWidth, var(--salt-size-border, 0));
border-radius: var(--saltButton-borderRadius, var(--salt-palette-corner-weak, 0));
color: var(--saltButton-text-color, var(--button-text-color));
cursor: var(--saltButton-cursor, pointer);
cursor: var(--saltButton-cursor, var(--salt-actionable-cursor-hover));
display: inline-flex;
gap: var(--salt-spacing-50);
justify-content: var(--saltButton-justifyContent, center);
Expand Down Expand Up @@ -174,3 +174,45 @@
--button-borderColor-active: var(--salt-actionable-subtle-borderColor-active);
--button-borderColor-disabled: var(--salt-actionable-subtle-borderColor-disabled);
}

.saltButton-cta.saltButton-loading {
--button-text-color: var(--salt-content-primary-foreground);
--button-background: var(--salt-container-primary-background);
--button-borderColor: var(--salt-actionable-cta-borderColor);
}

.saltButton-cta.saltButton-loading:hover,
.saltButton-cta.saltButton-loading:active {
color: var(--salt-content-primary-foreground);
background: var(--salt-container-primary-background);
border-color: var(--salt-actionable-cta-borderColor);
cursor: default;
}

.saltButton-primary.saltButton-loading {
--button-text-color: var(--salt-content-primary-foreground);
--button-background: var(--salt-container-primary-background);
--button-borderColor: var(--salt-actionable-primary-borderColor);
}

.saltButton-primary.saltButton-loading:hover,
.saltButton-primary.saltButton-loading:active {
color: var(--salt-content-primary-foreground);
background: var(--salt-container-primary-background);
border-color: var(--salt-actionable-primary-borderColor);
cursor: default;
}

.saltButton-secondary.saltButton-loading {
--button-text-color: var(--salt-content-primary-foreground);
--button-background: var(--salt-container-primary-background);
--button-borderColor: var(--salt-actionable-secondary-borderColor);
}

.saltButton-secondary.saltButton-loading:hover,
.saltButton-secondary.saltButton-loading:active {
color: var(--salt-content-primary-foreground);
background: var(--salt-container-primary-background);
border-color: var(--salt-actionable-secondary-borderColor);
cursor: default;
}
8 changes: 7 additions & 1 deletion packages/core/src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
* The color of the button. Options are 'accent' and 'neutral'.
*/
color?: ButtonColor;
/**
* To show a loading spinner.
*/
loading?: boolean;
}

function variantToAppearanceAndColor(variant: ButtonVariant) {
Expand Down Expand Up @@ -71,9 +75,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
color: colorProp,
type = "button",
variant = "primary",
loading,
...restProps
},
ref?,
ref,
): ReactElement<ButtonProps> {
const { active, buttonProps } = useButton({
disabled,
Expand Down Expand Up @@ -106,6 +111,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
withBaseName(variant),
{
[withBaseName("disabled")]: disabled,
[withBaseName("loading")]: loading,
[withBaseName("active")]: active,
[withBaseName(appearance)]: appearance,
[withBaseName(color)]: color,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/button/useButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ButtonHookProps<T extends Element> {
onKeyDown?: (event: KeyboardEvent<T>) => void;
onClick?: (event: MouseEvent<T>) => void;
onBlur?: (event: FocusEvent<T>) => void;
loading?: boolean;
}

export interface ButtonHookResult<T extends Element> {
Expand All @@ -35,6 +36,7 @@ export const useButton = <T extends Element>({
onKeyDown,
onClick,
onBlur,
loading,
}: ButtonHookProps<T>): ButtonHookResult<T> => {
const [keyIsDown, setkeyIsDown] = useState("");
const [active, setActive] = useState(false);
Expand Down Expand Up @@ -64,6 +66,9 @@ export const useButton = <T extends Element>({

const handleClick = (event: MouseEvent<T>) => {
setActive(true);
if (loading) {
event.preventDefault();
}
onClick?.(event);
};

Expand Down
262 changes: 261 additions & 1 deletion packages/core/stories/button/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { Button, type ButtonProps, StackLayout } from "@salt-ds/core";
import {
Button,
type ButtonProps,
Dialog,
DialogActions,
DialogContent,
DialogHeader,
FlowLayout,
FormField,
FormFieldLabel,
Input,
Spinner,
StackLayout,
useId,
} from "@salt-ds/core";
import {
DoubleChevronRightIcon,
DownloadIcon,
RefreshIcon,
SearchIcon,
SendIcon,
SettingsSolidIcon,
SyncIcon,
} from "@salt-ds/icons";
import type { Meta, StoryFn } from "@storybook/react";
import { useState } from "react";

export default {
title: "Core/Button",
Expand Down Expand Up @@ -216,3 +234,245 @@ export const FullWidth: StoryFn<typeof Button> = () => {
</StackLayout>
);
};

function useLoadOnClick() {
const [loading, setLoading] = useState(false);

const handleClick = () => {
if (!loading) {
setLoading(true);
setTimeout(() => {
setLoading(false);
}, 3000);
}
};

return [loading, handleClick] as const;
}

export const LoadingButtonsReplaceIcon: StoryFn<typeof Button> = () => {
const [primaryLoading, setPrimaryLoading] = useLoadOnClick();
const [secondaryLoading, setSecondaryLoading] = useLoadOnClick();
const [ctaLoading, setCtaLoading] = useLoadOnClick();

return (
<FlowLayout>
<Button variant="cta" loading={ctaLoading} onClick={setCtaLoading}>
{ctaLoading ? (
<Spinner size="small" aria-label="Sending" />
) : (
<SendIcon aria-hidden />
)}
Send Email
</Button>
<Button
variant="primary"
loading={primaryLoading}
onClick={setPrimaryLoading}
>
{primaryLoading ? (
<Spinner aria-label="Syncing" size="small" />
) : (
<SyncIcon aria-hidden />
)}
Sync Files
</Button>
<Button
variant="secondary"
loading={secondaryLoading}
onClick={setSecondaryLoading}
>
{secondaryLoading ? (
<Spinner size="small" aria-label="Refreshing" />
) : (
<RefreshIcon aria-hidden />
)}
Refresh Page
</Button>
</FlowLayout>
);
};

export const LoadingButtons: StoryFn<typeof Button> = () => {
const [primaryLoading, setPrimaryLoading] = useLoadOnClick();
const [secondaryLoading, setSecondaryLoading] = useLoadOnClick();
const [ctaLoading, setCtaLoading] = useLoadOnClick();

return (
<FlowLayout>
<Button
variant="cta"
loading={ctaLoading}
onClick={setCtaLoading}
style={{ width: 66 }}
>
{ctaLoading ? (
<Spinner size="small" aria-label="Sending" />
) : (
<>
<SendIcon aria-hidden />
Send
</>
)}
</Button>
<Button
variant="primary"
loading={primaryLoading}
onClick={setPrimaryLoading}
style={{ width: 66 }}
>
{primaryLoading ? (
<Spinner size="small" aria-label="Syncing" />
) : (
<>
<SyncIcon aria-hidden />
Sync
</>
)}
</Button>
<Button
variant="secondary"
loading={secondaryLoading}
onClick={setSecondaryLoading}
style={{ width: 87 }}
>
{secondaryLoading ? (
<Spinner size="small" aria-label="Refreshing" />
) : (
<>
<RefreshIcon aria-hidden />
Refresh
</>
)}
</Button>
</FlowLayout>
);
};

export const LoadingButtonsWithLabel: StoryFn<typeof Button> = () => {
const [primaryLoading, setPrimaryLoading] = useLoadOnClick();
const [secondaryLoading, setSecondaryLoading] = useLoadOnClick();
const [ctaLoading, setCtaLoading] = useLoadOnClick();

return (
<FlowLayout>
<Button variant="cta" loading={ctaLoading} onClick={setCtaLoading}>
{ctaLoading ? (
<>
<Spinner size="small" aria-label="Sending" />
Sending
</>
) : (
<>
<SendIcon aria-hidden />
Send
</>
)}
</Button>
<Button
variant="primary"
loading={primaryLoading}
onClick={setPrimaryLoading}
>
{primaryLoading ? (
<>
<Spinner size="small" aria-label="Syncing" />
Syncing
</>
) : (
<>
<SyncIcon aria-hidden />
Sync
</>
)}
</Button>
<Button
variant="secondary"
loading={secondaryLoading}
onClick={setSecondaryLoading}
>
{secondaryLoading ? (
<>
<Spinner size="small" aria-label="Refreshing" />
Refreshing
</>
) : (
<>
<RefreshIcon aria-hidden />
Refresh
</>
)}
</Button>
</FlowLayout>
);
};

export const LoadingButtonRenameExample = () => {
const [loading, setLoading] = useState(false);

const handlePrimaryClick = () => {
if (!loading) {
setLoading(true);
setTimeout(() => {
setLoading(false);
handleClose();
}, 3000);
}
};
const [open, setOpen] = useState(false);
const id = useId();

const handleRequestOpen = () => {
setOpen(true);
};

const onOpenChange = (value: boolean) => {
setOpen(value);
};

const handleClose = () => {
setOpen(false);
};

return (
<>
<Button onClick={handleRequestOpen}>Open dialog</Button>
<Dialog open={open} onOpenChange={onOpenChange} id={id}>
<DialogHeader header="Find and rename layers" />
<DialogContent>
<StackLayout>
<FormField>
<FormFieldLabel>Find</FormFieldLabel>
<Input defaultValue="UITK" />
</FormField>
<FormField>
<FormFieldLabel>Rename</FormFieldLabel>
<Input defaultValue="Salt" />
</FormField>
</StackLayout>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button
variant="cta"
loading={loading}
onClick={handlePrimaryClick}
style={{ width: 135 }}
>
{loading ? (
<>
<Spinner size="small" aria-label="Renaming" />
Renaming
</>
) : (
<>
Rename Layers
<DoubleChevronRightIcon />
</>
)}
</Button>
</DialogActions>
</Dialog>
</>
);
};
Loading

0 comments on commit 9285dfe

Please sign in to comment.