自定义仿Mac OS UI过渡效果的Popper组件技术问题
仿Mac OS风格Popper组件的过渡效果优化
问题说明
基于react-transition-group实现了仿Mac OS UI风格的自定义Popper组件,当前存在过渡效果不符合预期的问题:
- 预期行为:右键点击区域内新位置时,旧菜单的过渡未完全卸载前,新菜单就在新位置开始挂载并执行过渡,实现连贯的Mac OS式切换效果
- 当前实际行为:右键点击时旧菜单立即卸载,虽不影响基本使用,但过渡连贯性不足
问题根源分析
- 已导入
SwitchTransition但未使用,该组件是实现新旧组件过渡衔接的核心,可让旧组件退出过渡与新组件进入过渡同时进行 - 用
setKey强制重挂载+setTimeout延迟打开的逻辑,打断了过渡连贯性,导致旧组件直接卸载后才加载新组件 handleContextMenu中重复调用handlePopperMenuClose,逻辑冗余且会提前触发旧组件卸载onTransitionEnd中直接调用handlePopperMenuClose,会干扰新组件的正常显示流程
修改方案
- 使用
SwitchTransition包裹CSSTransition,配置mode="in-out"实现新旧组件过渡的衔接 - 移除
key状态与setTimeout逻辑,用锚点坐标作为CSSTransition的key,位置变化时自动触发组件切换 - 简化
handleContextMenu逻辑,直接设置新锚点并保持菜单打开状态 - 修正过渡结束钩子逻辑,仅在旧组件退出过渡完成后清理锚点
- 正确配置
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
相关产品推荐
相关产品推荐

