Files
web-ui/src/components/Dropdown.tsx
Beatrice Dellacà 29a4e8c2ee
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
add width, v0.1.10
2026-02-23 19:57:19 +01:00

98 lines
3.0 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;
width?: 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',
width = 'md',
layout = 'stacked',
disabled = false,
required = false,
onChange,
error,
className = '',
selectClassName = '',
}: Readonly<DropdownProps>) {
const containerWidthClass = {
sm: 'max-w-xs',
md: 'max-w-sm',
lg: 'max-w-md',
full: 'max-w-none',
}[width];
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'} ${containerWidthClass} ${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>
);
}