feat(twenty-front/Button): add loading state on Button (#10536)
This commit is contained in:
@ -17,12 +17,12 @@ import {
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { Keys } from 'react-hotkeys-hook';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { useDropdown } from '../hooks/useDropdown';
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
const StyledDropdownFallbackAnchor = styled.div`
|
||||
left: 0;
|
||||
|
||||
@ -82,9 +82,7 @@ export const Empty: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
|
||||
const buttons = await canvas.findAllByRole('button', {
|
||||
name: 'Open Dropdown',
|
||||
});
|
||||
const buttons = await canvas.findAllByRole('button');
|
||||
userEvent.click(buttons[0]);
|
||||
|
||||
await waitFor(async () => {
|
||||
@ -225,16 +223,16 @@ export const WithHeaders: Story = {
|
||||
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<>
|
||||
{optionsMock.slice(0, 3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
{optionsMock.slice(0, 3).map((item) => (
|
||||
<MenuItem key={item.id} text={item.name} />
|
||||
))}
|
||||
</>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{optionsMock.slice(3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
{optionsMock.slice(3).map((item) => (
|
||||
<MenuItem key={item.id} text={item.name} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
@ -282,7 +280,7 @@ export const WithInput: Story = {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{optionsMock.map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
<MenuItem key={name} text={name} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
|
||||
@ -31,6 +32,12 @@ export const Default: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
sleep(1000);
|
||||
|
||||
await canvas.findByRole('button', { name: 'View billing details' });
|
||||
const buttons = await canvas.findAllByRole('button');
|
||||
|
||||
expect(
|
||||
buttons.findIndex((button) =>
|
||||
button.outerHTML.includes('View billing details'),
|
||||
),
|
||||
).toBeGreaterThan(-1);
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
|
||||
@ -30,6 +31,10 @@ export const Default: Story = {
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
await canvas.getByRole('button', { name: 'Copy link' });
|
||||
const buttons = await canvas.getAllByRole('button');
|
||||
|
||||
expect(
|
||||
buttons.findIndex((button) => button.outerHTML.includes('Copy link')),
|
||||
).toBeGreaterThan(-1);
|
||||
},
|
||||
};
|
||||
|
||||
@ -28,7 +28,7 @@ describe('formatDateString', () => {
|
||||
|
||||
it('should format date as relative when displayAsRelativeDate is true', () => {
|
||||
const mockDate = DateTime.now().minus({ months: 2 }).toISO();
|
||||
const mockRelativeDate = '2 months ago';
|
||||
const mockRelativeDate = 'about 2 months ago';
|
||||
|
||||
jest.mock('@/localization/utils/formatDateISOStringToRelativeDate', () => ({
|
||||
formatDateISOStringToRelativeDate: jest
|
||||
|
||||
@ -30,7 +30,7 @@ describe('formatDateTimeString', () => {
|
||||
|
||||
it('should format date as relative when displayAsRelativeDate is true', () => {
|
||||
const mockDate = DateTime.now().minus({ months: 2 }).toISO();
|
||||
const mockRelativeDate = '2 months ago';
|
||||
const mockRelativeDate = 'about 2 months ago';
|
||||
|
||||
jest.mock('@/localization/utils/formatDateISOStringToRelativeDate', () => ({
|
||||
formatDateISOStringToRelativeDate: jest
|
||||
|
||||
@ -2,7 +2,7 @@ import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconInfoCircle } from '@ui/display/icon/components/TablerIcons';
|
||||
|
||||
import { Button } from '@ui/input/button/components/Button';
|
||||
import { Button } from '@ui/input/button/components/Button/Button';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
@ -6,7 +6,12 @@ import { getOsShortcutSeparator } from '@ui/utilities/device/getOsShortcutSepara
|
||||
import { MotionProps, motion } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { ButtonAccent, ButtonProps, ButtonSize, ButtonVariant } from './Button';
|
||||
import {
|
||||
ButtonAccent,
|
||||
ButtonProps,
|
||||
ButtonSize,
|
||||
ButtonVariant,
|
||||
} from './Button/Button';
|
||||
|
||||
export type AnimatedButtonProps = ButtonProps &
|
||||
Pick<MotionProps, 'animate' | 'transition'> & {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import isPropValid from '@emotion/is-prop-valid';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Pill } from '@ui/components/Pill/Pill';
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { useIsMobile } from '@ui/utilities';
|
||||
import { getOsShortcutSeparator } from '@ui/utilities/device/getOsShortcutSeparator';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ButtonText } from './internal/ButtonText';
|
||||
import { ButtonIcon } from '@ui/input/button/components/Button/internal/ButtonIcon';
|
||||
import { ButtonSoon } from '@ui/input/button/components/Button/internal/ButtonSoon';
|
||||
import { ButtonHotkeys } from '@ui/input/button/components/Button/internal/ButtonHotKeys';
|
||||
|
||||
export type ButtonSize = 'medium' | 'small';
|
||||
export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right';
|
||||
@ -33,6 +35,7 @@ export type ButtonProps = {
|
||||
dataTestId?: string;
|
||||
hotkeys?: string[];
|
||||
ariaLabel?: string;
|
||||
loading?: boolean;
|
||||
} & React.ComponentProps<'button'>;
|
||||
|
||||
const StyledButton = styled('button', {
|
||||
@ -51,7 +54,8 @@ const StyledButton = styled('button', {
|
||||
| 'justify'
|
||||
| 'to'
|
||||
| 'target'
|
||||
>
|
||||
| 'loading'
|
||||
> & { hasIcon: boolean }
|
||||
>`
|
||||
align-items: center;
|
||||
${({ theme, variant, inverted, accent, disabled, focus }) => {
|
||||
@ -337,8 +341,10 @@ const StyledButton = styled('button', {
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
||||
justify-content: ${({ justify }) => justify};
|
||||
padding: ${({ theme }) => {
|
||||
return `0 ${theme.spacing(2)}`;
|
||||
padding: ${({ theme, hasIcon }) => {
|
||||
return `0 ${theme.spacing(2)} 0 ${
|
||||
hasIcon ? theme.spacing(7) : theme.spacing(2)
|
||||
}`;
|
||||
}};
|
||||
|
||||
transition: background 0.1s ease;
|
||||
@ -352,47 +358,56 @@ const StyledButton = styled('button', {
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSoonPill = styled(Pill)`
|
||||
margin-left: auto;
|
||||
`;
|
||||
const StyledButtonWrapper = styled.div<
|
||||
Pick<ButtonProps, 'loading' | 'variant' | 'accent' | 'inverted' | 'disabled'>
|
||||
>`
|
||||
${({ theme, variant, accent, inverted, disabled }) => css`
|
||||
--tw-button-color: ${(() => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
switch (accent) {
|
||||
case 'default':
|
||||
return !inverted
|
||||
? !disabled
|
||||
? theme.font.color.secondary
|
||||
: theme.font.color.extraLight
|
||||
: theme.font.color.secondary;
|
||||
case 'blue':
|
||||
return !inverted ? theme.grayScale.gray0 : theme.color.blue;
|
||||
case 'danger':
|
||||
return !inverted ? theme.background.primary : theme.color.red;
|
||||
}
|
||||
break;
|
||||
case 'secondary':
|
||||
case 'tertiary':
|
||||
switch (accent) {
|
||||
case 'default':
|
||||
return !inverted
|
||||
? !disabled
|
||||
? theme.font.color.secondary
|
||||
: theme.font.color.extraLight
|
||||
: theme.font.color.inverted;
|
||||
case 'blue':
|
||||
return !inverted
|
||||
? !disabled
|
||||
? theme.color.blue
|
||||
: theme.accent.accent4060
|
||||
: theme.font.color.inverted;
|
||||
case 'danger':
|
||||
return !inverted
|
||||
? theme.font.color.danger
|
||||
: theme.font.color.inverted;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return theme.font.color.secondary; // Valeur par défaut
|
||||
})()};
|
||||
`}
|
||||
|
||||
const StyledSeparator = styled.div<{
|
||||
buttonSize: ButtonSize;
|
||||
accent: ButtonAccent;
|
||||
}>`
|
||||
background: ${({ theme, accent }) => {
|
||||
switch (accent) {
|
||||
case 'blue':
|
||||
return theme.border.color.blue;
|
||||
case 'danger':
|
||||
return theme.border.color.danger;
|
||||
default:
|
||||
return theme.font.color.light;
|
||||
}
|
||||
}};
|
||||
height: ${({ theme, buttonSize }) =>
|
||||
theme.spacing(buttonSize === 'small' ? 2 : 4)};
|
||||
margin: 0;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledShortcutLabel = styled.div<{
|
||||
variant: ButtonVariant;
|
||||
accent: ButtonAccent;
|
||||
}>`
|
||||
color: ${({ theme, variant, accent }) => {
|
||||
switch (accent) {
|
||||
case 'blue':
|
||||
return theme.border.color.blue;
|
||||
case 'danger':
|
||||
return variant === 'primary'
|
||||
? theme.border.color.danger
|
||||
: theme.color.red40;
|
||||
default:
|
||||
return theme.font.color.light;
|
||||
}
|
||||
}};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 100%;
|
||||
max-width: ${({ loading, theme }) =>
|
||||
loading ? `calc(100% - ${theme.spacing(8)})` : 'none'};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const Button = ({
|
||||
@ -416,46 +431,53 @@ export const Button = ({
|
||||
hotkeys,
|
||||
ariaLabel,
|
||||
type,
|
||||
loading,
|
||||
}: ButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [isFocused, setIsFocused] = React.useState(propFocus);
|
||||
const [isFocused, setIsFocused] = useState(propFocus);
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
fullWidth={fullWidth}
|
||||
<StyledButtonWrapper
|
||||
loading={loading}
|
||||
variant={variant}
|
||||
inverted={inverted}
|
||||
size={size}
|
||||
position={position}
|
||||
disabled={soon || disabled}
|
||||
focus={isFocused}
|
||||
justify={justify}
|
||||
accent={accent}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
to={to}
|
||||
as={to ? Link : 'button'}
|
||||
target={target}
|
||||
data-testid={dataTestId}
|
||||
aria-label={ariaLabel}
|
||||
type={type}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
>
|
||||
{Icon && <Icon size={theme.icon.size.sm} />}
|
||||
{title}
|
||||
{hotkeys && !isMobile && (
|
||||
<>
|
||||
<StyledSeparator buttonSize={size} accent={accent} />
|
||||
<StyledShortcutLabel variant={variant} accent={accent}>
|
||||
{hotkeys.join(getOsShortcutSeparator())}
|
||||
</StyledShortcutLabel>
|
||||
</>
|
||||
)}
|
||||
{soon && <StyledSoonPill label="Soon" />}
|
||||
</StyledButton>
|
||||
{(loading || Icon) && <ButtonIcon Icon={Icon} loading={loading} />}
|
||||
<StyledButton
|
||||
fullWidth={fullWidth}
|
||||
variant={variant}
|
||||
inverted={inverted}
|
||||
position={position}
|
||||
disabled={soon || disabled}
|
||||
hasIcon={!!Icon}
|
||||
focus={isFocused}
|
||||
justify={justify}
|
||||
accent={accent}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
to={to}
|
||||
as={to ? Link : 'button'}
|
||||
target={target}
|
||||
data-testid={dataTestId}
|
||||
aria-label={ariaLabel}
|
||||
type={type}
|
||||
loading={loading}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
>
|
||||
<ButtonText hasIcon={!!Icon} title={title} loading={loading} />
|
||||
{hotkeys && !isMobile && (
|
||||
<ButtonHotkeys
|
||||
hotkeys={hotkeys}
|
||||
variant={variant}
|
||||
accent={accent}
|
||||
size={size}
|
||||
/>
|
||||
)}
|
||||
{soon && <ButtonSoon />}
|
||||
</StyledButton>
|
||||
</StyledButtonWrapper>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const baseTransitionTiming = 300;
|
||||
@ -0,0 +1,61 @@
|
||||
import { getOsShortcutSeparator } from '@ui/utilities';
|
||||
import { ButtonAccent, ButtonSize, ButtonVariant } from '@ui/input';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledSeparator = styled.div<{
|
||||
buttonSize: ButtonSize;
|
||||
accent: ButtonAccent;
|
||||
}>`
|
||||
background: ${({ theme, accent }) => {
|
||||
switch (accent) {
|
||||
case 'blue':
|
||||
return theme.border.color.blue;
|
||||
case 'danger':
|
||||
return theme.border.color.danger;
|
||||
default:
|
||||
return theme.font.color.light;
|
||||
}
|
||||
}};
|
||||
height: ${({ theme, buttonSize }) =>
|
||||
theme.spacing(buttonSize === 'small' ? 2 : 4)};
|
||||
margin: 0;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledShortcutLabel = styled.div<{
|
||||
variant: ButtonVariant;
|
||||
accent: ButtonAccent;
|
||||
}>`
|
||||
color: ${({ theme, variant, accent }) => {
|
||||
switch (accent) {
|
||||
case 'blue':
|
||||
return theme.border.color.blue;
|
||||
case 'danger':
|
||||
return variant === 'primary'
|
||||
? theme.border.color.danger
|
||||
: theme.color.red40;
|
||||
default:
|
||||
return theme.font.color.light;
|
||||
}
|
||||
}};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
export const ButtonHotkeys = ({
|
||||
size,
|
||||
accent,
|
||||
variant,
|
||||
hotkeys,
|
||||
}: {
|
||||
size: ButtonSize;
|
||||
accent: ButtonAccent;
|
||||
variant: ButtonVariant;
|
||||
hotkeys: string[];
|
||||
}) => (
|
||||
<>
|
||||
<StyledSeparator buttonSize={size} accent={accent} />
|
||||
<StyledShortcutLabel variant={variant} accent={accent}>
|
||||
{hotkeys.join(getOsShortcutSeparator())}
|
||||
</StyledShortcutLabel>
|
||||
</>
|
||||
);
|
||||
@ -0,0 +1,69 @@
|
||||
import { Loader } from '@ui/feedback';
|
||||
import { baseTransitionTiming } from '@ui/input/button/components/Button/constant';
|
||||
import { IconComponent } from '@ui/display';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
const StyledIcon = styled.div<{
|
||||
loading?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
color: var(--tw-button-color);
|
||||
|
||||
padding: 8px;
|
||||
|
||||
opacity: ${({ loading }) => (loading ? 0 : 1)};
|
||||
transition: opacity ${baseTransitionTiming / 2}ms ease;
|
||||
transition-delay: ${({ loading }) =>
|
||||
loading ? '0ms' : `${baseTransitionTiming / 2}ms`};
|
||||
`;
|
||||
|
||||
const StyledIconWrapper = styled.div<{ loading?: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
width: ${({ loading }) => (loading ? 0 : '100%')};
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div<{ loading?: boolean }>`
|
||||
left: ${({ theme }) => theme.spacing(2)};
|
||||
opacity: ${({ loading }) => (loading ? 1 : 0)};
|
||||
position: absolute;
|
||||
|
||||
transition: opacity ${baseTransitionTiming / 2}ms ease;
|
||||
transition-delay: ${({ loading }) =>
|
||||
loading ? `${baseTransitionTiming / 2}ms` : '0ms'};
|
||||
width: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
export const ButtonIcon = ({
|
||||
Icon,
|
||||
loading,
|
||||
}: {
|
||||
Icon?: IconComponent;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledIconWrapper loading={loading}>
|
||||
{isDefined(loading) && (
|
||||
<StyledLoader loading={loading}>
|
||||
<Loader />
|
||||
</StyledLoader>
|
||||
)}
|
||||
{Icon && (
|
||||
<StyledIcon loading={loading}>
|
||||
<Icon size={theme.icon.size.sm} />
|
||||
</StyledIcon>
|
||||
)}
|
||||
</StyledIconWrapper>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Pill } from '@ui/components';
|
||||
|
||||
const StyledSoonPill = styled(Pill)`
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const ButtonSoon = () => <StyledSoonPill label="Soon" />;
|
||||
@ -0,0 +1,59 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { baseTransitionTiming } from '@ui/input/button/components/Button/constant';
|
||||
|
||||
const StyledEllipsis = styled.div<{ loading?: boolean }>`
|
||||
right: 0;
|
||||
clip-path: ${({ theme, loading }) =>
|
||||
loading ? `inset(0 0 0 0)` : `inset(0 0 0 ${theme.spacing(6)})`};
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
|
||||
transition: clip-path ${baseTransitionTiming}ms ease;
|
||||
`;
|
||||
|
||||
const StyledTextWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledText = styled.div<{ loading?: boolean; hasIcon: boolean }>`
|
||||
clip-path: ${({ loading, theme, hasIcon }) =>
|
||||
loading
|
||||
? ` inset(0 ${!hasIcon ? theme.spacing(12) : theme.spacing(6)} 0 0)`
|
||||
: ' inset(0 0 0 0)'};
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
transform: ${({ theme, loading, hasIcon }) =>
|
||||
loading
|
||||
? `translateX(${!hasIcon ? theme.spacing(7) : theme.spacing(3)})`
|
||||
: 'none'};
|
||||
|
||||
transition:
|
||||
transform ${baseTransitionTiming}ms ease,
|
||||
clip-path ${baseTransitionTiming}ms ease;
|
||||
|
||||
transition-delay: ${({ loading }) =>
|
||||
loading ? '0ms' : `${baseTransitionTiming / 4}ms`};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ButtonText = ({
|
||||
hasIcon = false,
|
||||
loading,
|
||||
title,
|
||||
}: {
|
||||
loading?: boolean;
|
||||
hasIcon: boolean;
|
||||
title?: string;
|
||||
}) => (
|
||||
<StyledTextWrapper>
|
||||
<StyledText loading={loading} hasIcon={hasIcon}>
|
||||
{title}
|
||||
</StyledText>
|
||||
<StyledEllipsis loading={loading}>...</StyledEllipsis>
|
||||
</StyledTextWrapper>
|
||||
);
|
||||
@ -3,7 +3,7 @@ import React, { ReactNode } from 'react';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { ButtonPosition, ButtonProps } from './Button';
|
||||
import { ButtonPosition, ButtonProps } from './Button/Button';
|
||||
|
||||
const StyledButtonGroupContainer = styled.div`
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { IconSearch } from '@ui/display';
|
||||
import { IconReload, IconSearch } from '@ui/display';
|
||||
import {
|
||||
CatalogDecorator,
|
||||
CatalogStory,
|
||||
@ -11,7 +11,7 @@ import {
|
||||
ButtonPosition,
|
||||
ButtonSize,
|
||||
ButtonVariant,
|
||||
} from '../Button';
|
||||
} from '../Button/Button';
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'UI/Input/Button/Button',
|
||||
@ -39,6 +39,7 @@ export const Default: Story = {
|
||||
position: 'standalone',
|
||||
Icon: IconSearch,
|
||||
className: '',
|
||||
loading: false,
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
@ -55,6 +56,7 @@ export const Catalog: CatalogStory<Story, typeof Button> = {
|
||||
soon: { control: false },
|
||||
position: { control: false },
|
||||
className: { control: false },
|
||||
loading: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
|
||||
@ -322,3 +324,35 @@ export const FullWidth: Story = {
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const LoadingButton: Story = {
|
||||
args: {
|
||||
title: 'Reload',
|
||||
Icon: IconReload,
|
||||
loading: true,
|
||||
},
|
||||
argTypes: {
|
||||
size: { control: false },
|
||||
variant: { control: false },
|
||||
accent: { control: false },
|
||||
focus: { control: false },
|
||||
disabled: { control: false },
|
||||
fullWidth: { control: false },
|
||||
soon: { control: false },
|
||||
position: { control: false },
|
||||
className: { control: false },
|
||||
loading: { control: 'boolean' },
|
||||
},
|
||||
parameters: {
|
||||
catalog: {
|
||||
loading: [
|
||||
{
|
||||
name: 'loading',
|
||||
values: [true, false] satisfies boolean[],
|
||||
props: (value: boolean) => ({ loading: value }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
@ -5,7 +5,12 @@ import {
|
||||
CatalogStory,
|
||||
ComponentDecorator,
|
||||
} from '@ui/testing';
|
||||
import { Button, ButtonAccent, ButtonSize, ButtonVariant } from '../Button';
|
||||
import {
|
||||
Button,
|
||||
ButtonAccent,
|
||||
ButtonSize,
|
||||
ButtonVariant,
|
||||
} from '../Button/Button';
|
||||
import { ButtonGroup } from '../ButtonGroup';
|
||||
|
||||
const meta: Meta<typeof ButtonGroup> = {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export * from './button/components/AnimatedButton';
|
||||
export * from './button/components/AnimatedLightIconButton';
|
||||
export * from './button/components/Button';
|
||||
export * from './button/components/Button/Button';
|
||||
export * from './button/components/Button/constant';
|
||||
export * from './button/components/ButtonGroup';
|
||||
export * from './button/components/ColorPickerButton';
|
||||
export * from './button/components/FloatingButton';
|
||||
|
||||
Reference in New Issue
Block a user