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

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右边界。此时鼠标继续向右移动会脱离边框的可点击区域,导致光标样式丢失,且后续拖拽逻辑失效。

修改要点

  1. 记录拖拽初始状态:用ref存储拖拽开始时的鼠标X坐标和div初始宽度,避免依赖width状态更新带来的计算偏差
  2. 全局光标锁定:拖拽开始时给document添加拖拽光标样式,直到鼠标释放时移除,确保光标始终保持拖拽样式
  3. 优化宽度计算逻辑:基于初始位置计算目标宽度,而非累加movementX,避免状态更新滞后问题
  4. 维持拖拽状态:即使触达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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 11:05:55