feat: enhance dropdown components by integrating useClickOutside hook for improved user experience and accessibility in NewWorkshopDropdown and WorkshopTabs

This commit is contained in:
Julien Froidefond
2026-02-18 08:25:08 +01:00
parent d50a8a0266
commit ee13f8ba99
9 changed files with 189 additions and 197 deletions

View File

@@ -1,24 +1,63 @@
import { forwardRef, SelectHTMLAttributes } from 'react';
interface SelectOption {
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
const SIZE_STYLES = {
xs: 'px-2 py-1 pr-7 text-xs',
sm: 'px-2 py-2 pr-8 text-sm',
md: 'px-4 py-2.5 pr-10 text-sm',
lg: 'px-4 py-2.5 pr-10 text-base',
} as const;
const ICON_SIZES = {
xs: 'h-3 w-3',
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-5 w-5',
} as const;
const ICON_POSITION = {
xs: 'right-2',
sm: 'right-2',
md: 'right-3',
lg: 'right-3',
} as const;
interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children' | 'size'> {
label?: string;
error?: string;
options: SelectOption[];
placeholder?: string;
size?: keyof typeof SIZE_STYLES;
wrapperClassName?: string;
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ className = '', label, error, id, options, placeholder, ...props }, ref) => {
(
{
className = '',
label,
error,
id,
options,
placeholder,
size = 'md',
wrapperClassName = '',
...props
},
ref
) => {
const selectId = id || props.name;
const sizeStyles = SIZE_STYLES[size];
const iconSize = ICON_SIZES[size];
const iconPosition = ICON_POSITION[size];
return (
<div className="w-full">
<div className={wrapperClassName || 'w-full'}>
{label && (
<label htmlFor={selectId} className="mb-2 block text-sm font-medium text-foreground">
{label}
@@ -29,11 +68,13 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
ref={ref}
id={selectId}
className={`
w-full appearance-none rounded-lg border bg-input px-4 py-2.5 pr-10 text-foreground
w-full appearance-none rounded-lg border bg-input text-foreground
placeholder:text-muted-foreground
focus:outline-none focus:ring-2 focus:ring-primary/20
disabled:cursor-not-allowed disabled:opacity-50
${error ? 'border-destructive focus:border-destructive' : 'border-input-border focus:border-primary'}
border-input-border focus:border-primary
${sizeStyles}
${error ? 'border-destructive focus:border-destructive' : ''}
${className}
`}
{...props}
@@ -49,14 +90,8 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
</option>
))}
</select>
{/* Custom arrow icon */}
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
<svg
className="h-5 w-5 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<div className={`pointer-events-none absolute ${iconPosition} top-1/2 -translate-y-1/2 text-muted-foreground`}>
<svg className={iconSize} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
@@ -68,4 +103,3 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
);
Select.displayName = 'Select';

View File

@@ -14,6 +14,7 @@ export { ParticipantInput } from './ParticipantInput';
export { Modal, ModalFooter } from './Modal';
export { RocketIcon } from './RocketIcon';
export { Select } from './Select';
export type { SelectOption } from './Select';
export { Textarea } from './Textarea';
export { ToggleGroup } from './ToggleGroup';
export type { ToggleOption } from './ToggleGroup';