feat(select): implement custom Select component with dropdown functionality

This commit is contained in:
Supra4E8C
2026-02-14 12:01:11 +08:00
parent bf824f8561
commit 5dce24e3ea
6 changed files with 214 additions and 42 deletions

View File

@@ -0,0 +1,75 @@
import { useState, useEffect, useRef } from 'react';
import { IconChevronDown } from './icons';
import styles from './Select.module.scss';
export interface SelectOption {
value: string;
label: string;
}
interface SelectProps {
value: string;
options: SelectOption[];
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}
export function Select({ value, options, onChange, placeholder, className }: SelectProps) {
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!open) return;
const handleClickOutside = (event: MouseEvent) => {
if (!wrapRef.current?.contains(event.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);
const selected = options.find((o) => o.value === value);
const displayText = selected?.label ?? placeholder ?? '';
const isPlaceholder = !selected && placeholder;
return (
<div className={`${styles.wrap} ${className ?? ''}`} ref={wrapRef}>
<button
type="button"
className={styles.trigger}
onClick={() => setOpen((prev) => !prev)}
aria-haspopup="listbox"
aria-expanded={open}
>
<span className={`${styles.triggerText} ${isPlaceholder ? styles.placeholder : ''}`}>
{displayText}
</span>
<span className={styles.triggerIcon} aria-hidden="true">
<IconChevronDown size={14} />
</span>
</button>
{open && (
<div className={styles.dropdown} role="listbox">
{options.map((opt) => {
const active = opt.value === value;
return (
<button
key={opt.value}
type="button"
role="option"
aria-selected={active}
className={`${styles.option} ${active ? styles.optionActive : ''}`}
onClick={() => {
onChange(opt.value);
setOpen(false);
}}
>
{opt.label}
</button>
);
})}
</div>
)}
</div>
);
}