Menu: fix #47, skip animateOpen if animating

This commit is contained in:
thislight 2024-11-20 00:27:52 +08:00
parent a69a5c31e5
commit 9a7710c070
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
3 changed files with 124 additions and 73 deletions

View file

@ -7,6 +7,7 @@
width: max-content;
box-shadow: var(--tutu-shadow-e8);
contain: content;
overscroll-behavior: contain;
&.e1 {
box-shadow: var(--tutu-shadow-e9);

View file

@ -1,6 +1,7 @@
import { useWindowSize } from "@solid-primitives/resize-observer";
import { MenuList } from "@suid/material";
import {
batch,
createEffect,
createSignal,
splitProps,
@ -65,11 +66,39 @@ export function createManagedMenuState() {
return !!anchor();
},
anchor: anchor as () => Anchor,
onClose: () => setAnchor(),
onClose: (event: Event) => {
event.preventDefault();
return setAnchor();
},
},
] as const;
}
function animateGrowFromTopLeft(
element: HTMLElement,
opts?: Omit<KeyframeAnimationOptions, "duration">,
) {
const rend = element.getBoundingClientRect();
const overflow = element.style.overflow;
element.style.overflow = "hidden";
const duration = (rend.height / 1600) * 1000;
const animation = element.animate(
{
height: [`${rend.height / 2}px`, `${rend.height}px`],
width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
},
{
duration,
...opts,
},
);
animation.addEventListener(
"finish",
() => (element.style.overflow = overflow),
);
return animation;
}
/**
* Material Menu Component. This component is
* implemented with dialog and {@link MenuList} from SUID.
@ -78,10 +107,16 @@ export function createManagedMenuState() {
* - Use {@link createManagedMenuState} and you don't need to manage the open and close.
* - Use {@link MenuItem} from SUID as children.
*/
const Menu: Component<MenuProps> = (props) => {
const Menu: Component<MenuProps> = (oprops) => {
let root: HTMLDialogElement;
const windowSize = useWindowSize();
const [, rest] = splitProps(props, ["open", "onClose", "anchor"]);
const [props, rest] = splitProps(oprops, [
"open",
"onClose",
"anchor",
"MenuListProps",
"children",
]);
const [anchorPos, setAnchorPos] = createSignal<{
left?: number;
@ -106,16 +141,30 @@ const Menu: Component<MenuProps> = (props) => {
let openAnimationOrigin: "lt" | "rt" = "lt";
createEffect(() => {
if (props.open) {
const animateOpen = () => {
const a = props.anchor();
const { width } = windowSize;
const { left, top, right, e } = a;
const isOpened = root.open;
// There are incomplete animations.
// For `getBoundingClientRect()`, WebKit reports the initial state
// of the element, whilst Firefox reports the final state.
//
// We skip if animations are still on the element
// to avoid the problem on WebKit.
// Here use the final state.
//
// This is a dirty workaround. It's here because the feature is still
// works with it.
// I am curious that why the ones on the other parts are works. (Rubicon)
if (root.getAnimations().length > 0) {
return;
}
if (!root.open) {
root.showModal();
const rend = root.getBoundingClientRect();
const { width } = windowSize;
const { left, top, right, e } = a;
if (left > width / 2) {
openAnimationOrigin = "rt";
setAnchorPos({
@ -123,34 +172,26 @@ const Menu: Component<MenuProps> = (props) => {
top,
e,
});
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
} else {
openAnimationOrigin = "lt";
setAnchorPos({ left, top, e });
}
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),
);
if (!isOpened) {
switch (openAnimationOrigin) {
case "lt":
animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
break;
case "rt":
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
break;
}
} else {
// TODO: update the pos
}
};
createEffect(() => {
if (props.open) {
animateOpen();
} else {
animateClose();
}
@ -185,27 +226,42 @@ const Menu: Component<MenuProps> = (props) => {
}
};
return (
<dialog
ref={root!}
onClose={props.onClose}
onClick={(e) => {
if (e.target === root) {
const onDialogClick = (
event: MouseEvent & { currentTarget: HTMLDialogElement },
) => {
event.stopPropagation();
if (event.currentTarget !== event.target) return;
if (!event.currentTarget.open) return;
const rect = event.currentTarget.getBoundingClientRect();
const isNotInDialog =
event.clientY < rect.top ||
event.clientY > rect.bottom ||
event.clientX < rect.left ||
event.clientX > rect.right;
if (isNotInDialog) {
if (props.onClose) {
if (Array.isArray(props.onClose)) {
props.onClose[0](props.onClose[1], e);
props.onClose[0](props.onClose[1], event);
} else {
(
props.onClose as (
event: Event & { currentTarget: HTMLDialogElement },
) => void
)(e);
)(event);
}
}
e.stopPropagation();
}
}}
class={`Menu e${anchorPos().e || 0}`}
};
return (
<dialog
ref={root!}
onClose={props.onClose}
onCancel={props.onClose}
onClick={onDialogClick}
class={`Menu e${anchorPos().e || "0"}`}
style={{
left: px(anchorPos().left),
top: px(anchorPos().top),

View file

@ -7,11 +7,7 @@ import {
ListItemText,
MenuItem,
} from "@suid/material";
import {
Show,
createUniqueId,
type ParentComponent,
} from "solid-js";
import { Show, createUniqueId, type ParentComponent } from "solid-js";
import {
Settings as SettingsIcon,
Bookmark as BookmarkIcon,
@ -39,9 +35,7 @@ const ProfileMenuButton: ParentComponent<{
const [open, state] = createManagedMenuState();
const onClick = (
event: MouseEvent & { currentTarget: HTMLButtonElement },
) => {
const onClick = (event: { currentTarget: HTMLElement }) => {
open(event.currentTarget.getBoundingClientRect());
};
@ -54,8 +48,8 @@ const ProfileMenuButton: ParentComponent<{
sx={{ borderRadius: "50%" }}
id={buttonId}
onClick={onClick}
aria-controls={open() ? menuId : undefined}
aria-expanded={open() ? "true" : undefined}
aria-controls={state.open ? menuId : undefined}
aria-expanded={state.open ? "true" : "false"}
>
<Avatar
alt={`${inf()?.displayName}'s avatar`}