import { autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom"; import { useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Icon, type IconProps } from "./Icon"; import { checkIcon, chevronDownIcon, windowCloseIcon } from "./icons"; import { VirtualList } from "../virtual/VirtualList"; import styles from "./MultiSelect.module.scss"; export type MultiSelectOption = { value: string; label: string; icon?: IconProps["icon"]; }; export function MultiSelect({ label, value, options, onChange }: { label: string; value: string[]; options: MultiSelectOption[]; onChange: (value: string[]) => void; }) { const root = useRef(null); const trigger = useRef(null); const menu = useRef(null); const firstOption = useRef(null); const menuId = useId(); const [open, setOpen] = useState(false); const [position, setPosition] = useState({ left: 0, top: 0, width: 220, maxHeight: 280 }); useLayoutEffect(() => { if (!open || !root.current || !menu.current) return; return autoUpdate(root.current, menu.current, () => void computePosition(root.current!, menu.current!, { placement: "bottom-start", middleware: [offset(5), flip({ padding: 10 }), shift({ padding: 10 }), size({ padding: 10, apply: ({ rects, availableHeight }) => setPosition((current) => ({ ...current, width: rects.reference.width, maxHeight: Math.max(120, availableHeight), })), })], }).then(({ x, y }) => setPosition((current) => ({ ...current, left: x, top: y })))); }, [open]); useEffect(() => { if (!open) return; firstOption.current?.focus(); const closeOutside = (event: PointerEvent) => { const target = event.target as Node; if (!root.current?.contains(target) && !menu.current?.contains(target)) setOpen(false); }; document.addEventListener("pointerdown", closeOutside); return () => document.removeEventListener("pointerdown", closeOutside); }, [open]); const selected = new Set(value); const summary = value.length === 0 ? t("全部") : t("已选 {count} 项", { count: value.length }); const toggle = (option: string) => onChange(selected.has(option) ? value.filter((item) => item !== option) : [...value, option]); const close = () => { setOpen(false); trigger.current?.focus(); }; const openMenu = () => { const width = root.current?.getBoundingClientRect().width; if (width) setPosition((current) => ({ ...current, width })); setOpen(true); }; return <>
{value.length > 0 && }
{open && createPortal(
event.stopPropagation()} onKeyDown={(event) => { if (event.key === "Escape") { event.preventDefault(); close(); } }} >
{t("暂无选项")}
} >{(option, index) => }
{options.length > 0 &&
} , document.body)} ; }