89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
import type { ChangeEventHandler } from 'react';
|
|
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
|
import type { ComponentSize } from './types';
|
|
|
|
type DropdownLayout = 'stacked' | 'inline';
|
|
|
|
type DropdownChoice = {
|
|
label: string;
|
|
id: string;
|
|
};
|
|
|
|
type DropdownProps = {
|
|
label?: string;
|
|
value: string;
|
|
choices: DropdownChoice[];
|
|
size?: ComponentSize;
|
|
layout?: DropdownLayout;
|
|
disabled?: boolean;
|
|
required?: boolean;
|
|
onChange?: (value: string) => void;
|
|
error?: string;
|
|
className?: string;
|
|
selectClassName?: string;
|
|
};
|
|
|
|
export function Dropdown({
|
|
label,
|
|
value,
|
|
choices,
|
|
size = 'md',
|
|
layout = 'stacked',
|
|
disabled = false,
|
|
required = false,
|
|
onChange,
|
|
error,
|
|
className = '',
|
|
selectClassName = ''
|
|
}: Readonly<DropdownProps>) {
|
|
const containerSizeClass = {
|
|
sm: 'max-w-xs',
|
|
md: 'max-w-sm',
|
|
lg: 'max-w-md',
|
|
full: 'max-w-none'
|
|
}[size];
|
|
|
|
const selectSizeClass = {
|
|
sm: 'h-8 !text-xs',
|
|
md: 'h-10 text-sm',
|
|
lg: 'h-12 text-sm',
|
|
full: 'h-10 text-sm'
|
|
}[size];
|
|
|
|
const handleChange: ChangeEventHandler<HTMLSelectElement> = (event) => {
|
|
onChange?.(event.target.value);
|
|
};
|
|
|
|
const wrapperClass = layout === 'inline'
|
|
? 'inline-flex w-auto items-center gap-2'
|
|
: 'block w-full gap-1';
|
|
|
|
const selectWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1';
|
|
const labelClass = layout === 'inline' ? 'text-xs ui-body-secondary' : '';
|
|
|
|
return (
|
|
<label className={`${wrapperClass} text-sm font-medium ${disabled ? 'ui-label-disabled' : 'ui-label'} ${containerSizeClass} ${className}`.trim()}>
|
|
{label ? <span className={labelClass}>{label}</span> : null}
|
|
<div className={selectWrapperClass}>
|
|
<select
|
|
value={value}
|
|
onChange={handleChange}
|
|
disabled={disabled}
|
|
required={required}
|
|
className={`field w-full appearance-none pr-9 disabled:opacity-100 ${selectSizeClass} ${error ? 'border-red-400/70 focus:border-red-400 focus:ring-red-400/30' : ''} ${selectClassName}`.trim()}
|
|
>
|
|
{choices.map((choice) => (
|
|
<option key={choice.id} value={choice.id}>
|
|
{choice.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className={`pointer-events-none absolute inset-y-0 right-3 flex items-center ${disabled ? 'ui-label-disabled' : 'ui-body-secondary'}`}>
|
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
|
</span>
|
|
</div>
|
|
{error ? <span className="mt-1 block text-xs" style={{ color: 'var(--error-text)' }}>{error}</span> : null}
|
|
</label>
|
|
);
|
|
}
|