← All proposals

Button

Starting point for a Button proposal.

Extends a PDS componentOpen in Storybook

proposals/Button/Button.stories.tsx390 lines · View on GitHub
import React from 'react';

import { Meta, StoryObj } from '@storybook/react-vite';

import { Button } from './Button';

const meta: Meta<typeof Button> = {
	title: 'Proposals/Buttons/Button',
	component: Button,
	parameters: {
		docs: {
			description: {
				component: 'A Component used to render a Button',
			},
		},
	},
	argTypes: {
		label: {
			control: 'text',
			table: {
				category: 'Content',
			},
		},
		iconName: {
			table: {
				category: 'Content',
			},
		},
		tooltipText: {
			table: {
				category: 'Content',
			},
		},
		size: {
			control: 'select',
			table: {
				category: 'Component configuration',
			},
		},
		variant: {
			control: 'select',
			options: [
				'primary',
				'secondary',
				'subtle',
				'brand',
				'highlight',
				'critical',
				'critical-secondary',
				'link',
				'reverse',
				'reverse-secondary',
			],
			table: {
				category: 'Component configuration',
			},
		},
		displayType: {
			control: 'select',
			table: {
				category: 'Component configuration',
			},
		},
		isAgentic: {
			control: 'boolean',
			table: {
				category: 'Component configuration',
			},
		},
		isFullWidth: {
			control: 'boolean',
			table: {
				category: 'Component configuration',
			},
		},
		isLoading: {
			control: 'boolean',
			table: {
				category: 'Status',
			},
		},
		isWorking: {
			control: 'boolean',
			table: {
				category: 'Status',
			},
		},
		disableWhenWorking: {
			control: 'boolean',
			table: {
				category: 'Status',
			},
		},
		onClick: {
			table: { category: 'Functions' },
		},
		ariaLabel: {
			control: 'select',
			table: {
				category: 'Base attributes',
			},
		},
		buttonType: {
			control: 'select',
			table: {
				category: 'Base attributes',
			},
		},
		disabled: {
			table: {
				category: 'Base attributes',
			},
		},
		className: {
			table: {
				category: 'Base attributes',
			},
		},
		isSplitButton: {
			table: {
				disable: true,
			},
		},
		tally: {
			table: {
				category: 'Content',
			},
		},
		statusDot: {
			control: 'select',
			table: {
				category: 'Content',
			},
		},
	},
	args: {
		label: 'Button',
		variant: 'primary',
		size: 's',
		displayType: 'label-only',
		buttonType: 'button',
		disabled: false,
		isAgentic: true,
		isLoading: false,
		isWorking: false,
		disableWhenWorking: true,
	},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
	args: {},
};

export const WithIcon: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'Buttons can display an icon before or after the label using `displayType` (`icon-start`, `icon-end`, `icon-only`). Use icons to reinforce the action, not to replace a clear label.',
			},
		},
	},
	args: {
		displayType: 'icon-start',
		iconName: 'zappy',
	},
};

export const WithTally: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'A `Tally` badge can be attached to a button to surface a count (e.g. pending items, notifications). Pass `tally.type` to choose the status color.',
			},
		},
	},
	args: {
		tally: { label: 4, type: 'neutral' },
	},
};

export const WithStatusDot: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'A `StatusDot` can be overlaid on the button to communicate real-time status (e.g. a connection that is online or degraded). Use when the button itself is the primary way users identify the resource.',
			},
		},
	},
	args: {
		statusDot: 'discovery',
	},
};

export const LoadingState: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'`isLoading` replaces the label with a spinner to indicate that data is being fetched before the action is available. Pair with `tooltipText` to explain the wait to keyboard and screen-reader users.',
			},
		},
	},
	args: {
		isLoading: true,
		tooltipText: 'Loading...',
	},
};

export const WorkingState: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'`isWorking` shows a spinner alongside the label to indicate an in-progress operation triggered by a prior click. Unlike `isLoading`, the label remains visible so the user knows what action is running.',
			},
		},
	},
	args: {
		isWorking: true,
		tooltipText: 'This button is waiting for an action to complete',
	},
};

