注册
web

彻底解决PC滚动穿透问题

背景:

之前在做需求的时候,产品有提到一个bug,说是在某些情况下不应该触发外部滚动条滚动,例如鼠标在气泡框的内部就不能产生外部滚动条滚动,这样会影响用户的体验


原效果:


ef821c8d53f71271f871b9fe17630818.jpg

禁止滚动穿透之后效果:


1792757a3d2ca729768f86b8b0ecbda3.jpg

可以看到浏览器的默认行为就是会滚动穿透的,因此我也查找了一些解决方案,但是似乎效果都不太理想,例如最简单有效的方式就是通过设置一个css属性来解决这个问题


overscroll-behavior: contain;

但是呢这个属性实现的效果并不完美,以上方示例的黄色滚动区域为例,如果黄色区域是可以滚动的,那么该属性有效,如果黄色区域不可以滚动,该属性就会失效了,所以没办法只能另寻他法,用JS去解决这个问题


原理:通过监听目标元素内部的滚轮事件,如果内部还有元素可滚动则不做处理,如果内部元素已无法滚动,则禁止滚轮事件冒泡至外部,从而导致无法触发外部滚动条滚动的行为


以下是整体代码,我封装了一个VUE版本的通用HOOKS函数,具体实现大家可参考代码,希望给大家带来帮助!


import { isRef, onMounted, onUnmounted, nextTick } from "vue"

import type { Ref } from "vue"

/**
* 可解析为 DOM 元素的数据源类型
* - 选择器字符串 | Ref | 返回dom函数
*/
type TElement<T> = string | Ref<T> | (() => T)

/**
* HOOKS: 使用滚动隔离
*
* @author dyb-dev
* @date 21/06/2025/ 14:20:34
* @param {(TElement<HTMLElement | HTMLElement[]>)} target 目标元素 `[选择器字符串 | ref对象 | 返回dom函数]`
* @param {TElement<HTMLElement>} [scope=() => document.documentElement] 作用域元素(注意:目标元素为 `选择器字符串` 才奏效) `[选择器字符串 | ref对象 | 返回dom函数]`
*/
export const useScrollIsolate = (
target: TElement<HTMLElement | HTMLElement[]>,
scope: TElement<HTMLElement> = () => document.documentElement
) => {

/** LET: 当前绑定监听器的目标元素列表 */
let _targetElementList: HTMLElement[] = []

/** HOOKS: 挂载钩子 */
onMounted(async() => {

await nextTick()
// 获取目标元素列表
_targetElementList = _getTargetElementList()
// 遍历绑定 `滚轮` 事件
_targetElementList.forEach(_element => {

_element.addEventListener("wheel", _onWheel, { passive: false })

})

})

/** HOOKS: 卸载钩子 */
onUnmounted(() => {

_targetElementList.forEach(_element => {

_element.removeEventListener("wheel", _onWheel)

})

})

/**
* FUN: 获取目标元素列表
* - 字符串时基于作用域选择器查找
*
* @returns {HTMLElement[]} 目标元素列表
*/
const _getTargetElementList = (): HTMLElement[] => {

let _getter: () => unknown

if (typeof target === "string") {

_getter = () => {

const _scopeElement = _getScopeElement()
return _scopeElement ? [..._scopeElement.querySelectorAll(target)] : []

}

}
else {

_getter = _createGetter(target)

}

const _result = _getter()
const _normalized = Array.isArray(_result) ? _result : [_result]
return _normalized.filter(_node => _node instanceof HTMLElement)

}

/**
* FUN: 获取作用域元素(scope)
* - 字符串时使用 querySelector
*
* @returns {HTMLElement | null} 作用域元素
*/
const _getScopeElement = (): HTMLElement | null => {

let _getter: () => unknown

if (typeof scope === "string") {

_getter = () => document.querySelector(scope)

}
else {

_getter = _createGetter(scope)

}

const _result = _getter()
return _result instanceof HTMLElement ? _result : null

}

/**
* FUN: 创建公共 getter 函数
* - 支持 Ref、函数、直接值
*
* @param {unknown} target 目标元素
* @returns {(() => unknown)} 公共 getter 函数
*/
const _createGetter = (target: unknown): (() => unknown) => {

if (isRef(target)) {

return () => (target as Ref<unknown>).value

}
if (typeof target === "function") {

return target as () => unknown

}
return () => target

}

/**
* FUN: 监听滚轮事件
*
* @param {WheelEvent} event 滚轮事件
*/
const _onWheel = (event: WheelEvent) => {

const { target, currentTarget, deltaY } = event
let _element = target as HTMLElement

while (_element) {

// 启用滚动时
if (_isScrollEnabled(_element)) {

// 无法在当前滚动方向上继续滚动时
if (!_isScrollFurther(_element, deltaY)) {

event.preventDefault()

}
return

}

// 向上查找不到滚动元素且到达当前目标元素边界时
if (_element === currentTarget) {

event.preventDefault()
return

}

_element = _element.parentElement as HTMLElement

}

}

/**
* FUN: 是否启用滚动
*
* @param {HTMLElement} element 目标元素
* @returns {boolean} 是否启用滚动
*/
const _isScrollEnabled = (element: HTMLElement): boolean =>
/(auto|scroll)/.test(getComputedStyle(element).overflowY) && element.scrollHeight > element.clientHeight

/**
* FUN: 是否能够在当前滚动方向上继续滚动
*
* @param {HTMLElement} element 目标元素
* @param {number} deltaY 滚动方向
* @returns {boolean} 是否能够在当前滚动方向上继续滚动
*/
const _isScrollFurther = (element: HTMLElement, deltaY: number): boolean => {

/** 是否向下滚动 */
const _isScrollingDown = deltaY > 0
/** 是否向上滚动 */
const _isScrollingUp = deltaY < 0

const { scrollTop, scrollHeight, clientHeight } = element

/** 是否已到顶部 */
const _isAtTop = scrollTop === 0
/** 是否已到底部 */
const _isAtBottom = scrollTop + clientHeight >= scrollHeight - 1

/** 是否还能向下滚动 */
const _willScrollDown = _isScrollingDown && !_isAtBottom
/** 是否还能向上滚动 */
const _willScrollUp = _isScrollingUp && !_isAtTop

return _willScrollDown || _willScrollUp

}

}

作者:dyb
来源:juejin.cn/post/7519695901289267254

0 个评论

要回复文章请先登录注册