import { useEffect, useId, useRef, useState } from 'react'; import { ChevronDown, Filter, Search, X } from 'lucide-react'; import './dimension-picker.css'; export interface DimensionPickerOption { value: string; label: string; count?: number } export interface DimensionPickerProps { label: string; grouped: boolean; order?: number; options: DimensionPickerOption[]; selectedValues?: string[]; onToggleGroup: () => void; onApplyFilter: (values: string[] | undefined) => void; } /** A grouping switch and an independently applied, searchable multi-value filter. */ export function DimensionPicker({ label, grouped, order, options, selectedValues, onToggleGroup, onApplyFilter }: DimensionPickerProps) { const [open, setOpen] = useState(false); const [draft, setDraft] = useState([]); const [query, setQuery] = useState(''); const [position, setPosition] = useState({ left: 8, top: 8, maxHeight: 420 }); const root = useRef(null); const trigger = useRef(null); const search = useRef(null); const menuId = useId(); const selected = selectedValues ?? []; const labels = new Map(options.map(option => [option.value, option.label])); const selectedLabel = selected.map(value => labels.get(value) || value || '未标注').join('、'); const visible = options.filter(option => option.label.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())); const close = (restoreFocus = true) => { setOpen(false); if (restoreFocus) trigger.current?.focus(); }; const show = () => { if (open) { close(); return; } setDraft([...selected]); setQuery(''); setOpen(true); }; useEffect(() => { if (!open) return; const place = () => { const rect = trigger.current?.getBoundingClientRect(); if (!rect) return; const width = Math.min(320, window.innerWidth - 16); const roomBelow = window.innerHeight - rect.bottom - 12; const useAbove = roomBelow < 220 && rect.top > roomBelow; const height = Math.min(420, Math.max(160, useAbove ? rect.top - 12 : roomBelow)); setPosition({ left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)), top: useAbove ? Math.max(8, rect.top - height - 5) : rect.bottom + 5, maxHeight: height }); }; place(); const frame = requestAnimationFrame(() => search.current?.focus()); const outside = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) setOpen(false); }; document.addEventListener('pointerdown', outside); window.addEventListener('resize', place); window.addEventListener('scroll', place, true); return () => { cancelAnimationFrame(frame); document.removeEventListener('pointerdown', outside); window.removeEventListener('resize', place); window.removeEventListener('scroll', place, true); }; }, [open]); return
{selected.length ? : null} {open ? : null}
; }