import { ComponentPropsWithoutRef, forwardRef, MouseEvent, Ref } from 'react';
import {
ButtonType,
Icon,
IconSize,
Tooltip,
} from '@pantheon-systems/pds-toolkit-react';
import { Spinner, SpinnerProps, SpinnerSize } from '../Spinner/Spinner';
import './send-button.css';
export type SendButtonSize = 'xs' | 's' | 'm';
export type SendButtonState = 'send' | 'stop' | 'working';
// PDS has no filled-square "stop" icon yet, so it is drawn here with the same
// markup and size classes as `Icon`. The path is Font Awesome Free's solid
// `stop`, so the port adds it as an FA registry entry (`faStop`, style
// 'solid') and uses `<Icon iconName='stop' />`.
const StopIcon = ({
className,
size,
}: {
className: string;
size: IconSize;
}) => (
<svg
aria-hidden='true'
className={`pds-icon pds-icon--${size} pds-icon--stop ${className}`}
fill='none'
focusable='false'
role='img'
viewBox='0 0 640 640'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z'
fill='currentColor'
/>
</svg>
);
interface SendButtonProps extends ComponentPropsWithoutRef<'button'> {
/**
* Aria-label attribute. Describes what the button does in its current
* state (for example "Send message" or "Stop generating"), so update it
* when `state` changes. Also the tooltip text.
*/
ariaLabel: string;
/**
* Type of button
*/
buttonType?: ButtonType;
/**
* Additional class names.
*/
className?: string;
/**
* Is the button disabled? Use when there is nothing to send yet, such as an
* empty prompt. Always disabled while `state` is `working`.
*/
disabled?: boolean;
/**
* Show tooltip on hover. If false, the title attribute will be used.
*/
hasTooltip?: boolean;
/**
* Click event handler callback.
*/
onClick?: (e: MouseEvent<HTMLElement>) => void;
/**
* Which size of button to render.
*/
size?: SendButtonSize;
/**
* Color of the working spinner. Any CSS color or `var(--token)`, or
* `'agentic'` for the AI gradient. Defaults to the button's foreground
* color (`currentColor`).
*/
spinnerColor?: SpinnerProps['color'];
/**
* What the button is doing. `send` submits the prompt, `working` shows a
* spinner while the request is in flight (disabled), and `stop` lets
* the user cancel a running response.
*/
state?: SendButtonState;
}
/**
* SendButton UI component
*/
export const SendButton = forwardRef(
(
{
ariaLabel,
buttonType = 'button',
className,
disabled = false,
hasTooltip = true,
onClick,
size = 's',
spinnerColor = 'currentColor',
state = 'send',
...props
}: SendButtonProps,
ref: Ref<HTMLButtonElement>,
) => {
const baseClass = 'pds-send-button';
const classes = [
baseClass,
size !== 's' ? `${baseClass}--${size}` : null,
`${baseClass}--${state}`,
className,
]
.filter(Boolean)
.join(' ');
// Same icon sizes as IconButton; the spinner is matched to the icon.
let setIconSize: IconSize;
let setSpinnerSize: SpinnerSize;
let setOffsetValue: number;
switch (size) {
case 'xs':
setIconSize = 'm';
setSpinnerSize = 's';
setOffsetValue = 6;
break;
case 'm':
setIconSize = 'xl';
setSpinnerSize = 'l';
setOffsetValue = 11;
break;
default:
setIconSize = 'm';
setSpinnerSize = 'm';
setOffsetValue = 8;
break;
}
const isWorking = state === 'working';
const isDisabled = disabled || isWorking;
const content = (
<div className={`${baseClass}__content`}>
{isWorking ? (
<Spinner
className={`${baseClass}__spinner`}
color={spinnerColor}
size={setSpinnerSize}
/>
) : (
<>
{state === 'stop' ? (
<StopIcon className={`${baseClass}__icon`} size={setIconSize} />
) : (
<Icon
className={`${baseClass}__icon`}
iconName='paperPlane'
size={setIconSize}
/>
)}
</>
)}
</div>
);
return (
<button
ref={ref}
aria-busy={isWorking || undefined}
aria-label={ariaLabel}
className={classes}
disabled={isDisabled}
onClick={onClick}
title={hasTooltip && !isDisabled ? undefined : ariaLabel}
type={buttonType}
{...props}
>
{hasTooltip && !isDisabled ? (
<Tooltip
disableTapTrigger
className={`${baseClass}__tooltip`}
content={ariaLabel}
customTrigger={content}
offsetValue={setOffsetValue}
/>
) : (
content
)}
</button>
);
},
);
Send Button
A square, filled icon button (primary Button colors, IconButton shape and sizes) with three states:
Net-new componentOpen in Storybook