JavaScript固定鼠标光标:拖拽Resize组件光标脱离问题求助
拖拽调整宽度触达maxWidth后鼠标指针固定方案
问题描述
我实现了一个可通过拖拽边框调整宽度的div,设置了maxWidth限制。当div宽度达到最大值后不再变化,此时鼠标指针会脱离拖拽边框区域,导致后续拖拽操作失效。需要实现:触达最大宽度后,鼠标指针仍固定在边框样式,直到鼠标释放。
原实现代码
import { cn } from "@/lib/utils"; import { useEffect, useRef, useState } from "react"; type ExtraProps = { width?: number; maxWidth?: number; minWidth?: number; isLast?: boolean; }; type ResizablePaneProps = React.HTMLAttributes<HTMLDivElement> & ExtraProps; function ResizablePane({ className, width: initialWidth, maxWidth, minWidth, isLast = false, ...props }: ResizablePaneProps) { const [width, setWidth] = useState(initialWidth ?? 0); const [isMouseDown, setIsMouseDown] = useState(false); const divRef = useRef<HTMLDivElement>(null); useEffect(() => { const handleMouseMove = (e:MouseEvent) => { if (!isMouseDown) return; const movement = e.movementX; setWidth(calcMovement(width + movement, maxWidth, minWidth)); }; const handleMouseUp = () => setIsMouseDown(false); document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [width, isMouseDown]); useEffect(() => { if (divRef.current && !initialWidth) { setWidth(divRef.current.offsetWidth); } }, []); return ( <div ref={divRef} {...props} className={cn( calcClass({ width, maxWidth, minWidth, isLast }), className )} style={{ width, maxWidth, minWidth }} > <h1 className="text-3xl">{width}</h1> {String(isMouseDown)} {props.children} {!isLast && ( <div className="absolute w-[5px] h-full -right-[3px] top-0 cursor-ew-resize flex justify-center z-10" onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); setIsMouseDown(true); }} > <div className="w-[1px] bg-red-400 h-full"></div> </div> )} </div> ); } function calcClass({ width, maxWidth, minWidth, isLast }: ExtraProps) { const defaultCls = "relative"; const nonEmpty = [width, maxWidth, minWidth].filter(Boolean); return nonEmpty.length === 0 || isLast ? "grow " + defaultCls : defaultCls; } function calcMovement(width: number, maxWidth?: number, minWidth?: number) { if (maxWidth && width >= maxWidth) { return maxWidth; } if (minWidth && width <= minWidth) { return minWidth; } return width; } export default ResizablePane;
解决方案
核心原因
原实现中,当div触达maxWidth后,宽度不再变化,拖拽边框的位置固定在div右边界。此时鼠标继续向右移动会脱离边框的可点击区域,导致光标样式丢失,且后续拖拽逻辑失效。
修改要点
- 记录拖拽初始状态:用ref存储拖拽开始时的鼠标X坐标和div初始宽度,避免依赖
width状态更新带来的计算偏差 - 全局光标锁定:拖拽开始时给document添加拖拽光标样式,直到鼠标释放时移除,确保光标始终保持拖拽样式
- 优化宽度计算逻辑:基于初始位置计算目标宽度,而非累加
movementX,避免状态更新滞后问题 - 维持拖拽状态:即使触达maxWidth/minWidth,也要保持
isMouseDown状态为true,直到鼠标释放
修改后的完整代码
import { cn } from "@/lib/utils"; import { useEffect, useRef, useState } from "react"; type ExtraProps = { width?: number; maxWidth?: number; minWidth?: number; isLast?: boolean; }; type ResizablePaneProps = React.HTMLAttributes<HTMLDivElement> & ExtraProps; function ResizablePane({ className, width: initialWidth, maxWidth, minWidth, isLast = false, ...props }: ResizablePaneProps) { const [width, setWidth] = useState(initialWidth ?? 0); const [isMouseDown, setIsMouseDown] = useState(false); const divRef = useRef<HTMLDivElement>(null); // 存储拖拽开始时的鼠标X坐标和初始宽度 const dragStartRef = useRef({ startX: 0, startWidth: 0 }); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isMouseDown) return; // 基于初始位置计算目标宽度 const targetWidth = dragStartRef.current.startWidth + (e.clientX - dragStartRef.current.startX); setWidth(calcMovement(targetWidth, maxWidth, minWidth)); }; const handleMouseUp = () => { setIsMouseDown(false); // 移除全局光标样式 document.documentElement.style.cursor = ""; }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.documentElement.style.cursor = ""; }; }, [isMouseDown, maxWidth, minWidth]); useEffect(() => { if (divRef.current && !initialWidth) { setWidth(divRef.current.offsetWidth); } }, []); return ( <div ref={divRef} {...props} className={cn( calcClass({ width, maxWidth, minWidth, isLast }), className )} style={{ width, maxWidth, minWidth }} > <h1 className="text-3xl">{width}</h1> {String(isMouseDown)} {props.children} {!isLast && ( <div className="absolute w-[5px] h-full -right-[3px] top-0 cursor-ew-resize flex justify-center z-10" onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); setIsMouseDown(true); // 记录拖拽初始状态 dragStartRef.current = { startX: e.clientX, startWidth: width }; // 设置全局光标样式 document.documentElement.style.cursor = "ew-resize"; }} > <div className="w-[1px] bg-red-400 h-full"></div> </div> )} </div> ); } function calcClass({ width, maxWidth, minWidth, isLast }: ExtraProps) { const defaultCls = "relative"; const nonEmpty = [width, maxWidth, minWidth].filter(Boolean); return nonEmpty.length === 0 || isLast ? "grow " + defaultCls : defaultCls; } function calcMovement(width: number, maxWidth?: number, minWidth?: number) { if (maxWidth && width >= maxWidth) { return maxWidth; } if (minWidth && width <= minWidth) { return minWidth; } return width; } export default ResizablePane;
效果说明
修改后,即使div触达maxWidth或minWidth,鼠标指针会一直保持拖拽样式,直到用户释放鼠标按键。同时拖拽状态会持续跟踪,避免中途失效的问题。
内容的提问来源于stack exchange,提问作者Tang Si
相关产品推荐
相关产品推荐

