feat(ui): add loading state to Button and ButtonIcon components

Add optional `loading` prop to both Button and ButtonIcon components that displays
a spinner and disables interaction while async operations are in progress.

Implementation details:
- Uses React Aria's ProgressBar with indeterminate state for spinner
- Displays RiLoader4Line icon with smooth rotation animation
- Wraps content in container with opacity transition when loading
- Accessible via aria-label "Loading" on progress indicator
- Component definitions updated with content, spinner class names and loading data attribute

Documentation:
- Added Loading examples to both Button and ButtonIcon docs showing interactive demos
- Updated props tables with loading boolean prop
- Created Storybook stories covering all variants, sizes, and with icons

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2025-11-07 09:57:23 +01:00
parent 0f43bad993
commit 7839e7bd9e
15 changed files with 392 additions and 57 deletions
+13
View File
@@ -9,6 +9,7 @@ import {
buttonIconVariantsSnippet,
buttonIconSizesSnippet,
buttonIconDisabledSnippet,
buttonIconLoadingSnippet,
buttonIconResponsiveSnippet,
buttonIconAsLinkSnippet,
} from './button-icon.props';
@@ -74,6 +75,18 @@ Here's a view when buttons are disabled.
code={buttonIconDisabledSnippet}
/>
### Loading
Here's a view when buttons are in a loading state.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Loading" />}
code={buttonIconLoadingSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
+3
View File
@@ -19,6 +19,7 @@ export const buttonIconPropDefs: Record<string, PropDef> = {
},
icon: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
loading: { type: 'boolean', default: 'false', responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
@@ -50,6 +51,8 @@ export const buttonIconSizesSnippet = `<Flex align="center">
export const buttonIconDisabledSnippet = `<ButtonIcon icon={<Icon name="cloud" />} isDisabled />`;
export const buttonIconLoadingSnippet = `<ButtonIcon icon={<Icon name="cloud" />} variant="primary" loading={isLoading} onPress={handleClick} />`;
export const buttonIconResponsiveSnippet = `<ButtonIcon icon={<Icon name="cloud" />} variant={{ initial: 'primary', lg: 'secondary' }} />`;
export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
+13
View File
@@ -9,6 +9,7 @@ import {
buttonSizesSnippet,
buttonIconsSnippet,
buttonDisabledSnippet,
buttonLoadingSnippet,
buttonResponsiveSnippet,
buttonAsLinkSnippet,
} from './button.props';
@@ -86,6 +87,18 @@ Here's a view when buttons are disabled.
code={buttonDisabledSnippet}
/>
### Loading
Here's a view when buttons are in a loading state.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="Loading" />}
code={buttonLoadingSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
+5
View File
@@ -17,6 +17,7 @@ export const buttonPropDefs: Record<string, PropDef> = {
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
loading: { type: 'boolean', default: 'false', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
type: {
type: 'enum',
@@ -65,6 +66,10 @@ export const buttonResponsiveSnippet = `<Button variant={{ initial: 'primary', l
Responsive Button
</Button>`;
export const buttonLoadingSnippet = `<Button variant="primary" loading={isLoading} onPress={handleClick}>
Load more items
</Button>`;
export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
<ButtonLink href="https://ui.backstage.io" target="_blank">