137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
|
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";
|
||
|
|
||
|
type Props = {
|
||
|
open?: boolean;
|
||
|
onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>;
|
||
|
anchor: () => DOMRect;
|
||
|
};
|
||
|
|
||
|
function adjustMenuPosition(
|
||
|
rect: DOMRect,
|
||
|
[left, top]: [number, number],
|
||
|
{ width, height }: { width: number; height: number },
|
||
|
) {
|
||
|
const ntop = rect.bottom > height ? top - (rect.bottom - height) : top;
|
||
|
const nleft = rect.right > width ? left - (rect.right - width) : left;
|
||
|
return [nleft, ntop] as [number, number];
|
||
|
}
|
||
|
|
||
|
const Menu: ParentComponent<Props> = (props) => {
|
||
|
let root: HTMLDialogElement;
|
||
|
const [pos, setPos] = createSignal<[number, number]>([0, 0]);
|
||
|
const windowSize = useWindowSize();
|
||
|
|
||
|
createEffect(() => {
|
||
|
if (props.open) {
|
||
|
const a = props.anchor();
|
||
|
if (!root.open) {
|
||
|
root.showModal();
|
||
|
const rend = root.getBoundingClientRect();
|
||
|
|
||
|
setPos(adjustMenuPosition(rend, [a.left, a.top], windowSize));
|
||
|
|
||
|
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 {
|
||
|
setPos(
|
||
|
adjustMenuPosition(
|
||
|
root.getBoundingClientRect(),
|
||
|
[a.left, a.top],
|
||
|
windowSize,
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
} else {
|
||
|
animateClose();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const animateClose = () => {
|
||
|
const rend = root.getBoundingClientRect();
|
||
|
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();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
}}
|
||
|
style={{
|
||
|
position: "absolute",
|
||
|
left: `${pos()[0]}px`,
|
||
|
top: `${pos()[1]}px`,
|
||
|
border: "none",
|
||
|
padding: 0,
|
||
|
"max-width": "560px",
|
||
|
width: "max-content",
|
||
|
/*"min-width": "20vw", */
|
||
|
"box-shadow": "var(--tutu-shadow-e8)",
|
||
|
}}
|
||
|
>
|
||
|
<div
|
||
|
style={{
|
||
|
background: "var(--tutu-color-surface)",
|
||
|
}}
|
||
|
>
|
||
|
<MenuList>{props.children}</MenuList>
|
||
|
</div>
|
||
|
</dialog>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default Menu;
|