diff --git a/packages/core/src/button/Button.css b/packages/core/src/button/Button.css index de5bfa342d..a3ffc08418 100644 --- a/packages/core/src/button/Button.css +++ b/packages/core/src/button/Button.css @@ -175,44 +175,28 @@ --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); +.saltButton-accent.saltButton-loading { + --button-text-color: var(--salt-actionable-accent-foreground); + --button-text-color-hover: var(--salt-actionable-accent-foreground); + --button-text-color-active: var(--salt-actionable-accent-foreground); + --button-background: var(--salt-actionable-accent-background); + --button-background-hover: var(--salt-actionable-accent-background); + --button-background-active: var(--salt-actionable-accent-background); + --button-borderColor: var(--salt-actionable-accent-borderColor); + --button-borderColor-hover: var(--salt-actionable-accent-borderColor); + --button-borderColor-active: var(--salt-actionable-accent-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); +.saltButton-neutral.saltButton-loading { + --button-text-color: var(--salt-actionable-foreground); + --button-text-color-hover: var(--salt-actionable-foreground); + --button-text-color-active: var(--salt-actionable-foreground); + --button-background: var(--salt-actionable-background); + --button-background-hover: var(--salt-actionable-background); + --button-background-active: var(--salt-actionable-background); + --button-borderColor: var(--salt-actionable-borderColor); + --button-borderColor-hover: var(--salt-actionable-borderColor); + --button-borderColor-active: var(--salt-actionable-borderColor); cursor: default; } diff --git a/packages/core/stories/button/button.qa.stories.tsx b/packages/core/stories/button/button.qa.stories.tsx index 68fdacc470..5d509fff23 100644 --- a/packages/core/stories/button/button.qa.stories.tsx +++ b/packages/core/stories/button/button.qa.stories.tsx @@ -1,4 +1,4 @@ -import { Button } from "@salt-ds/core"; +import { Button, StackLayout } from "@salt-ds/core"; import { SearchIcon } from "@salt-ds/icons"; import type { Meta, StoryFn } from "@storybook/react"; import { @@ -18,82 +18,128 @@ export default { } as Meta; export const AllVariantsGrid: StoryFn = (props) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/packages/core/stories/button/button.stories.tsx b/packages/core/stories/button/button.stories.tsx index cc40586700..067d68b63f 100644 --- a/packages/core/stories/button/button.stories.tsx +++ b/packages/core/stories/button/button.stories.tsx @@ -1,20 +1,11 @@ import { Button, type ButtonProps, - Dialog, - DialogActions, - DialogContent, - DialogHeader, - FlowLayout, - FormField, - FormFieldLabel, - Input, + GridLayout, Spinner, StackLayout, - useId, } from "@salt-ds/core"; import { - DoubleChevronRightIcon, DownloadIcon, RefreshIcon, SearchIcon, @@ -250,15 +241,25 @@ function useLoadOnClick() { return [loading, handleClick] as const; } -export const LoadingButtonsReplaceIcon: StoryFn = () => { - const [primaryLoading, setPrimaryLoading] = useLoadOnClick(); - const [secondaryLoading, setSecondaryLoading] = useLoadOnClick(); - const [ctaLoading, setCtaLoading] = useLoadOnClick(); +export const LoadingButtonsReplaceIcon: StoryFn = () => { + const [accentSolidLoading, setAccentSolidLoading] = useLoadOnClick(); + const [accentOutlineLoading, setAccentOutlineLoading] = useLoadOnClick(); + const [accentTransparentLoading, setAccentTransparentLoading] = + useLoadOnClick(); + const [neutralSolidLoading, setNeutralSolidLoading] = useLoadOnClick(); + const [neutralOutlineLoading, setNeutralOutlineLoading] = useLoadOnClick(); + const [neutralTransparentLoading, setNeutralTransparentLoading] = + useLoadOnClick(); return ( - - - + + + + ); }; -export const LoadingButtons: StoryFn = () => { - const [primaryLoading, setPrimaryLoading] = useLoadOnClick(); - const [secondaryLoading, setSecondaryLoading] = useLoadOnClick(); - const [ctaLoading, setCtaLoading] = useLoadOnClick(); +export const LoadingButtons: StoryFn = () => { + const [accentSolidLoading, setAccentSolidLoading] = useLoadOnClick(); + const [accentOutlineLoading, setAccentOutlineLoading] = useLoadOnClick(); + const [accentTransparentLoading, setAccentTransparentLoading] = + useLoadOnClick(); + const [neutralSolidLoading, setNeutralSolidLoading] = useLoadOnClick(); + const [neutralOutlineLoading, setNeutralOutlineLoading] = useLoadOnClick(); + const [neutralTransparentLoading, setNeutralTransparentLoading] = + useLoadOnClick(); return ( - + - + + + + ); }; -export const LoadingButtonsWithLabel: StoryFn = () => { - const [primaryLoading, setPrimaryLoading] = useLoadOnClick(); - const [secondaryLoading, setSecondaryLoading] = useLoadOnClick(); - const [ctaLoading, setCtaLoading] = useLoadOnClick(); +export const LoadingButtonsWithLabel: StoryFn = () => { + const [accentSolidLoading, setAccentSolidLoading] = useLoadOnClick(); + const [accentOutlineLoading, setAccentOutlineLoading] = useLoadOnClick(); + const [accentTransparentLoading, setAccentTransparentLoading] = + useLoadOnClick(); + const [neutralSolidLoading, setNeutralSolidLoading] = useLoadOnClick(); + const [neutralOutlineLoading, setNeutralOutlineLoading] = useLoadOnClick(); + const [neutralTransparentLoading, setNeutralTransparentLoading] = + useLoadOnClick(); return ( - - - - ); -}; - -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 ( - <> - - - - - - - Find - - - - Rename - - - - - - - - - - + + + + ); }; diff --git a/site/docs/components/button/examples.mdx b/site/docs/components/button/examples.mdx index b2e4ea7362..9cae7aa32c 100644 --- a/site/docs/components/button/examples.mdx +++ b/site/docs/components/button/examples.mdx @@ -90,34 +90,36 @@ Use full width buttons on mobile devices or smaller viewports, the button will t - + - ## Loading Buttons +## Loading - The buttons will show a loading spinner when setting the prop `loading` to `true`. You should pass a message using the `loadingText` prop for screenreader to announce while loading state is true. It defaults to "Loading". +Buttons can show that a short action is being performed by passing `loading={true}`. This allows a [Spinner](/salt/components/spinner) to be displayed inside the button. - A Loading Button provides immediate feedback to the user after they trigger an action. It's typically used in situations where a user's action requires some processing time (more than a few seconds) before the result can be displayed. Use cases include: +### Best practices - - Form Submission: When a user submits a [form](/salt/patterns/forms), the Loading Button indicates that the form data is being processed. This helps prevent the user from clicking the submit button multiple times, which could potentially cause duplicate submissions or errors. - - Data Fetching: If a button triggers a data fetch from a server, a Loading Button indicates that the data is being retrieved. This is especially useful in applications where data retrieval might take a significant amount of time. - - Processing Actions: If a button triggers a complex action that requires processing, such as generating a report or performing a calculation, a Loading Button indicates that the action is in progress. +- Use loading buttons for actions that take a short amount of time to complete, such as submitting a form, fetching data from a server or a quick processing action (generating a report or performing a calculation). +- The [Content Status](salt/patterns/content-status) pattern can be used with a loading button to provide additional feedback to the user. +- Don't use loading buttons if an action is instantaneous or very quick. For further guidance, view the [Salt duration foundation](/salt/foundations/duration). +- Don't use loading buttons if the action is complex and will take a significant amount of time (more than a few seconds) e.g. uploading or downloading large files, installing software, or completing a multi-step process. Use a [Progress Bar](/salt/components/progress) or a [Toast](/salt/components/toast) instead. - The [Content Status](salt/patterns/content-status) pattern can be displayed with a Loading Button to provide additional feedback to the user. + - Alternative solutions for other use cases: + - - Instant Actions: If an action is instantaneous or very quick. A Loading Button is not necessary. - - Complex processing: Use a [Progress Bar](/salt/components/progress) when a task is going to take a significant amount of time (more than a few seconds) and when it's possible to estimate the progress of the task. This could be when uploading or downloading large files, installing software, or completing a multi-step process. +## Loading buttons with labels - +A loading button can also have a label to provide more context about the action being performed. - + - ## Loading Buttons with Label + - The buttons will show a loading spinner when setting the prop `loading` to `true`. You should pass a message using the `loadingText` prop and `showLoadingText` as `true` for screenreader to announce while loading state is true. It defaults to "Loading". +## Loading buttons - Replace icon - +An icon can be swapped out with a spinner when a button is in a loading state. + + diff --git a/site/src/examples/button/Loading.tsx b/site/src/examples/button/Loading.tsx new file mode 100644 index 0000000000..d47f298730 --- /dev/null +++ b/site/src/examples/button/Loading.tsx @@ -0,0 +1,130 @@ +import { Button, GridLayout, Spinner } from "@salt-ds/core"; +import { RefreshIcon, SendIcon, SyncIcon } from "@salt-ds/icons"; +import { type ReactElement, useState } from "react"; + +function useLoadOnClick() { + const [loading, setLoading] = useState(false); + + const handleClick = () => { + if (!loading) { + setLoading(true); + setTimeout(() => { + setLoading(false); + }, 3000); + } + }; + + return [loading, handleClick] as const; +} + +export const Loading = (): ReactElement => { + const [accentSolidLoading, setAccentSolidLoading] = useLoadOnClick(); + const [accentOutlineLoading, setAccentOutlineLoading] = useLoadOnClick(); + const [accentTransparentLoading, setAccentTransparentLoading] = + useLoadOnClick(); + const [neutralSolidLoading, setNeutralSolidLoading] = useLoadOnClick(); + const [neutralOutlineLoading, setNeutralOutlineLoading] = useLoadOnClick(); + const [neutralTransparentLoading, setNeutralTransparentLoading] = + useLoadOnClick(); + + return ( + + + + + + + + + ); +}; diff --git a/site/src/examples/button/LoadingButtons.tsx b/site/src/examples/button/LoadingButtons.tsx deleted file mode 100644 index ce32e761ce..0000000000 --- a/site/src/examples/button/LoadingButtons.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Button } from "@salt-ds/core"; -import { RefreshIcon, SendIcon, SyncIcon } from "@salt-ds/icons"; -import { type ReactElement, useState } from "react"; - -export const LoadingButtons = (): ReactElement => { - const [primaryLoadingState, setPrimaryLoadingState] = useState(false); - const [secondaryLoadingState, setSecondaryLoadingState] = useState(false); - const [ctaLoadingState, setCtaLoadingState] = useState(false); - - const handlePrimaryClick = () => { - setPrimaryLoadingState(true); - setTimeout(() => { - setPrimaryLoadingState(false); - }, 3000); - }; - const handleSecondaryClick = () => { - setSecondaryLoadingState(true); - setTimeout(() => { - setSecondaryLoadingState(false); - }, 3000); - }; - const handleCtaClick = () => { - setCtaLoadingState(true); - setTimeout(() => { - setCtaLoadingState(false); - }, 3000); - }; - - return ( -
- - - -
- ); -}; diff --git a/site/src/examples/button/LoadingButtonsWithLabel.tsx b/site/src/examples/button/LoadingButtonsWithLabel.tsx deleted file mode 100644 index ad74f82fb9..0000000000 --- a/site/src/examples/button/LoadingButtonsWithLabel.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Button } from "@salt-ds/core"; -import { RefreshIcon, SendIcon, SyncIcon } from "@salt-ds/icons"; -import { type ReactElement, useState } from "react"; - -export const LoadingButtonsWithLabel = (): ReactElement => { - const [primaryLoadingState, setPrimaryLoadingState] = useState(false); - const [secondaryLoadingState, setSecondaryLoadingState] = useState(false); - const [ctaLoadingState, setCtaLoadingState] = useState(false); - - const handlePrimaryClick = () => { - setPrimaryLoadingState(true); - setTimeout(() => { - setPrimaryLoadingState(false); - }, 3000); - }; - const handleSecondaryClick = () => { - setSecondaryLoadingState(true); - setTimeout(() => { - setSecondaryLoadingState(false); - }, 3000); - }; - const handleCtaClick = () => { - setCtaLoadingState(true); - setTimeout(() => { - setCtaLoadingState(false); - }, 3000); - }; - - return ( -
- - - -
- ); -}; diff --git a/site/src/examples/button/LoadingReplaceIcon.tsx b/site/src/examples/button/LoadingReplaceIcon.tsx new file mode 100644 index 0000000000..fcf07187aa --- /dev/null +++ b/site/src/examples/button/LoadingReplaceIcon.tsx @@ -0,0 +1,112 @@ +import { Button, GridLayout, Spinner } from "@salt-ds/core"; +import { RefreshIcon, SendIcon, SyncIcon } from "@salt-ds/icons"; +import { type ReactElement, useState } from "react"; + +function useLoadOnClick() { + const [loading, setLoading] = useState(false); + + const handleClick = () => { + if (!loading) { + setLoading(true); + setTimeout(() => { + setLoading(false); + }, 3000); + } + }; + + return [loading, handleClick] as const; +} + +export const LoadingReplaceIcon = (): ReactElement => { + const [accentSolidLoading, setAccentSolidLoading] = useLoadOnClick(); + const [accentOutlineLoading, setAccentOutlineLoading] = useLoadOnClick(); + const [accentTransparentLoading, setAccentTransparentLoading] = + useLoadOnClick(); + const [neutralSolidLoading, setNeutralSolidLoading] = useLoadOnClick(); + const [neutralOutlineLoading, setNeutralOutlineLoading] = useLoadOnClick(); + const [neutralTransparentLoading, setNeutralTransparentLoading] = + useLoadOnClick(); + + return ( + + + + + + + + + ); +}; diff --git a/site/src/examples/button/LoadingWithLabel.tsx b/site/src/examples/button/LoadingWithLabel.tsx new file mode 100644 index 0000000000..8a526c1fba --- /dev/null +++ b/site/src/examples/button/LoadingWithLabel.tsx @@ -0,0 +1,142 @@ +import { Button, GridLayout, Spinner } from "@salt-ds/core"; +import { RefreshIcon, SendIcon, SyncIcon } from "@salt-ds/icons"; +import { type ReactElement, useState } from "react"; + +function useLoadOnClick() { + const [loading, setLoading] = useState(false); + + const handleClick = () => { + if (!loading) { + setLoading(true); + setTimeout(() => { + setLoading(false); + }, 3000); + } + }; + + return [loading, handleClick] as const; +} + +export const LoadingWithLabel = (): ReactElement => { + const [accentSolidLoading, setAccentSolidLoading] = useLoadOnClick(); + const [accentOutlineLoading, setAccentOutlineLoading] = useLoadOnClick(); + const [accentTransparentLoading, setAccentTransparentLoading] = + useLoadOnClick(); + const [neutralSolidLoading, setNeutralSolidLoading] = useLoadOnClick(); + const [neutralOutlineLoading, setNeutralOutlineLoading] = useLoadOnClick(); + const [neutralTransparentLoading, setNeutralTransparentLoading] = + useLoadOnClick(); + + return ( + + + + + + + + + ); +}; diff --git a/site/src/examples/button/index.ts b/site/src/examples/button/index.ts index 205d1e5e5e..edd758b9a9 100644 --- a/site/src/examples/button/index.ts +++ b/site/src/examples/button/index.ts @@ -8,5 +8,6 @@ export * from "./Primary"; export * from "./Secondary"; export * from "./FullWidth"; export * from "./FocusableWhenDisabled"; -export * from "./LoadingButtons"; -export * from "./LoadingButtonsWithLabel"; +export * from "./Loading"; +export * from "./LoadingWithLabel"; +export * from "./LoadingReplaceIcon";