export const WorkingStateInteractive: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'Set `disableWhenWorking` to `false` for an ongoing, interactive working state — such as a toggle that starts and stops a live process — where the button must stay clickable (e.g. to stop it) while `isWorking` is true.',
			},
		},
	},
	args: {
		displayType: 'icon-end',
		iconName: 'pause',
		isWorking: true,
		disableWhenWorking: false,
		label: 'Investigating',
	},
};

export const AllVariants: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'Quick reference for all button variants. Reverse variants require a dark or brand-colored background and are shown here over `--pds-color-surface-reverse`. Use the `isAgentic` control to toggle the agentic treatment; it applies to `primary` and `secondary` only.',
			},
		},
	},
	render: ({ isAgentic }) => {
		const wrapStyle = {
			display: 'flex',
			flexWrap: 'wrap' as const,
			gap: 'var(--pds-spacing-m)',
			padding: 'var(--pds-spacing-m)',
		};

		return (
			<>
				<div style={wrapStyle}>
					<Button
						isAgentic={isAgentic}
						label='Primary'
						onClick={() => {}}
						variant='primary'
					/>
					<Button
						isAgentic={isAgentic}
						label='Secondary'
						onClick={() => {}}
						variant='secondary'
					/>
					<Button label='Subtle' onClick={() => {}} variant='subtle' />
					<Button label='Brand' onClick={() => {}} variant='brand' />
					<Button label='Highlight' onClick={() => {}} variant='highlight' />
					<Button label='Critical' onClick={() => {}} variant='critical' />
					<Button
						label='Critical Secondary'
						onClick={() => {}}
						variant='critical-secondary'
					/>
					<Button label='Link' onClick={() => {}} variant='link' />
				</div>
				<div
					style={{
						...wrapStyle,
						backgroundColor: 'var(--pds-color-surface-reverse)',
					}}
				>
					<Button label='Reverse' onClick={() => {}} variant='reverse' />
					<Button
						label='Reverse Secondary'
						onClick={() => {}}
						variant='reverse-secondary'
					/>
				</div>
			</>
		);
	},
};

export const AgenticStates: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'`isAgentic` adds the AI gradient border and the gradient working spinner. It is supported on `primary` and `secondary`. Columns: default, disabled, working (leading icon), working (spinner).',
			},
		},
	},
	render: () => {
		const variants = ['primary', 'secondary'] as const;

		return (
			<div
				style={{
					display: 'grid',
					gap: 'var(--pds-spacing-m)',
					gridTemplateColumns: 'repeat(4, max-content)',
					padding: 'var(--pds-spacing-m)',
				}}
			>
				{variants.map((variant) => (
					<React.Fragment key={variant}>
						<Button isAgentic label='Button' variant={variant} />
						<Button disabled isAgentic label='Button' variant={variant} />
						<Button
							displayType='icon-start'
							iconName='zappy'
							isAgentic
							label='Button'
							variant={variant}
						/>
						<Button isAgentic isWorking label='Button' variant={variant} />
					</React.Fragment>
				))}
			</div>
		);
	},
};

export const AllSizes: Story = {
	parameters: {
		docs: {
			description: {
				story:
					'Quick reference for all button sizes, smallest to largest. Use the `isAgentic` control to toggle the agentic treatment.',
			},
		},
	},
	render: ({ isAgentic }) => {
		const sizes = [
			{ label: 'Extra small button', size: 'xs' },
			{ label: 'Small button', size: 's' },
			{ label: 'Medium button', size: 'm' },
			{ label: 'Large button', size: 'l' },
		] as const;

		return (
			<div
				style={{
					alignItems: 'center',
					display: 'flex',
					flexWrap: 'wrap',
					gap: 'var(--pds-spacing-m)',
					padding: 'var(--pds-spacing-m)',
				}}
			>
				{sizes.map(({ label, size }) => (
					<Button
						isAgentic={isAgentic}
						key={size}
						label={label}
						onClick={() => {}}
						size={size}
					/>
				))}
			</div>
		);
	},
};