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,51 +141,57 @@ const Menu: Component<MenuProps> = (props) => {
 | 
			
		|||
 | 
			
		||||
  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(() => {
 | 
			
		||||
    if (props.open) {
 | 
			
		||||
      const a = props.anchor();
 | 
			
		||||
 | 
			
		||||
      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
 | 
			
		||||
      }
 | 
			
		||||
      animateOpen();
 | 
			
		||||
    } else {
 | 
			
		||||
      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 (
 | 
			
		||||
    <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();
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      class={`Menu e${anchorPos().e || 0}`}
 | 
			
		||||
      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…
	
	Add table
		Add a link
		
	
		Reference in a new issue