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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Added `loading` prop to Button and ButtonIcon components for displaying spinner during async operations.
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -187,6 +187,8 @@ export interface ButtonIconProps extends ButtonProps_2 {
|
||||
// (undocumented)
|
||||
icon?: ReactElement;
|
||||
// (undocumented)
|
||||
loading?: boolean;
|
||||
// (undocumented)
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
// (undocumented)
|
||||
variant?:
|
||||
@@ -228,6 +230,8 @@ export interface ButtonProps extends ButtonProps_2 {
|
||||
// (undocumented)
|
||||
iconStart?: ReactElement;
|
||||
// (undocumented)
|
||||
loading?: boolean;
|
||||
// (undocumented)
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
// (undocumented)
|
||||
variant?:
|
||||
@@ -418,15 +422,20 @@ export const componentDefinitions: {
|
||||
readonly Button: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Button';
|
||||
readonly content: 'bui-ButtonContent';
|
||||
readonly spinner: 'bui-ButtonSpinner';
|
||||
};
|
||||
readonly dataAttributes: {
|
||||
readonly size: readonly ['small', 'medium', 'large'];
|
||||
readonly variant: readonly ['primary', 'secondary', 'tertiary'];
|
||||
readonly loading: readonly [true, false];
|
||||
};
|
||||
};
|
||||
readonly ButtonIcon: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-ButtonIcon';
|
||||
readonly content: 'bui-ButtonIconContent';
|
||||
readonly spinner: 'bui-ButtonIconSpinner';
|
||||
};
|
||||
};
|
||||
readonly ButtonLink: {
|
||||
|
||||
@@ -18,22 +18,27 @@
|
||||
|
||||
@layer components {
|
||||
.bui-Button {
|
||||
border: none;
|
||||
--loading-duration: 200ms;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
user-select: none;
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: var(--bui-radius-2);
|
||||
gap: var(--bui-space-1_5);
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--loading-duration) ease-out,
|
||||
box-shadow var(--loading-duration) ease-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-loading='true'] {
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='primary'] {
|
||||
@@ -54,7 +59,8 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
background-color: var(--bui-bg-solid-disabled);
|
||||
color: var(--bui-fg-solid-disabled);
|
||||
}
|
||||
@@ -80,7 +86,8 @@
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-disabled);
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
@@ -105,31 +112,92 @@
|
||||
box-shadow: inset 0 0 0 2px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
background-color: transparent;
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] {
|
||||
font-size: var(--bui-font-size-3);
|
||||
padding: 0 var(--bui-space-2);
|
||||
height: 2rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='medium'] {
|
||||
font-size: var(--bui-font-size-4);
|
||||
padding: 0 var(--bui-space-3);
|
||||
height: 2.5rem;
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] {
|
||||
font-size: var(--bui-font-size-3);
|
||||
padding: 0 var(--bui-space-2);
|
||||
height: 2rem;
|
||||
.bui-ButtonContent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--bui-space-1_5);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: opacity var(--loading-duration) ease-out;
|
||||
|
||||
.bui-Button[data-loading='true'] & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
.bui-ButtonSpinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity var(--loading-duration) ease-in;
|
||||
|
||||
.bui-Button[data-loading='true'] & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& svg {
|
||||
animation: bui-spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='medium'] svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bui-Button {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.bui-ButtonContent {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.bui-ButtonSpinner {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.bui-ButtonSpinner svg {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bui-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Button } from './Button';
|
||||
import { Flex } from '../Flex';
|
||||
import { Text } from '../Text';
|
||||
import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/Button',
|
||||
@@ -212,3 +213,80 @@ export const Playground: Story = {
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="primary" loading={isLoading} onPress={handleClick}>
|
||||
Load more items
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingVariants: Story = {
|
||||
render: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Primary</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<Button variant="primary" size="small" loading>
|
||||
Small Loading
|
||||
</Button>
|
||||
<Button variant="primary" size="medium" loading>
|
||||
Medium Loading
|
||||
</Button>
|
||||
<Button variant="primary" loading iconStart={<RiCloudLine />}>
|
||||
With Icon
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Text>Secondary</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<Button variant="secondary" size="small" loading>
|
||||
Small Loading
|
||||
</Button>
|
||||
<Button variant="secondary" size="medium" loading>
|
||||
Medium Loading
|
||||
</Button>
|
||||
<Button variant="secondary" loading iconStart={<RiCloudLine />}>
|
||||
With Icon
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Text>Tertiary</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<Button variant="tertiary" size="small" loading>
|
||||
Small Loading
|
||||
</Button>
|
||||
<Button variant="tertiary" size="medium" loading>
|
||||
Medium Loading
|
||||
</Button>
|
||||
<Button variant="tertiary" loading iconStart={<RiCloudLine />}>
|
||||
With Icon
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Text>Loading vs Disabled</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<Button variant="primary" loading>
|
||||
Loading
|
||||
</Button>
|
||||
<Button variant="primary" isDisabled>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button variant="primary" loading isDisabled>
|
||||
Both (Disabled Wins)
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Button as RAButton } from 'react-aria-components';
|
||||
import { Button as RAButton, ProgressBar } from 'react-aria-components';
|
||||
import { RiLoader4Line } from '@remixicon/react';
|
||||
import type { ButtonProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import styles from './Button.module.css';
|
||||
@@ -30,18 +31,38 @@ export const Button = forwardRef(
|
||||
...props,
|
||||
});
|
||||
|
||||
const { children, className, iconStart, iconEnd, ...rest } = cleanedProps;
|
||||
const { children, className, iconStart, iconEnd, loading, ...rest } =
|
||||
cleanedProps;
|
||||
|
||||
return (
|
||||
<RAButton
|
||||
className={clsx(classNames.root, styles[classNames.root], className)}
|
||||
ref={ref}
|
||||
isPending={loading}
|
||||
{...dataAttributes}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
{({ isPending }) => (
|
||||
<>
|
||||
<span
|
||||
className={clsx(classNames.content, styles[classNames.content])}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</span>
|
||||
|
||||
{isPending && (
|
||||
<ProgressBar
|
||||
aria-label="Loading"
|
||||
isIndeterminate
|
||||
className={clsx(classNames.spinner, styles[classNames.spinner])}
|
||||
>
|
||||
<RiLoader4Line aria-hidden="true" />
|
||||
</ProgressBar>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</RAButton>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -33,4 +33,5 @@ export interface ButtonProps extends RAButtonProps {
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
children?: ReactNode;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { ButtonIcon } from './ButtonIcon';
|
||||
import { Flex } from '../Flex';
|
||||
import { Text } from '../Text';
|
||||
import { RiCloudLine } from '@remixicon/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/ButtonIcon',
|
||||
@@ -83,3 +85,91 @@ export const Responsive: Story = {
|
||||
},
|
||||
render: args => <ButtonIcon {...args} icon={<RiCloudLine />} />,
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonIcon
|
||||
variant="primary"
|
||||
icon={<RiCloudLine />}
|
||||
loading={isLoading}
|
||||
onPress={handleClick}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingVariants: Story = {
|
||||
render: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Primary</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<ButtonIcon
|
||||
variant="primary"
|
||||
size="small"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
/>
|
||||
<ButtonIcon
|
||||
variant="primary"
|
||||
size="medium"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Text>Secondary</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<ButtonIcon
|
||||
variant="secondary"
|
||||
size="small"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
/>
|
||||
<ButtonIcon
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Text>Tertiary</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<ButtonIcon
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
/>
|
||||
<ButtonIcon
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Text>Loading vs Disabled</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<ButtonIcon variant="primary" icon={<RiCloudLine />} loading />
|
||||
<ButtonIcon variant="primary" icon={<RiCloudLine />} isDisabled />
|
||||
<ButtonIcon
|
||||
variant="primary"
|
||||
icon={<RiCloudLine />}
|
||||
loading
|
||||
isDisabled
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Button as RAButton } from 'react-aria-components';
|
||||
import { Button as RAButton, ProgressBar } from 'react-aria-components';
|
||||
import { RiLoader4Line } from '@remixicon/react';
|
||||
import type { ButtonIconProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import stylesButtonIcon from './ButtonIcon.module.css';
|
||||
@@ -33,7 +34,7 @@ export const ButtonIcon = forwardRef(
|
||||
|
||||
const { classNames: classNamesButtonIcon } = useStyles('ButtonIcon');
|
||||
|
||||
const { className, icon, ...rest } = cleanedProps;
|
||||
const { className, icon, loading, ...rest } = cleanedProps;
|
||||
|
||||
return (
|
||||
<RAButton
|
||||
@@ -45,10 +46,41 @@ export const ButtonIcon = forwardRef(
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
isPending={loading}
|
||||
{...dataAttributes}
|
||||
{...rest}
|
||||
>
|
||||
{icon}
|
||||
{({ isPending }) => (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
classNames.content,
|
||||
classNamesButtonIcon.content,
|
||||
stylesButton[classNames.content],
|
||||
stylesButtonIcon[classNamesButtonIcon.content],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
|
||||
{isPending && (
|
||||
<ProgressBar
|
||||
aria-label="Loading"
|
||||
isIndeterminate
|
||||
className={clsx(
|
||||
classNames.spinner,
|
||||
classNamesButtonIcon.spinner,
|
||||
stylesButton[classNames.spinner],
|
||||
stylesButtonIcon[classNamesButtonIcon.spinner],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<RiLoader4Line aria-hidden="true" />
|
||||
</ProgressBar>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</RAButton>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -31,4 +31,5 @@ export interface ButtonIconProps extends RAButtonProps {
|
||||
| 'tertiary'
|
||||
| Partial<Record<Breakpoint, 'primary' | 'secondary' | 'tertiary'>>;
|
||||
icon?: ReactElement;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -41,47 +41,38 @@ export const ButtonLink = forwardRef(
|
||||
|
||||
const isExternal = isExternalLink(href);
|
||||
|
||||
// If it's an external link, render RALink without RouterProvider
|
||||
if (isExternal) {
|
||||
return (
|
||||
<RALink
|
||||
className={clsx(
|
||||
classNames.root,
|
||||
classNamesButtonLink.root,
|
||||
stylesButton[classNames.root],
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
href={href}
|
||||
{...rest}
|
||||
const linkButton = (
|
||||
<RALink
|
||||
className={clsx(
|
||||
classNames.root,
|
||||
classNamesButtonLink.root,
|
||||
stylesButton[classNames.root],
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
href={href}
|
||||
{...rest}
|
||||
>
|
||||
<span
|
||||
className={clsx(classNames.content, stylesButton[classNames.content])}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</RALink>
|
||||
);
|
||||
</span>
|
||||
</RALink>
|
||||
);
|
||||
|
||||
// If it's an external link, render RALink without RouterProvider
|
||||
if (isExternal) {
|
||||
return linkButton;
|
||||
}
|
||||
|
||||
// For internal links, use RouterProvider
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<RALink
|
||||
className={clsx(
|
||||
classNames.root,
|
||||
classNamesButtonLink.root,
|
||||
stylesButton[classNames.root],
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
href={href}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
{children}
|
||||
{iconEnd}
|
||||
</RALink>
|
||||
{linkButton}
|
||||
</RouterProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -63,15 +63,20 @@ export const componentDefinitions = {
|
||||
Button: {
|
||||
classNames: {
|
||||
root: 'bui-Button',
|
||||
content: 'bui-ButtonContent',
|
||||
spinner: 'bui-ButtonSpinner',
|
||||
},
|
||||
dataAttributes: {
|
||||
size: ['small', 'medium', 'large'] as const,
|
||||
variant: ['primary', 'secondary', 'tertiary'] as const,
|
||||
loading: [true, false] as const,
|
||||
},
|
||||
},
|
||||
ButtonIcon: {
|
||||
classNames: {
|
||||
root: 'bui-ButtonIcon',
|
||||
content: 'bui-ButtonIconContent',
|
||||
spinner: 'bui-ButtonIconSpinner',
|
||||
},
|
||||
},
|
||||
ButtonLink: {
|
||||
|
||||
Reference in New Issue
Block a user