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;
|
width: max-content;
|
||||||
box-shadow: var(--tutu-shadow-e8);
|
box-shadow: var(--tutu-shadow-e8);
|
||||||
contain: content;
|
contain: content;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
|
||||||
&.e1 {
|
&.e1 {
|
||||||
box-shadow: var(--tutu-shadow-e9);
|
box-shadow: var(--tutu-shadow-e9);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useWindowSize } from "@solid-primitives/resize-observer";
|
import { useWindowSize } from "@solid-primitives/resize-observer";
|
||||||
import { MenuList } from "@suid/material";
|
import { MenuList } from "@suid/material";
|
||||||
import {
|
import {
|
||||||
|
batch,
|
||||||
createEffect,
|
createEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
splitProps,
|
splitProps,
|
||||||
|
@ -65,11 +66,39 @@ export function createManagedMenuState() {
|
||||||
return !!anchor();
|
return !!anchor();
|
||||||
},
|
},
|
||||||
anchor: anchor as () => Anchor,
|
anchor: anchor as () => Anchor,
|
||||||
onClose: () => setAnchor(),
|
onClose: (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
return setAnchor();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
] as const;
|
] 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
|
* Material Menu Component. This component is
|
||||||
* implemented with dialog and {@link MenuList} from SUID.
|
* 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 createManagedMenuState} and you don't need to manage the open and close.
|
||||||
* - Use {@link MenuItem} from SUID as children.
|
* - Use {@link MenuItem} from SUID as children.
|
||||||
*/
|
*/
|
||||||
const Menu: Component<MenuProps> = (props) => {
|
const Menu: Component<MenuProps> = (oprops) => {
|
||||||
let root: HTMLDialogElement;
|
let root: HTMLDialogElement;
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const [, rest] = splitProps(props, ["open", "onClose", "anchor"]);
|
const [props, rest] = splitProps(oprops, [
|
||||||
|
"open",
|
||||||
|
"onClose",
|
||||||
|
"anchor",
|
||||||
|
"MenuListProps",
|
||||||
|
"children",
|
||||||
|
]);
|
||||||
|
|
||||||
const [anchorPos, setAnchorPos] = createSignal<{
|
const [anchorPos, setAnchorPos] = createSignal<{
|
||||||
left?: number;
|
left?: number;
|
||||||
|
@ -106,16 +141,30 @@ const Menu: Component<MenuProps> = (props) => {
|
||||||
|
|
||||||
let openAnimationOrigin: "lt" | "rt" = "lt";
|
let openAnimationOrigin: "lt" | "rt" = "lt";
|
||||||
|
|
||||||
createEffect(() => {
|
const animateOpen = () => {
|
||||||
if (props.open) {
|
|
||||||
const a = props.anchor();
|
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();
|
root.showModal();
|
||||||
const rend = root.getBoundingClientRect();
|
const rend = root.getBoundingClientRect();
|
||||||
|
|
||||||
const { width } = windowSize;
|
|
||||||
const { left, top, right, e } = a;
|
|
||||||
if (left > width / 2) {
|
if (left > width / 2) {
|
||||||
openAnimationOrigin = "rt";
|
openAnimationOrigin = "rt";
|
||||||
setAnchorPos({
|
setAnchorPos({
|
||||||
|
@ -123,34 +172,26 @@ const Menu: Component<MenuProps> = (props) => {
|
||||||
top,
|
top,
|
||||||
e,
|
e,
|
||||||
});
|
});
|
||||||
|
|
||||||
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
|
|
||||||
} else {
|
} else {
|
||||||
openAnimationOrigin = "lt";
|
openAnimationOrigin = "lt";
|
||||||
setAnchorPos({ left, top, e });
|
setAnchorPos({ left, top, e });
|
||||||
|
}
|
||||||
|
|
||||||
const overflow = root.style.overflow;
|
if (!isOpened) {
|
||||||
root.style.overflow = "hidden";
|
switch (openAnimationOrigin) {
|
||||||
const duration = (rend.height / 1600) * 1000;
|
case "lt":
|
||||||
const easing = ANIM_CURVE_STD;
|
animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
|
||||||
const animation = root.animate(
|
break;
|
||||||
{
|
case "rt":
|
||||||
height: [`${rend.height / 2}px`, `${rend.height}px`],
|
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
|
||||||
width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
|
break;
|
||||||
},
|
|
||||||
{
|
|
||||||
duration,
|
|
||||||
easing,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
animation.addEventListener(
|
|
||||||
"finish",
|
|
||||||
() => (root.style.overflow = overflow),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO: update the pos
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.open) {
|
||||||
|
animateOpen();
|
||||||
} else {
|
} else {
|
||||||
animateClose();
|
animateClose();
|
||||||
}
|
}
|
||||||
|
@ -185,27 +226,42 @@ const Menu: Component<MenuProps> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const onDialogClick = (
|
||||||
<dialog
|
event: MouseEvent & { currentTarget: HTMLDialogElement },
|
||||||
ref={root!}
|
) => {
|
||||||
onClose={props.onClose}
|
event.stopPropagation();
|
||||||
onClick={(e) => {
|
if (event.currentTarget !== event.target) return;
|
||||||
if (e.target === root) {
|
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 (props.onClose) {
|
||||||
if (Array.isArray(props.onClose)) {
|
if (Array.isArray(props.onClose)) {
|
||||||
props.onClose[0](props.onClose[1], e);
|
props.onClose[0](props.onClose[1], event);
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
props.onClose as (
|
props.onClose as (
|
||||||
event: Event & { currentTarget: HTMLDialogElement },
|
event: Event & { currentTarget: HTMLDialogElement },
|
||||||
) => void
|
) => 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={{
|
style={{
|
||||||
left: px(anchorPos().left),
|
left: px(anchorPos().left),
|
||||||
top: px(anchorPos().top),
|
top: px(anchorPos().top),
|
||||||
|
|
|
@ -7,11 +7,7 @@ import {
|
||||||
ListItemText,
|
ListItemText,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from "@suid/material";
|
} from "@suid/material";
|
||||||
import {
|
import { Show, createUniqueId, type ParentComponent } from "solid-js";
|
||||||
Show,
|
|
||||||
createUniqueId,
|
|
||||||
type ParentComponent,
|
|
||||||
} from "solid-js";
|
|
||||||
import {
|
import {
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
Bookmark as BookmarkIcon,
|
Bookmark as BookmarkIcon,
|
||||||
|
@ -39,9 +35,7 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
|
|
||||||
const [open, state] = createManagedMenuState();
|
const [open, state] = createManagedMenuState();
|
||||||
|
|
||||||
const onClick = (
|
const onClick = (event: { currentTarget: HTMLElement }) => {
|
||||||
event: MouseEvent & { currentTarget: HTMLButtonElement },
|
|
||||||
) => {
|
|
||||||
open(event.currentTarget.getBoundingClientRect());
|
open(event.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,8 +48,8 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
sx={{ borderRadius: "50%" }}
|
sx={{ borderRadius: "50%" }}
|
||||||
id={buttonId}
|
id={buttonId}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-controls={open() ? menuId : undefined}
|
aria-controls={state.open ? menuId : undefined}
|
||||||
aria-expanded={open() ? "true" : undefined}
|
aria-expanded={state.open ? "true" : "false"}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={`${inf()?.displayName}'s avatar`}
|
alt={`${inf()?.displayName}'s avatar`}
|
||||||
|
|
Loading…
Reference in a new issue