import { ComponentPropsWithoutRef, useRef } from 'react';
import type { Placement } from '@floating-ui/react';
import {
ButtonLink,
LinkItemType,
MenuItemType,
PDSIcon,
} from '@pantheon-systems/pds-toolkit-react';
import { Button } from '../_base/Button/Button';
import { mergeClasses } from '../_base/Button/utils';
import { MenuButton } from '../_base/MenuButton/MenuButton';
import './agentic-split-button.css';
type SplitButtonSize = 's' | 'm';
/**
* Prop types for AgenticSplitButton
*/
export interface AgenticSplitButtonProps extends ComponentPropsWithoutRef<'span'> {
/**
* Array of menu items
*/
actionItems: Array<LinkItemType | MenuItemType>;
/**
* Anchor the actions menu panel to the whole button (primary action +
* caret combined) instead of just the caret trigger. Defaults to
* `false` — the panel positions relative to the caret alone, matching
* current behavior. Set this when `placement` needs to align the panel
* with an edge of the whole control (e.g. `bottom-start` under the
* primary action's left edge, for a table row action) rather than just
* the caret's narrow width.
*/
anchorToFullButton?: boolean;
/**
* Additional class names
*/
className?: string;
/**
* Is the button disabled?
*/
disabled?: boolean;
/**
* Icon shown at the start of the primary action, e.g. `'plus'`. Only
* applies to the primary action — there's no equivalent for menu items.
*/
iconName?: PDSIcon;
/**
* ID value for this component.
*/
id: string;
/**
* Shimmers the gradient border. Off for reduced motion.
*/
isAnimated?: boolean;
/**
* Label for the buttons additional actions menu.
*/
moreActionsLabel?: string;
/**
* Preferred placement of the actions menu panel. Defaults to
* `bottom-start`, matching `MenuButton`'s own default. Aligns relative
* to the caret trigger alone unless `anchorToFullButton` is set, in
* which case it aligns relative to the whole control instead. Passed
* straight through to the underlying `MenuButton`.
*/
placement?: Placement;
/**
* Which size of button to render
*/
size?: SplitButtonSize;
}
/**
* A split button for actions the agent does: a primary action with a menu of
* additional options, with the AI gradient as the border around the whole
* control. The secondary look (there is no primary version).
*/
export const AgenticSplitButton = ({
actionItems,
anchorToFullButton = false,
className,
disabled,
iconName,
id,
isAnimated = false,
moreActionsLabel = 'More actions',
placement = 'bottom-start',
size = 's',
...props
}: AgenticSplitButtonProps) => {
// Set up classes.
const baseClass = 'pds-split-button';
const variantClass = `${baseClass}--secondary`;
const agenticClass = 'pds-agentic-split-button';
const animatedClass = isAnimated ? `${agenticClass}--animated` : null;
const rootRef = useRef<HTMLSpanElement>(null);
// Separate the primary action from the rest.
const primaryAction = actionItems[0];
const moreActions = actionItems.slice(1);
// Preprocess the first item based on its type.
let primaryActionElement = null;
if ('isLink' in primaryAction && primaryAction.isLink === true) {
primaryActionElement = (
<ButtonLink
className={`${baseClass}__primary-action`}
disabled={disabled}
displayType={iconName ? 'icon-start' : 'label-only'}
iconName={iconName}
linkContent={primaryAction.linkContent}
size={size}
variant='secondary'
/>
);
} else if ('label' in primaryAction) {
primaryActionElement = (
<Button
className={`${baseClass}__primary-action`}
disabled={disabled}
displayType={iconName ? 'icon-start' : 'label-only'}
iconName={iconName}
label={primaryAction.label}
onClick={() => primaryAction.callback?.()}
size={size}
variant='secondary'
/>
);
}
// Render the output
return (
<span
ref={rootRef}
className={mergeClasses([
baseClass,
variantClass,
agenticClass,
animatedClass,
className,
])}
id={id}
{...props}
>
{primaryActionElement}
<MenuButton
isSplitButton
className={`${baseClass}__menu-button`}
disabled={disabled}
id={`${id}-menu`}
label={moreActionsLabel}
menuItems={moreActions}
placement={placement}
positionReferenceRef={anchorToFullButton ? rootRef : undefined}
size={size}
variant='secondary'
/>
</span>
);
};
Agentic Split Button
A separate component, `AgenticSplitButton`, a copy of the PDS v2 Split Button, so the engineer's port is mostly a copy:
Net-new componentOpen in Storybook