feat: More obvious loading state when submitting a post (#35153)

This commit is contained in:
diondiondion 2025-06-24 16:08:48 +02:00 committed by GitHub
commit 644da36336
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 120 additions and 28 deletions

View file

@ -11,6 +11,7 @@ const meta = {
compact: false,
dangerous: false,
disabled: false,
loading: false,
onClick: fn(),
},
argTypes: {
@ -41,16 +42,6 @@ const buttonTest: Story['play'] = async ({ args, canvas, userEvent }) => {
await expect(args.onClick).toHaveBeenCalled();
};
const disabledButtonTest: Story['play'] = async ({
args,
canvas,
userEvent,
}) => {
const button = await canvas.findByRole('button');
await userEvent.click(button);
await expect(args.onClick).not.toHaveBeenCalled();
};
export const Primary: Story = {
args: {
children: 'Primary button',
@ -82,6 +73,18 @@ export const Dangerous: Story = {
play: buttonTest,
};
const disabledButtonTest: Story['play'] = async ({
args,
canvas,
userEvent,
}) => {
const button = await canvas.findByRole('button');
await userEvent.click(button);
// Disabled controls can't be focused
await expect(button).not.toHaveFocus();
await expect(args.onClick).not.toHaveBeenCalled();
};
export const PrimaryDisabled: Story = {
args: {
...Primary.args,
@ -97,3 +100,24 @@ export const SecondaryDisabled: Story = {
},
play: disabledButtonTest,
};
const loadingButtonTest: Story['play'] = async ({
args,
canvas,
userEvent,
}) => {
const button = await canvas.findByRole('button', {
name: 'Primary button Loading…',
});
await userEvent.click(button);
await expect(button).toHaveFocus();
await expect(args.onClick).not.toHaveBeenCalled();
};
export const Loading: Story = {
args: {
...Primary.args,
loading: true,
},
play: loadingButtonTest,
};

View file

@ -3,12 +3,15 @@ import { useCallback } from 'react';
import classNames from 'classnames';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
compact?: boolean;
dangerous?: boolean;
loading?: boolean;
}
interface PropsChildren extends PropsWithChildren<BaseProps> {
@ -34,6 +37,7 @@ export const Button: React.FC<Props> = ({
secondary,
compact,
dangerous,
loading,
className,
title,
text,
@ -42,13 +46,18 @@ export const Button: React.FC<Props> = ({
}) => {
const handleClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(e) => {
if (!disabled && onClick) {
if (disabled || loading) {
e.stopPropagation();
e.preventDefault();
} else if (onClick) {
onClick(e);
}
},
[disabled, onClick],
[disabled, loading, onClick],
);
const label = text ?? children;
return (
<button
className={classNames('button', className, {
@ -56,14 +65,27 @@ export const Button: React.FC<Props> = ({
'button--compact': compact,
'button--block': block,
'button--dangerous': dangerous,
loading,
})}
disabled={disabled}
// Disabled buttons can't have focus, so we don't really
// disable the button during loading
disabled={disabled && !loading}
aria-disabled={loading}
// If the loading prop is used, announce label changes
aria-live={loading !== undefined ? 'polite' : undefined}
onClick={handleClick}
title={title}
type={type}
{...props}
>
{text ?? children}
{loading ? (
<>
<span className='button__label-wrapper'>{label}</span>
<LoadingIndicator role='none' />
</>
) : (
label
)}
</button>
);
};