react弹窗Modal滚动穿透踩坑及解决(可能是全网最详细了)
最近工作时发现,产品总喜欢做弹窗,还要弹窗里的内容可以滑动。这要是偶尔提一次就算了,我们大不了把页面加个overflow:hidden,弹窗内加scroll呗。但是经常提这种需求,作为程序员可不能重复造轮子呀,所以最近花了一周,把网上的相关文章(看来看去都是抄的),提取出了一个hooks方法,顺便记录踩坑过程。
首先是body的overflow:hidden
单独设这个属性在安卓和pc上确实能做到防止背景滚动,但是在IOS上不行,原因是IOS中,如果html没有设置overflow,body会继承html的overflow,也就是overflow:visible
那就加上呗
html, body {
overflow: hidden;
}
很可惜IOS还是不行,需要加上高度
html, body {
height: 100vh;
overflow: hidden;
}
但这样就会出现,弹窗出现时,背景页面会自己滚动到顶部。因为原body的overflow是visible,没有滚动条的,加了高度和hidden(overflow:hidden下,页面也有滚动条,只是不显示),body里出现了滚动条(滚动距离为0),所以页面置顶。
这个问题可以在弹窗出现前,将html和body的overflow设为auto,这样就不会置顶了。这对于新页面可能是个不错的解决方案,不过如果是旧页面的代码(那些没加overflow:auto的页面),改动可能会很大,而且没法封装。如果有大神用这个解法封装的,请评论教一下小弟,谢谢
event.preventDefault()
这个网上很多文章都说了,会将弹窗内的滚动也禁止掉,当然,这个也是可解的,并且我最后也是基于这个来封装的。
先看看坑,既然不想内部被禁止,那就滚动内部的时候不冒泡呗,
maskRef.current.addEventListener('touchmove', e => {e.preventDefault()}, {passive: false} )
contentRef.current.addEventListenner('touchmove', e => {e.stopPropagation()}, {passive: false})
会发现,内部元素滚动到顶部或者底部,再次继续滚动,背景也会滚动了。
那就加上边界判断吧
let startY
contentRef.current.addEventListener('touchstart', e=> {
startY = e.targetTouches[0].clientY
})
contentRef.current.addEventListenner('touchmove', e => {
// 不考虑横向滑动
const dy = e.changedTouches[0].clientY - startY
const offsetHeight = contentRef.current.offsetHeight
const scrollHeight = contentRef.current.scrollHeight
const scrollTop = contentRef.current.scrollTop
if(dy > 0 && scrollTop === 0) {
return e.preventDefault()
}
if(dy < 0 && scrollTop + offsetHeight >= scrollHgeight) {
return e.preventDefault()
}
e.stopPropagation()
}, {passive: false})
看起来代码没啥问题了,如果按网上其他文章,这就是完美解法了,但是实际体验后发现,根本不够,稍微认真点测试就能发现,在边界情况,如底部,先按住上划再下划,背景就会移动了,顶部也有这种情况。这可就难搞了,因为代码逻辑上已经找不到问题了,但是在浏览器的表现上出现了问题,这种情况目前发现在pc,安卓。IOS我只有一个,暂时没出现这个情况。
body Position:fixed
这是这段时间网上查到文章说的最优解,我一开始先排除了这个解法,因为在仓库的老页面中,有使用window.scrollTo这个方法,将body改为position:fixed,直接导致window.scrollTo无法使用,并且看到网上的文章提到,在页面使用Swiper时会有天坑,IOS的输入框偏移(这两点我并未尝试,因为我没打算用这种方式),这里把网上的代码介绍一下
.bodyNoScroll {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
}
// 解决页面置顶问题
// 弹窗出现调用
let scrollTop = 0
const modalVisible = () => {
scrollTop = document.body.scrollTop
document.body.className = 'bodyNoScroll'
document.body.style.top = scrollTop
}
// 弹窗隐藏调用,还原body的滚动条
const modalHidden = () => {
document.body.className = ''
document.body.scrollTop = scrollTop
}
最终封装的结果
以上三种都有各自的小问题,愁死我了,刚好找到了旭神的文章,里面主要是结合了方法2和方法1
在event.preventDefault() 中,ISO没试出问题,安卓出现了问题, body overflow:hidden中,IOS出现问题,安卓和pc没试出问题。
这不巧了,结合一下不就好了。
因为最近离职了,原代码没法找到,如果有需要的话我再重新写一份