使用鼠标滚轮实现React组件的水平滚动

21

我有一个组件,在较小的桌面窗口中调整大小为Bootstrap卡片的水平行。对于没有水平鼠标滚轮且不使用触摸板的用户,我希望允许用户在悬停在此特定组件上时使用其垂直鼠标滚轮移动进行水平滚动。

这是我基于以下原始StackOverflow问题编写代码: https://dev59.com/k2gt5IYBdhLWcg3w8h01#15343916

水平滚动辅助组件:

function horizontalScroll (event) {
  const delta = Math.max(-1, Math.min(1, (event.nativeEvent.wheelDelta || -event.nativeEvent.detail)))
  event.currentTarget.scrollLeft -= (delta * 10)
  event.preventDefault
}

我在需要水平滚动的组件上如何实现它:

<Row className='announcements-home' onWheel={horizontalScroll} >
当我将这个horizontalScroll帮助函数放在React onWheel事件中时,它会水平和垂直滚动。我的期望结果只是水平滚动。此外,Firefox在这些更改中似乎根本不响应水平滚动。
6个回答

19

好的,问题似乎在于您只是引用函数event.preventDefault而没有调用它。在末尾添加一些括号来调用它应该就可以解决问题:event.preventDefault()

然而,我在寻找一些简单的代码时发现了这个问题,所以我也会留下我为此制作的钩子,以帮助其他遇到同样情况的人:

import { useRef, useEffect } from "react";

export function useHorizontalScroll() {
  const elRef = useRef();
  useEffect(() => {
    const el = elRef.current;
    if (el) {
      const onWheel = e => {
        if (e.deltaY == 0) return;
        e.preventDefault();
        el.scrollTo({
          left: el.scrollLeft + e.deltaY,
          behavior: "smooth"
        });
      };
      el.addEventListener("wheel", onWheel);
      return () => el.removeEventListener("wheel", onWheel);
    }
  }, []);
  return elRef;
}

使用方法:

import React from "react";
import { useSideScroll } from "./useSideScroll";

export const SideScrollTest = () => {
  const scrollRef = useHorizontalScroll();
  return (
    <div ref={scrollRef} style={{ width: 300, overflow: "auto" }}>
      <div style={{ whiteSpace: "nowrap" }}>
        I will definitely overflow due to the small width of my parent container
      </div>
    </div>
  );
};

注意: 当尝试进行连续滚动时,“平滑”滚动行为似乎会引起一些问题。可以省略此行为以实现正确的连续滚动,但它看起来会很卡顿。
据我所知,这并没有简单的解决方案。然而,在我的项目中,我创建了一个相当复杂的解决方案,因此认为有些人可能也会欣赏它:https://gist.github.com/TarVK/4cc89772e606e57f268d479605d7aded

也许使用问题中所示的滚轮事件目标可以更容易地实现这一点,但在我的测试中,该目标是 div 的内容而不是容器。因此,元素引用方法对我来说似乎更可靠。 - TarVK
谢谢你的解决方案。它有效了。但是它禁用了触摸板的左右滚动。有什么建议可以调整吗? - Zach
1
@Zach,我以前从未使用过触摸板进行测试,所以不知道这个问题,谢谢您提出来!现在我也可以解决自己项目中的这个问题了 :) 我已经更新了帖子的代码,使用了一个简单的修复方法(如果不是纵向滚动,则不要阻止事件)。我认为如果我们能检测用户是否有横向滚动的方式(例如触摸板),那会很好,这样我们就可以在这种情况下防止任何操作,但目前似乎不可能。 - TarVK
behavior: "smooth" 这部分导致滚动对我来说非常缓慢。删除它后问题解决了。 - undefined

10
onWheel = (e) => {
    e.preventDefault()
    var container = document.getElementById('container')
    var containerScrollPosition = document.getElementById('container').scrollLeft
    container.scrollTo({
        top: 0,
        left: largeContainerScrollPosition + e.deltaY
        behaviour: 'smooth' //if you want smooth scrolling
    })
}

