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,51 +141,57 @@ const Menu: Component<MenuProps> = (props) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let openAnimationOrigin: "lt" | "rt" = "lt";
 | 
					  let openAnimationOrigin: "lt" | "rt" = "lt";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    root.showModal();
 | 
				
			||||||
 | 
					    const rend = root.getBoundingClientRect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (left > width / 2) {
 | 
				
			||||||
 | 
					      openAnimationOrigin = "rt";
 | 
				
			||||||
 | 
					      setAnchorPos({
 | 
				
			||||||
 | 
					        left: right - rend.width,
 | 
				
			||||||
 | 
					        top,
 | 
				
			||||||
 | 
					        e,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      openAnimationOrigin = "lt";
 | 
				
			||||||
 | 
					      setAnchorPos({ left, top, e });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!isOpened) {
 | 
				
			||||||
 | 
					      switch (openAnimationOrigin) {
 | 
				
			||||||
 | 
					        case "lt":
 | 
				
			||||||
 | 
					          animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case "rt":
 | 
				
			||||||
 | 
					          animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createEffect(() => {
 | 
					  createEffect(() => {
 | 
				
			||||||
    if (props.open) {
 | 
					    if (props.open) {
 | 
				
			||||||
      const a = props.anchor();
 | 
					      animateOpen();
 | 
				
			||||||
 | 
					 | 
				
			||||||
      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({
 | 
					 | 
				
			||||||
            left: right - rend.width,
 | 
					 | 
				
			||||||
            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),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // TODO: update the pos
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      animateClose();
 | 
					      animateClose();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -185,27 +226,42 @@ const Menu: Component<MenuProps> = (props) => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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], event);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            props.onClose as (
 | 
				
			||||||
 | 
					              event: Event & { currentTarget: HTMLDialogElement },
 | 
				
			||||||
 | 
					            ) => void
 | 
				
			||||||
 | 
					          )(event);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <dialog
 | 
					    <dialog
 | 
				
			||||||
      ref={root!}
 | 
					      ref={root!}
 | 
				
			||||||
      onClose={props.onClose}
 | 
					      onClose={props.onClose}
 | 
				
			||||||
      onClick={(e) => {
 | 
					      onCancel={props.onClose}
 | 
				
			||||||
        if (e.target === root) {
 | 
					      onClick={onDialogClick}
 | 
				
			||||||
          if (props.onClose) {
 | 
					      class={`Menu e${anchorPos().e || "0"}`}
 | 
				
			||||||
            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 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue