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

横向滑动子元素时锁定父元素垂直滚动的问题修复

解决React横向滑动Hook的垂直拖拽与触摸事件取消错误

问题场景

开发React Hook监听元素横向滑动,需求是当横向滑动偏移超过5px时锁定父元素垂直滚动,避免横向滑动时出现垂直拖拽。但实现后控制台触发以下错误:
[Intervention] Ignored attempt to cancel a touchmove event with cancelable=false, for example because scrolling is in progress and cannot be interrupted.
尝试检查e.cancelable为真才执行逻辑后,Hook直接失效。

错误根源

  1. 滚动触发后无法取消事件:浏览器已启动垂直滚动时,touchmove事件的cancelable属性变为false,此时调用preventDefault()会触发警告。
  2. 重复绑定事件监听器:原代码每次touchmove都会给父容器添加一次cancelTouch监听器,导致多次执行preventDefault。
  3. DOM元素获取方式不合理:直接用document.getElementById可能在组件挂载前获取不到元素,不符合React数据流模式。
  4. touchAction设置逻辑冲突:动态切换touchAction的同时叠加事件监听,导致浏览器触摸行为处理混乱。

解决方案

  1. 用React Ref跟踪锁定状态,避免重复绑定事件
  2. 在取消触摸事件前先检查e.cancelable,仅允许取消可中断的事件
  3. 用touchAction: pan-x替代none,精准控制允许的触摸方向
  4. 通过React Ref传递父容器引用,替代直接操作DOM
  5. 对比横向/纵向偏移量,仅当用户明确横向滑动时才锁定垂直滚动

修改后的完整代码

Hook实现(TypeScript版)

import { useRef } from "react";

export const useSwipe = (containerRef: React.RefObject<HTMLElement>) => {
  const touchStartX = useRef<number | null>(null);
  const touchStartY = useRef<number | null>(null);
  const isHorizontalLocked = useRef(false);

  const minSwipeDistance = 100;
  const lockThreshold = 5;

  const cancelTouch = (e: TouchEvent) => {
    // 仅取消可中断的事件,避免警告
    if (e.cancelable) {
      e.preventDefault();
    }
  };

  const onTouchStart = (e: React.TouchEvent) => {
    touchStartX.current = e.targetTouches[0].clientX;
    touchStartY.current = e.targetTouches[0].clientY;
    isHorizontalLocked.current = false;
  };

  const onTouchMove = (e: React.TouchEvent) => {
    if (!touchStartX.current || !touchStartY.current) return;
    
    const currentX = e.targetTouches[0].clientX;
    const currentY = e.targetTouches[0].clientY;
    const deltaX = Math.abs(currentX - touchStartX.current);
    const deltaY = Math.abs(currentY - touchStartY.current);

    // 仅首次检测到横向滑动超过阈值时锁定
    if (deltaX > lockThreshold && deltaX > deltaY && !isHorizontalLocked.current) {
      isHorizontalLocked.current = true;
      const box = containerRef.current;
      if (box) {
        box.style.touchAction = "pan-x"; // 仅允许横向滑动
        box.addEventListener("touchmove", cancelTouch, { passive: false });
      }
    }
  };

  const onTouchEnd = () => {
    const box = containerRef.current;
    if (box) {
      box.style.touchAction = "auto";
      box.removeEventListener("touchmove", cancelTouch);
    }

    if (!touchStartX.current) return;
    const currentX = touchEndX?.current || touchStartX.current;
    const distanceX = touchStartX.current - currentX;
    const isLeftSwipe = distanceX > minSwipeDistance;
    const isRightSwipe = distanceX < -minSwipeDistance;
    
    isHorizontalLocked.current = false;
    return isRightSwipe ? "right" : isLeftSwipe ? "left" : undefined;
  };

  return { onTouchStart, onTouchMove, onTouchEnd };
};

组件使用示例(JavaScript版)

const { useRef } = React;

const useSwipe = (containerRef) => {
  const touchStartX = useRef(null);
  const touchStartY = useRef(null);
  const isHorizontalLocked = useRef(false);

  const minSwipeDistance = 100;
  const lockThreshold = 5;

  const cancelTouch = (e) => {
    if (e.cancelable) {
      e.preventDefault();
    }
  };

  const onTouchStart = (e) => {
    touchStartX.current = e.targetTouches[0].clientX;
    touchStartY.current = e.targetTouches[0].clientY;
    isHorizontalLocked.current = false;
  };

  const onTouchMove = (e) => {
    if (!touchStartX.current || !touchStartY.current) return;
    
    const currentX = e.targetTouches[0].clientX;
    const currentY = e.targetTouches[0].clientY;
    const deltaX = Math.abs(currentX - touchStartX.current);
    const deltaY = Math.abs(currentY - touchStartY.current);

    if (deltaX > lockThreshold && deltaX > deltaY && !isHorizontalLocked.current) {
      isHorizontalLocked.current = true;
      const box = containerRef.current;
      if (box) {
        box.style.touchAction = "pan-x";
        box.addEventListener("touchmove", cancelTouch, { passive: false });
      }
    }
  };

  const onTouchEnd = () => {
    const box = containerRef.current;
    if (box) {
      box.style.touchAction = "auto";
      box.removeEventListener("touchmove", cancelTouch);
    }

    if (!touchStartX.current) return;
    const currentX = touchEndX?.current || touchStartX.current;
    const distanceX = touchStartX.current - currentX;
    const isLeftSwipe = distanceX > minSwipeDistance;
    const isRightSwipe = distanceX < -minSwipeDistance;
    
    isHorizontalLocked.current = false;
    return isRightSwipe ? "right" : isLeftSwipe ? "left" : undefined;
  };

  return { onTouchStart, onTouchMove, onTouchEnd };
};

const List = () => {
  const containerRef = useRef(null);
  const { onTouchStart, onTouchMove, onTouchEnd } = useSwipe(containerRef);
  
  const handleTouchEnd = () => {
    const swipeDirection = onTouchEnd();
    if (swipeDirection === "left") console.log("左滑");
    if (swipeDirection === "right") console.log("右滑");
  };

  const items = Array(30).fill("");
  return (
    <div ref={containerRef} className="container" id="scrollable-cards-box">
      {items.map((_, index) => (
        <ul
          className="item"
          key={index}
          onTouchStart={onTouchStart}
          onTouchMove={onTouchMove}
          onTouchEnd={handleTouchEnd}
        >
          列表项 {index + 1}
        </ul>
      ))}
    </div>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<List />);

样式代码

.container {
  background-color: gray;
  overflow-y: auto;
  height: 400px;
}

.item {
  background-color: skyblue;
  padding: 15px 5px;
  margin: 5px 0;
  list-style: none;
}

HTML结构

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

关键改进说明

  • 状态跟踪:用isHorizontalLocked避免重复绑定事件和样式设置,提升性能
  • 事件安全处理:检查e.cancelable彻底消除控制台警告
  • 精准触摸控制:touchAction: pan-x明确允许横向滑动,同时禁止垂直滚动
  • React最佳实践:通过Ref传递容器引用,避免直接操作DOM
  • 滑动判断优化:对比横向/纵向偏移量,确保锁定逻辑仅在用户明确横向滑动时触发

内容的提问来源于stack exchange,提问作者magrega

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 07:59:50