1
这个很好用,但是 e.preventDefault() 会抛出警告,因为它是针对一个被动组件的。你觉得怎么样? - Alan Z

8

TarVK 提出的钩子还存在另一个小问题。当您滚动到底部并继续滚动时,原本应该开始滚动的包含元素并没有滚动。因此,我为此进行了修复:

export function useHorizontalScroll () {
  const elRef = useRef();
  useEffect(() => {
    const el = elRef.current;
    if (el) {
      const onWheel = (e) => {
        if (e.deltaY === 0) return;
        if (
          !(el.scrollLeft === 0 && e.deltaY < 0) &&
          !(el.scrollWidth - el.clientWidth - Math.round(el.scrollLeft) === 0 && 
              e.deltaY > 0)
        ) {
          e.preventDefault();
        }
        el.scrollTo({
          left: el.scrollLeft + e.deltaY,
          behavior: 'smooth'
        });
      };
      el.addEventListener('wheel', onWheel);
      return () => el.removeEventListener('wheel', onWheel);
    }
  }, []);
  return elRef;
}

只有在该方向上有滚动空间时,它才会有条件地防止默认行为,因此当没有滚动空间时,例如整个页面将开始滚动。更改在这里:

        if (
          !(el.scrollLeft === 0 && e.deltaY < 0) &&
          !(el.scrollWidth - el.clientWidth - Math.round(el.scrollLeft) === 0 && 
              e.deltaY > 0)
        ) {
          e.preventDefault();
        }

1
非常好的答案!你可以提出修改TarVK的答案,让它更简洁。 顺便说一下,我更愿意将此函数从组件中分离出来,并声明为带有“ref”作为参数的 const useHorizontalScroll = (ref) => {…},并删除此行const elRef = useRef(); - BonisTech

4

我的声望不够,无法发表评论。

@arVK的回答是正确的,但使用scrollBy而不是scrollTo可以实现更平滑的滚动。

import { useRef, useEffect } from "react";

export function useHorizontalScroll() {
  const elRef = useRef();
  useEffect(() => {
    const el = elRef.current;
    if (el) {
      const onWheel = e => {
        if (e.deltaY == 0) return;
        e.preventDefault();
        el.scrollBy(e.deltaY, 0);
      };
      el.addEventListener("wheel", onWheel);
      return () => el.removeEventListener("wheel", onWheel);
    }
  }, []);
  return elRef;
}

1

您可以直接使用onWheel事件:

import React, { useRef } from "react";
export const Content = () => {
  const ref = useRef<HTMLDivElement>(null);

  const onWheel = (e: UIEvent) => {
    const elelemnt = ref.current;
    if (elelemnt) {
      if (e.deltaY == 0) return;
      elelemnt.scrollTo({
        left: elelemnt.scrollLeft + e.deltaY,
      });
    }
  };
  return (
    <div ref={ref} onWheel={onWheel}>
          TEST CONTENT
    </div>
  );
};

1

我使用了@TarVK提供的答案,效果非常好。只是有一个问题:当输入来自触摸板时,滚动会变得非常不稳定。我的解决方案如下:

import { useEffect, useRef } from 'react';

export function useHorizontalScroll() {
    const elRef = useRef();

    useEffect(() => {
        const el = elRef.current;
        if (el) {
            const onWheel = (e: WheelEvent) => {
                if (e.deltaY === 0) {
                    return;
                }
                if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
                    e.preventDefault();
                    el.scrollLeft += e.deltaX;
                }
                el.scrollTo({ left: el.scrollLeft + e.deltaY });
            };
            el.addEventListener('wheel', onWheel);
            return () => el.removeEventListener('wheel', onWheel);
        }
    }, []);
    return elRef;
}

这个更新的实现检查水平滚动距离(e.deltaX)是否大于垂直滚动距离(e.deltaY),并且只有在前者大于后者时才防止默认行为并水平滚动。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接