You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义仿Mac OS UI过渡效果的Popper组件技术问题

仿Mac OS风格Popper组件的过渡效果优化

问题说明

基于react-transition-group实现了仿Mac OS UI风格的自定义Popper组件,当前存在过渡效果不符合预期的问题:

  • 预期行为:右键点击区域内新位置时,旧菜单的过渡未完全卸载前,新菜单就在新位置开始挂载并执行过渡,实现连贯的Mac OS式切换效果
  • 当前实际行为:右键点击时旧菜单立即卸载,虽不影响基本使用,但过渡连贯性不足

问题根源分析

  1. 已导入SwitchTransition但未使用,该组件是实现新旧组件过渡衔接的核心,可让旧组件退出过渡与新组件进入过渡同时进行
  2. 用setKey强制重挂载+setTimeout延迟打开的逻辑,打断了过渡连贯性,导致旧组件直接卸载后才加载新组件
  3. handleContextMenu中重复调用handlePopperMenuClose,逻辑冗余且会提前触发旧组件卸载
  4. onTransitionEnd中直接调用handlePopperMenuClose,会干扰新组件的正常显示流程

修改方案

  1. 使用SwitchTransition包裹CSSTransition,配置mode="in-out"实现新旧组件过渡的衔接
  2. 移除key状态与setTimeout逻辑,用锚点坐标作为CSSTransition的key,位置变化时自动触发组件切换
  3. 简化handleContextMenu逻辑,直接设置新锚点并保持菜单打开状态
  4. 修正过渡结束钩子逻辑,仅在旧组件退出过渡完成后清理锚点
  5. 正确配置nodeRef,确保CSSTransition能准确控制DOM元素的过渡

修改后完整代码

组件代码

import React, { FC, ReactNode, useRef, useState } from "react";
import { SwitchTransition, CSSTransition } from "react-transition-group";
import "./styles.css";

type Props = {
  children: ReactNode;
};

export const ContextMenuContainer = ({ children }: Props) => {
  const [popperMenuOpen, setPopperMenuOpen] = useState(false);
  const [anchor, setAnchor] = useState<{ x: number; y: number } | null>(null);
  const nodeRef = useRef<HTMLDivElement>(null);

  const handlePopperMenuShow = () => {
    setPopperMenuOpen(true);
  };

  const handlePopperMenuClose = () => {
    setPopperMenuOpen(false);
  };

  const countAnchorRef = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    const localX = event.pageX - event.currentTarget.offsetLeft;
    const localY = event.pageY - event.currentTarget.offsetTop;
    setAnchor({ x: localX, y: localY });
  };

  const handleContextMenu = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    e.preventDefault();
    countAnchorRef(e);
    handlePopperMenuShow();
  };

  return (
    <div
      style={{
        position: "relative",
        height: "100px",
        border: "1px dashed black"
      }}
      onContextMenu={handleContextMenu}
      onMouseDown={(e) => {
        if (e.button === 0) {
          handlePopperMenuClose();
        }
      }}
    >
      <SwitchTransition mode="in-out">
        <CSSTransition
          nodeRef={nodeRef}
          key={anchor ? `${anchor.x}-${anchor.y}` : "default"}
          timeout={300}
          in={popperMenuOpen && anchor !== null}
          mountOnEnter
          unmountOnExit
          classNames="popper"
          onExited={() => {
            setAnchor(null);
          }}
        >
          <div
            ref={nodeRef}
            style={{
              position: "absolute",
              top: anchor?.y ?? 0,
              left: anchor?.x ?? 0,
              zIndex: 10,
              backgroundColor: "rgba(25,25,25, 0.3)",
              padding: "8px 12px",
              color: "#fff"
            }}
          >
            test menu
          </div>
        </CSSTransition>
      </SwitchTransition>

      <div
        style={{
          width: "30px",
          height: "30px",
          backgroundColor: "rgba(100,100,100,0.12)"
        }}
      ></div>

      {children}
    </div>
  );
};

对应的styles.css

.popper-enter {
  opacity: 0;
  transform: scale(0.95);
}

.popper-enter-active {
  opacity: 1;
  transform: scale(1);
  transition: opacity 300ms, transform 300ms;
}

.popper-exit {
  opacity: 1;
  transform: scale(1);
}

.popper-exit-active {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 300ms, transform 300ms;
}

效果说明

  • SwitchTransition的mode="in-out"配置让新组件先启动进入过渡,旧组件再启动退出过渡,实现类似Mac OS的连贯切换体验
  • 用锚点坐标拼接作为CSSTransition的key,每次右键新位置都会自动触发组件切换
  • 300ms的过渡时长更贴近Mac OS的动画节奏
  • onExited钩子仅在旧组件完全退出后清理锚点,避免干扰新组件的显示

内容的提问来源于stack exchange,提问作者Brian Tzu-Pin Ho

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 10:35:17