蒙層禁止頁面滾動的方案
蒙層禁止頁面滾動的方案
彈窗是一種常見的互動方式,而蒙層是彈窗必不可少的元素,用於隔斷頁面與彈窗區塊,暫時阻斷頁面的互動。但是在蒙層出現的時候滾動頁面,如果不加處理,蒙層底部的頁面會開始滾動,實際上我們是不希望他進行滾動的,因此需要阻止這種行為。當彈出蒙層時禁止蒙層下的頁面滾動,也可以稱為滾動穿透的問題,文中介紹了一些常用的解決方案。
實現
首先需要實現一個蒙層下滾動的效果示例,當我們點選彈窗按鈕顯示蒙層之後,再滾動滑鼠的話能夠看到蒙層下的頁面依舊是能夠滾動的。如果在蒙層的內部進行滾動,當蒙層內滾動條滾動到底部的時候再繼續滾動的話,蒙層下的頁面也是能夠滾動的,這樣的互動就比較混亂,文中內容的測試環境是Chrome 96.0.4664.110
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>蒙層禁止頁面滾動的方案</title> <style type="text/css"> #mask{ position: fixed; height: 100vh; width: 100vw; background: rgba(0, 0, 0, 0.6); top: 0; left: 0; display: flex; align-items: center; justify-content: center; } .hide{ display: none !important; } .long-content > div{ height: 300px; } .mask-content{ width: 300px; height: 100px; overflow-x: auto; background: #fff; } .mask-content > div{ height: 300px; } </style> </head> <body> <button id="btn">彈窗</button> <div class="long-content"> <div>long content</div> <div>long content</div> <div>long content</div> <div>long content</div> <div>long content</div> <div>long content</div> <div>long content</div> </div> <div id="mask" class="hide"> <div class="mask-content"> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> <div>mask-content</div> </div> </div> </body> <script type="text/javascript"> (() => { const btn = document.getElementById("btn"); const mask = document.getElementById("mask"); btn.addEventListener("click", e => { mask.classList.remove("hide"); }) mask.addEventListener("click", e => { mask.classList.add("hide"); }) })(); </script> </html>
body hidden
此方案是一種比較常用的方案,即打開蒙層時給body
新增overflow: hidden;
,在關閉蒙層時就移除這個樣式,例如思否的登入彈窗、antd
的Modal
對話方塊就是這樣的方式。 這種方案的優點是簡單方便,只需新增css
樣式,沒有複雜的邏輯。缺點是在移動端的適配性差一些,部分安卓機型以及safari
中,無法阻止底部頁面滾動,另外有些機型可能需要給根節點<html>
新增overflow: hidden;
樣式才有效果,此外由於實際上是將頁面的內容給裁剪了,所以在設定這個樣式的時候滾動條會消失,而移除樣式的時候滾動條又會出現,所以在視覺上是會有一定的閃爍現象的,當然也可以定製滾動條的樣式,但滾動條樣式就是另一個相容性的問題了,還有同樣是因為裁剪。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>蒙層禁止頁面滾動的方案</title>
<style type="text/css">
#mask{
position: fixed;
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.6);
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hide{
display: none !important;
}
.long-content > div{
height: 300px;
}
.body-overflow-hidden{
overflow: hidden;
}
.mask-content{
width: 300px;
height: 100px;
overflow-x: auto;
background: #fff;
}
.mask-content > div{
height: 300px;
}
</style>
</head>
<body>
<button id="btn">彈窗</button>
<div class="long-content">
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
</div>
<div id="mask" class="hide">
<div class="mask-content">
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
</div>
</div>
</body>
<script type="text/javascript">
(() => {
const btn = document.getElementById("btn");
const mask = document.getElementById("mask");
const body = document.body;
btn.addEventListener("click", e => {
mask.classList.remove("hide");
body.classList.add("body-overflow-hidden");
})
mask.addEventListener("click", e => {
mask.classList.add("hide");
body.classList.remove("body-overflow-hidden");
})
})();
</script>
</html>
touch preventDefault
上邊的方案對於移動端的效果不是很理想,如果需要在移動端進行處理的話,可以利用移動端的touch
事件,來阻止預設行為,當然這是適用於移動端的方式,另外要是把手機通過藍芽也好轉接線也好接上滑鼠的話,那就是另一回事了。假如蒙層內容不會有滾動條,那麼上述方法是沒有問題的,但是假如蒙層內容有滾動條的話,那麼它再也無法動彈了。所以如果在蒙層內部有元素需要滾動的話,需要用Js
控制其邏輯,但是邏輯控制起來又是比較複雜的,我們可以判斷事件的event.target
元素,如果touch
的目標是彈窗不可滾動區域即背景蒙層就禁掉預設事件,反之就不做控制,之後又出現了問題,需要判斷滾動到頂部和滾動到底部的時候禁止滾動,否則觸碰到上下兩端,彈窗可滾動區域的滾動條到了頂部或者底部,依舊穿透到body
,使得body
跟隨彈窗滾動,這樣的話邏輯的複雜程度就比較高了。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>蒙層禁止頁面滾動的方案</title>
<style type="text/css">
#mask{
position: fixed;
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.6);
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hide{
display: none !important;
}
.long-content > div{
height: 300px;
}
.mask-content{
width: 300px;
height: 100px;
overflow-x: auto;
background: #fff;
}
.mask-content > div{
height: 300px;
}
</style>
</head>
<body>
<button id="btn">彈窗</button>
<div class="long-content">
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
</div>
<div id="mask" class="hide">
<div class="mask-content">
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
</div>
</div>
</body>
<script type="text/javascript">
(() => {
const btn = document.getElementById("btn");
const mask = document.getElementById("mask");
const body = document.body;
const scrollerContainer = document.querySelector(".mask-content");
let targetY = 0; // 記錄下第一次按下時的`clientY`
scrollerContainer.addEventListener("touchstart", e => {
targetY = Math.floor(e.targetTouches[0].clientY);
});
const touchMoveEventHandler = e => {
if(!scrollerContainer.contains(e.target)) {
e.preventDefault();
}
let newTargetY = Math.floor(e.targetTouches[0].clientY); //本次移動時滑鼠的位置,用於計算
let scrollTop = scrollerContainer.scrollTop; // 當前滾動的距離
let scrollHeight = scrollerContainer.scrollHeight; // 可滾動區域的高度
let containerHeight = scrollerContainer.clientHeight; //可視區域的高度
if (scrollTop <= 0 && newTargetY - targetY > 0) { // 到頂
console.log("到頂");
if(e.cancelable) e.preventDefault(); // 必須判斷`cancelable` 否則容易出現滾動正在進行無法取消的`error`
} else if (scrollTop >= scrollHeight - containerHeight && newTargetY - targetY < 0 ) { // 到底
console.log("到底");
if(e.cancelable) e.preventDefault(); // 必須判斷`cancelable` 否則容易出現滾動正在進行無法取消的`error`
}
}
btn.addEventListener("click", e => {
mask.classList.remove("hide");
body.addEventListener("touchmove", touchMoveEventHandler, { passive: false });
})
mask.addEventListener("click", e => {
mask.classList.add("hide");
body.removeEventListener("touchmove", touchMoveEventHandler);
})
})();
</script>
</html>
body fixed
目前常用的方案就是該方案了,要阻止頁面滾動,可以將其固定在檢視中即position: fixed
,這樣它就無法滾動了,當蒙層關閉時再釋放,當然還有一些細節要考慮,將頁面固定檢視後內容會回頭最頂端,這裡我們需要記錄一下用來同步top
值,這樣就可以得到一個相容移動端與PC
端的較為完善的方案了,當然對於瀏覽器的api
相容性是使用document.documentElement.scrollTop
控制還是window.pageYOffset + window.scrollTo
控制就需要另行適配了。在示例中為了演示彈窗時不會導致檢視重置到最頂端,將彈窗按鈕移動到了下方。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>蒙層禁止頁面滾動的方案</title>
<style type="text/css">
#mask{
position: fixed;
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.6);
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hide{
display: none !important;
}
.long-content > div{
height: 300px;
}
.mask-content{
width: 300px;
height: 100px;
overflow-x: auto;
background: #fff;
}
.mask-content > div{
height: 300px;
}
</style>
</head>
<body>
<div class="long-content">
<div>long content</div>
<div>long content</div>
<div>long content</div>
<button id="btn">彈窗</button>
<div>long content</div>
<div>long content</div>
<div>long content</div>
<div>long content</div>
</div>
<div id="mask" class="hide">
<div class="mask-content">
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
<div>mask-content</div>
</div>
</div>
</body>
<script type="text/javascript">
(() => {
const btn = document.getElementById("btn");
const mask = document.getElementById("mask");
const body = document.body;
let documentTop = 0; // 記錄按下按鈕時的 `top`
btn.addEventListener("click", e => {
mask.classList.remove("hide");
documentTop = document.scrollingElement.scrollTop;
body.style.position = "fixed"
body.style.top = -documentTop + "px";
})
mask.addEventListener("click", e => {
mask.classList.add("hide");
body.style.position = "static";
body.style.top = "auto";
document.scrollingElement.scrollTop = documentTop;
})
})();
</script>
</html>
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://zhuanlan.zhihu.com/p/373328247
https://ant.design/components/modal-cn/
https://juejin.cn/post/6844903519636422664
https://segmentfault.com/a/1190000038594173
https://www.cnblogs.com/padding1015/p/10568070.html
https://blog.csdn.net/licanty/article/details/86590360
https://blog.csdn.net/xiaonuanli/article/details/81015131