一个移动端弹框组件滚动穿透的解决方案

由 漆黑菌 于 2019年05月22日 发布

业务场景描述

组内有一个自制的vue弹框组件,就是出一个弹框然后周围都是半透明黑色遮罩的那种。之前的业务场景中弹框中的内容都不需要滚动,因此滚动穿透问题并没有得到重视,然后这个坑我就掉进去了……前人挖坑后人跳坑(确信。组件中原本的解决方案是在组件外层添加一个事件监听,阻止默认的touchmove事件冒泡,这样弹框中需要滚动的部分也不能滚动了,而且好像在iOS上也对穿透到body滚动的问题没啥效果。

尝试解决方案一

在原有的方案上做改进,传入一个class白名单,发现白名单的子元素或自身(使用contains方法判断)触发的touchmove事件就不阻止默认动作了,然后在要滚动的白名单class元素上增加CSSoverscroll-behavior: none;规则,使其冒泡到顶层的事件不能触发外层的浏览器默认动作。

问题

只在新版Android的chrome上有用,因为这个属性不能在Safari上生效,在IE10及以上上这个规则都有私有实现……迫真新时代IE6,可用列表详见caniuse

尝试解决方案二

在询问谷歌老师以后发现了文章移动端滚动穿透问题,文章进行了较好的总结,在其最后提供的方法上进行了小小的改进。在屏蔽滚动时,项目原有的body宽度规则会被出现问题,于是手动指定了在屏蔽和恢复时的body的宽度。

    // 打开弹窗时屏蔽滚动
    const scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    document.body.style.cssText += `position: fixed;top: -${scrollTop}px;width: 100vw;`;

    // 关闭弹窗时恢复滚动
    const body = document.querySelector('body');
    body.style.position = '';
    // 注意恢复原有宽度
    body.style.width = '100%';
    const top = parseInt(body.style.top, 10);
    // 重置body滚动高度
    document.body.scrollTop = -top;
    // 重置html滚动高度
    document.documentElement.scrollTop = -top;
    body.style.top = '';

尝试解决方案三

以上解决方案有一个问题就是在iOS上,似乎是会把滚动事件绑定到“手指焦点”的元素上,如果用户手指按到不需要滚动的元素上,还要手动把焦点切回去才能继续滚动滚动。在继续询问谷歌老师后发现了这篇文章移动端踩坑之滚动穿透,这位老哥提出了

touchmove 事件中调用 preventDefault,将 EventTarget 从 body 改为日期控件的dom。

这个解法,而且因为我现在的组件中已经有个之前尝试填坑用的可滚动白名单class。于是在这位老哥的基础上,在body上添加新的监听,把不在白名单的dom触发的touchmove事件全部重新分发到白名单dom上,目前效果似乎还不错?(不要忘记在关闭的时候移除这个事件的监听

    const touchmove = document.createEvent('TouchEvent');
    // 可以冒泡,可以被取消
    touchmove.initEvent('touchmove', true, true);
    movableNode.dispatchEvent(touchmove);