import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom"; import { useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Button } from "./Button"; import { Icon } from "./Icon"; import { chevronDownIcon } from "./icons"; import styles from "./ActionMenu.module.scss"; export type ActionMenuItem = | { id: string; label: string; type?: "action"; disabled?: boolean; onSelect: () => void; } | { id: string; label: string; type: "text"; }; /** 触发器 + 动作列表的下拉菜单,用于容纳卡片上的次要操作。 */ export function ActionMenu({ label, items, disabled }: { label: string; items: ActionMenuItem[]; disabled?: boolean; }) { const trigger = useRef(null); const menu = useRef(null); const menuId = useId(); const [open, setOpen] = useState(false); const [position, setPosition] = useState({ left: 0, top: 0 }); useLayoutEffect(() => { if (!open || !trigger.current || !menu.current) return; return autoUpdate(trigger.current, menu.current, () => void computePosition(trigger.current!, menu.current!, { placement: "bottom-end", middleware: [offset(5), flip({ padding: 10 }), shift({ padding: 10 })], }).then(({ x, y }) => setPosition({ left: x, top: y }))); }, [open]); useEffect(() => { if (!open) return; const outside = (event: PointerEvent) => { if (!trigger.current?.contains(event.target as Node) && !menu.current?.contains(event.target as Node)) { setOpen(false); } }; document.addEventListener("pointerdown", outside); return () => document.removeEventListener("pointerdown", outside); }, [open]); const close = () => { setOpen(false); trigger.current?.focus(); }; return <> {open && createPortal( , document.body, )} ; }