Menu: fix #47, skip animateOpen if animating
This commit is contained in:
parent
a69a5c31e5
commit
9a7710c070
3 changed files with 124 additions and 73 deletions
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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`}
|
||||
|
|
Loading…
Reference in a new issue