← All proposals

Send Button

A square, filled icon button (primary Button colors, IconButton shape and sizes) with three states:

Net-new componentOpen in Storybook

proposals/SendButton/SendButton.stories.tsx143 lines · View on GitHub
import { useState } from 'react';

import { FlexContainer } from '@pantheon-systems/pds-toolkit-react';
import { Meta, StoryObj } from '@storybook/react-vite';

import { SendButton, SendButtonState } from './SendButton';

const SIZES = ['xs', 's', 'm'] as const;

const STATES: {
	ariaLabel: string;
	name: string;
	state: SendButtonState;
}[] = [
	{ ariaLabel: 'Send message', name: 'Send', state: 'send' },
	{ ariaLabel: 'Sending message', name: 'Working', state: 'working' },
	{ ariaLabel: 'Stop generating', name: 'Stop', state: 'stop' },
];

const meta: Meta<typeof SendButton> = {
	tags: ['new-pattern'],
	title: 'Proposals/Buttons/Send Button',
	component: SendButton,
	parameters: {
		docs: {
			description: {
				component:
					'**New pattern:** not in PDS v2 today. Submits a prompt from a chat input. Same shape and sizes as `IconButton`, filled like a primary `Button`, with three states: send, working and stop.',
			},
		},
	},
	argTypes: {
		ariaLabel: { control: 'text', table: { category: 'Content' } },
		buttonType: { control: 'select', table: { category: 'Base attributes' } },
		className: { table: { category: 'Base attributes' } },
		disabled: { control: 'boolean', table: { category: 'Base attributes' } },
		hasTooltip: {
			control: 'boolean',
			table: { category: 'Component configuration' },
		},
		onClick: { table: { category: 'Functions' } },
		size: {
			control: 'select',
			options: SIZES,
			table: { category: 'Component configuration' },
		},
		spinnerColor: {
			control: 'select',
			mapping: {
				agentic: 'agentic',
				currentColor: 'currentColor',
				discovery: 'var(--pds-color-status-discovery-fill)',
			},
			options: ['currentColor', 'agentic', 'discovery'],
			table: { category: 'Spinner' },
		},
		state: {
			control: 'select',
			options: ['send', 'working', 'stop'],
			table: { category: 'State' },
		},
	},
	args: {
		ariaLabel: 'Send message',
		buttonType: 'button',
		disabled: false,
		hasTooltip: true,
		onClick: () => {
			console.log('Send button clicked');
		},
		size: 'm',
		state: 'send',
	},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const AllStates: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'The three states at each size: `send`, `working` (spinner, disabled) and `stop`.',
			},
		},
	},
	render: (args) => (
		<FlexContainer flexDirection='column' gap='m'>
			{STATES.map(({ ariaLabel, name, state }) => (
				<FlexContainer alignItems='center' gap='m' key={name}>
					<span style={{ width: '8rem' }}>{name}</span>
					{SIZES.map((size) => (
						<SendButton
							{...args}
							ariaLabel={ariaLabel}
							key={size}
							size={size}
							state={state}
						/>
					))}
				</FlexContainer>
			))}
		</FlexContainer>
	),
};

export const Interactive: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'How the three states chain in a chat input: click to send, it shows `working` until the request starts, then `stop` while the response streams. Clicking stop returns to `send`.',
			},
		},
	},
	render: (args) => {
		const [state, setState] = useState<SendButtonState>('send');
		const current = STATES.find((s) => s.state === state);

		const handleClick = () => {
			if (state === 'send') {
				setState('working');
				setTimeout(() => setState('stop'), 400);
			} else if (state === 'stop') {
				setState('send');
			}
		};

		return (
			<SendButton
				{...args}
				ariaLabel={current?.ariaLabel ?? ''}
				onClick={handleClick}
				state={state}
			/>
		);
	},
};