mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 10:40:50 +08:00
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
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: ReadonlyArray<SelectOption>;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
ariaLabel?: string;
|
|
fullWidth?: boolean;
|
|
}
|
|
|
|
export function Select({
|
|
value,
|
|
options,
|
|
onChange,
|
|
placeholder,
|
|
className,
|
|
disabled = false,
|
|
ariaLabel,
|
|
fullWidth = true
|
|
}: SelectProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const wrapRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open || disabled) return;
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (!wrapRef.current?.contains(event.target as Node)) setOpen(false);
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, [disabled, open]);
|
|
|
|
const isOpen = open && !disabled;
|
|
|
|
const selected = options.find((o) => o.value === value);
|
|
const displayText = selected?.label ?? placeholder ?? '';
|
|
const isPlaceholder = !selected && placeholder;
|
|
|
|
return (
|
|
<div
|
|
className={`${styles.wrap} ${fullWidth ? styles.wrapFullWidth : ''} ${className ?? ''}`}
|
|
ref={wrapRef}
|
|
>
|
|
<button
|
|
type="button"
|
|
className={styles.trigger}
|
|
onClick={disabled ? undefined : () => setOpen((prev) => !prev)}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={isOpen}
|
|
aria-label={ariaLabel}
|
|
disabled={disabled}
|
|
>
|
|
<span className={`${styles.triggerText} ${isPlaceholder ? styles.placeholder : ''}`}>
|
|
{displayText}
|
|
</span>
|
|
<span className={styles.triggerIcon} aria-hidden="true">
|
|
<IconChevronDown size={14} />
|
|
</span>
|
|
</button>
|
|
{isOpen && (
|
|
<div className={styles.dropdown} role="listbox" aria-label={ariaLabel}>
|
|
{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>
|
|
);
|
|
}
|