如何禁止子元素滚动触发父元素滚动

5,787 阅读2分钟

行为描述

当子元素和父元素属于嵌套关系的时候,滑动子元素至顶部/底部,会接着触发父元素的滚动。

在某些情景下需要禁掉父元素的滚动行为。

解决方案

overflow:hidden

鼠标划入子元素的时候,设置父元素css的overflow:hidden

const usePreventRootScroll = ({ rootDom, targetDom }) => {
  let previousBodyOverflowSetting, previousBodyPaddingRight
  const setOverflowHidden = () => {
    const scrollWidth = rootDom.offsetWidth - rootDom.clientWidth
    previousBodyOverflowSetting = targetDom.style.overflow
    rootDom.style.overflow = 'hidden'
    previousBodyPaddingRight = targetDom.style.paddingRight
    rootDom.style.paddingRight = `${scrollWidth}px`
  }
  const clearOverflowHidden = () => {
    if (previousBodyOverflowSetting !== undefined) {
      rootDom.style.overflow = previousBodyOverflowSetting
    }
    if (previousBodyPaddingRight !== undefined) {
      rootDom.style.paddingRight = previousBodyPaddingRight
    }
  }

  React.useEffect(() => {
    if (rootDom && targetDom) {
      targetDom.addEventListener('mouseenter', setOverflowHidden)
      targetDom.addEventListener('mouseleave', clearOverflowHidden)
    }
    return () => {
      if (rootDom && targetDom) {
        targetDom.removeEventListener('mouseenter', setOverflowHidden)
        targetDom.removeEventListener('mouseleave', clearOverflowHidden)
      }
    }
  }, [])
}

缺陷

  1. 移动端需要兼容,特别是安卓。

  2. 切换overflow属性导致滚动条显示/隐藏状态的切换,带来的页面轻微抖动。( windows 上面,返回的滚动条宽度不准确)

overscroll-behavior

设置子元素的css属性 overscroll-behavior :contain

缺陷

  1. 移动端兼容性问题巨大; safari / ie 团灭

监听 wheel 事件

 const usePreventRootScroll = ({ rootDom, targetDom }) => {
  // 阻止自元素滚动触底之后父元素滚动
  const firstDeltaY = React.useRef(0)
  const setScrollTop = React.useMemo(() => {
    const debounceSetScrollTop = debounce(value => {
      targetDom.scrollTop = value
    }, 50)
    return debounceSetScrollTop
  }, [targetDom])
  const setFirstDeltaY = value => {
    if (value !== 'init') {
      firstDeltaY.current = value
    } else {
      firstDeltaY.current = 0
    }
  }
  const changePreventScroll = event => {
    const { deltaY } = event
    const { scrollTop, clientHeight, scrollHeight } = targetDom
    // 如果是第一次,则直接阻止,后续,判断滚轮+ - 状态,不相同,则不阻止。
    if (targetDom && targetDom.contains(event.target)) {
      if (scrollTop === 0 || Math.abs(scrollTop + clientHeight - scrollHeight) < 1) {
        if (firstDeltaY.current) {
          const value = firstDeltaY.current
          const checkNumberType = num => {
            if (num === 0) {
              return 0
            }
            if (num < 0) {
              return '-'
            } else {
              return '+'
            }
          }
          if (checkNumberType(value) === checkNumberType(deltaY)) {
            event.preventDefault()
          } else {
            // 如果滚轮 deltaY === 0 的话,则进行矫正。
            const offset = Math.max(Math.abs(deltaY), 7) // 小于 5 的话,会有抖动。
            setScrollTop(scrollTop === 0 ? offset : scrollTop - offset)
            setFirstDeltaY('init')
          }
        } else {
          setFirstDeltaY(deltaY)
          event.preventDefault()
        }
      } else {
        setFirstDeltaY('init')
      }
    }
  }
  React.useEffect(() => {
    if (rootDom && targetDom) {
      rootDom.addEventListener('wheel', changePreventScroll)
    }
    return () => {
      if (rootDom && targetDom) {
        rootDom.removeEventListener('wheel', changePreventScroll)
      }
    }
  }, [])
  return [setFirstDeltaY]
}

缺陷

  1. 由于只是监听滚轮,如果是通过js设置scrollTop直接到底部,那么就会锁死滚轮事件;

  2. 判断是否解锁滚轮事件,取决于滚轮的方向,而滚轮方向是可以用户设置的,目前并没有方式能够取到用户向上滚动滚轮,视窗是否应该向上滚动。

监听 touchMove , 与wheel 雷同。