import { useWindowSize } from "@solid-primitives/resize-observer"; import { MenuList } from "@suid/material"; import { createEffect, createSignal, type JSX, type ParentComponent, } from "solid-js"; import { ANIM_CURVE_STD } from "./theme"; import "./Menu.css"; import { animateGrowFromTopRight, animateShrinkToTopRight, } from "../platform/anim"; type Props = { open?: boolean; onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>; anchor: () => DOMRect; }; function px(n?: number) { if (n) { return `${n}px`; } else { return undefined; } } const Menu: ParentComponent<Props> = (props) => { let root: HTMLDialogElement; const windowSize = useWindowSize(); const [anchorPos, setAnchorPos] = createSignal<{ left?: number; top?: number; }>({}); let openAnimationOrigin: "lt" | "rt" = "lt"; createEffect(() => { if (props.open) { const a = props.anchor(); if (!root.open) { root.showModal(); const rend = root.getBoundingClientRect(); const { width } = windowSize; const { left, top, right } = a; if (left > width / 2) { openAnimationOrigin = "rt"; setAnchorPos({ left: right - rend.width, top, }); animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD }); } else { openAnimationOrigin = "lt"; setAnchorPos({ left, top }); const overflow = root.style.overflow; root.style.overflow = "hidden"; const duration = (rend.height / 1600) * 1000; const easing = ANIM_CURVE_STD; const animation = root.animate( { height: [`${rend.height / 2}px`, `${rend.height}px`], width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`], }, { duration, easing, }, ); animation.addEventListener( "finish", () => (root.style.overflow = overflow), ); } } else { // TODO: update the pos } } else { animateClose(); } }); const animateClose = () => { const rend = root.getBoundingClientRect(); if (openAnimationOrigin === "lt") { const overflow = root.style.overflow; root.style.overflow = "hidden"; const animation = root.animate( { height: [`${rend.height}px`, `${rend.height / 2}px`], width: [`${rend.width}px`, `${(rend.width / 4) * 3}px`], }, { duration: (rend.height / 2 / 1600) * 1000, easing: ANIM_CURVE_STD, }, ); animation.addEventListener("finish", () => { root.style.overflow = overflow; root.close(); }); } else { const animation = animateShrinkToTopRight(root, { easing: ANIM_CURVE_STD, }); animation.addEventListener("finish", () => { root.close(); }); } }; return ( <dialog ref={root!} onClose={props.onClose} onClick={(e) => { if (e.target === root) { if (props.onClose) { if (Array.isArray(props.onClose)) { props.onClose[0](props.onClose[1], e); } else { ( props.onClose as ( event: Event & { currentTarget: HTMLDialogElement }, ) => void )(e); } } e.stopPropagation(); } }} class="Menu" style={{ left: px(anchorPos().left), top: px(anchorPos().top), /* FIXME: the content may be overflow */ }} > <div style={{ background: "var(--tutu-color-surface)", }} > <MenuList>{props.children}</MenuList> </div> </dialog> ); }; export default Menu;