← All proposals

Model Selector

A new component built on the local MenuButton (trigger, popover, keyboard and focus behavior come from it).

Net-new componentOpen in Storybook

proposals/ModelSelector/ModelSelector.tsx112 lines · View on GitHub
import { ComponentPropsWithoutRef } from 'react';

import { MenuItemType, PDSIcon } from '@pantheon-systems/pds-toolkit-react';

import { MenuButton } from '../MenuButton/MenuButton';

import './model-selector.css';

export type ModelSelectorSize = 'xs' | 's' | 'm';

export interface ModelSelectorOption {
	/**
	 * Short description shown under the name in the menu.
	 */
	description?: string;
	/**
	 * Is this option unavailable?
	 */
	disabled?: boolean;
	/**
	 * Optional icon shown before the name in the menu.
	 */
	iconName?: PDSIcon;
	/**
	 * Unique value for this option.
	 */
	id: string;
	/**
	 * Name of the model.
	 */
	label: string;
}

/**
 * Prop types for ModelSelector
 */
export interface ModelSelectorProps extends Omit<
	ComponentPropsWithoutRef<'span'>,
	'onClick'
> {
	/**
	 * Additional class names.
	 */
	className?: string;
	/**
	 * Is the selector disabled?
	 */
	disabled?: boolean;
	/**
	 * ID value for the trigger button.
	 */
	id: string;
	/**
	 * The models to choose from.
	 */
	options: ModelSelectorOption[];
	/**
	 * Called with the option `id` when a different model is picked.
	 */
	onValueChange?: (value: string) => void;
	/**
	 * Which size of trigger to render.
	 */
	size?: ModelSelectorSize;
	/**
	 * The `id` of the selected option. The trigger shows its name.
	 */
	value: string;
}

/**
 * ModelSelector UI component
 */
export const ModelSelector = ({
	className,
	disabled = false,
	id,
	onValueChange,
	options,
	size = 's',
	value,
	...props
}: ModelSelectorProps) => {
	const baseClass = 'pds-model-selector';
	const classes = [baseClass, className].filter(Boolean).join(' ');

	const selected = options.find((option) => option.id === value);

	const menuItems: MenuItemType[] = options.map((option) => ({
		callback: () => onValueChange?.(option.id),
		description: option.description,
		disabled: option.disabled,
		iconName: option.iconName,
		id: `${id}-option-${option.id}`,
		isSelected: option.id === value,
		label: option.label,
	}));

	return (
		<MenuButton
			className={classes}
			disabled={disabled}
			id={id}
			label={selected?.label}
			menuItems={menuItems}
			size={size}
			variant='subtle'
			{...props}
		/>
	);
};