滾動穿透
問題描述
在移動端 WEB 開發的時候(小程序也雷同),如上錄屏所示,如果頁面超過一屏高度出現滾動條時,在 fixed 定位的彈窗遮罩層上進行滑動,它下面的內容也會跟着一起滾動,看起來好像事件穿透到下面的DOM元素上一樣,我們姑且稱之為滾動穿透。
問題原因
能夠猜想是文檔(document)的滾動事件被觸發了,如果能禁用滾動事件就好辦了。
案例偽代碼
<div class="btn">點擊出現彈窗</div>
<div class="popup">
<div class="popup-mask"></div>
<div class="popup-body popup-bottom">
<div class="header">我是標題</div>
<div class="content">
<div>0</div>
<div>1</div>
<div>...</div>
</div>
</div>
</div>
.popup-mask {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
z-index: 998;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.popup-body {
padding: 0 50px 40px;
background-color: #fff;
position: fixed;
z-index: 999;
}
✅ 解決方案A (touch-action)
默認情況下,平移(滾動)和縮放手勢由瀏覽器專門處理,但是可以通過 CSS 特性 touch-action 來改變觸摸手勢的行為。摘取幾個 touch-action 的值如下。
| 值 | 描述 |
|---|---|
| auto | 啓用瀏覽器處理所有平移和縮放手勢。 |
| none | 禁用瀏覽器處理所有平移和縮放手勢。 |
| manipulation | 啓用平移和縮放手勢,但禁用其他非標準手勢,例如雙擊縮放。 |
| pinch-zoom | 啓用頁面的多指平移和縮放。 |
於是在 popup 元素上設置該屬性,禁用元素(及其不可滾動的後代)上的所有手勢就可以解決該問題了。
.popup {
touch-action: none;
}
Note: [無障礙設計] 阻止頁面縮放可能會影響視力不佳的人閲讀和理解頁面內容,不過小程序本身好像就不可以縮放!
✅ 解決方案B (event.preventDefault)
來自 W3C 的一個標準。
大意是説,在 touchstart 和 touchmove 事件中調用 preventDefault 方法可以阻止任何關聯事件的默認行為,包括鼠標事件和滾動。
因此我們可以這樣處理。
Step 1、監聽彈窗最外層元素(popup)的 touchmove 事件並阻止默認行為來禁用所有滾動(包括彈窗內部的滾動元素)。
Step 2、釋放彈窗內的滾動元素,允許其滾動:同樣監聽 touchmove 事件,但是阻止該滾動元素的冒泡行為(stopPropagation),使得在滾動的時候最外層元素(popup)無法接收到 touchmove 事件。
const popup = document.querySelector('.popup')
const scrollBox = document.querySelector('.content')
popup.addEventListener('touchmove', (e) => {
// Step 1: 阻止默認事件
e.preventDefault()
})
scrollBox.addEventListener('touchmove', (e) => {
// Step 2: 阻止冒泡
e.stopPropagation()
})
滾動溢出
問題描述
如上錄屏所示,彈窗內也含有滾動元素,在滾動元素滾到底部或頂部時,再往下或往上滾動,也會觸發頁面的滾動,這種現象稱之為滾動鏈(scroll chaining), 但是感覺滾動溢出(overscroll)這個名字更言辭達意。
❌ 解決方案A (overscroll-behavior)
overscroll-behavior 是 CSS 的一個特性,允許控制瀏覽器滾動到邊界的表現,它有如下幾個值。
| 值 | 描述 |
|---|---|
| auto | 默認效果,元素的滾動可以傳播到祖先元素。 |
| contain | 阻止滾動鏈,滾動不會傳播到祖先元素,但是會顯示節點自身的局部效果。例如 Android 上過度滾動的發光效果或 iOS 上的橡皮筋效果。 |
| none | 與 contain 相同,但是會阻止自身的過度效果。 |
所以可以這樣解決問題:
.content {
overscroll-behavior: none;
}
簡潔乾淨高性能,不過 Safari 全系不支持,兼容性如下,有沒有感覺 Safari 就是現代版的 IE(偶然聽路人説的)!
✅ 解決方案B (event.preventDefault)
借用 event.preventDefault 的能力,當組件滾動到底部或頂部時,通過調用 event.preventDefault 阻止所有滾動,從而頁面滾動也不會觸發了,而在滾動之間則不做處理。
let initialPageY = 0
scrollBox.addEventListener('touchstart', (e) => {
initialPageY = e.changedTouches[0].pageY
})
scrollBox.addEventListener('touchmove', (e) => {
const deltaY = e.changedTouches[0].pageY - initialPageY
// 禁止向上滾動溢出
if (e.cancelable && deltaY > 0 && scrollBox.scrollTop <= 0) {
e.preventDefault()
}
// 禁止向下滾動溢出
if (
e.cancelable &&
deltaY < 0 &&
scrollBox.scrollTop + scrollBox.clientHeight >= scrollBox.scrollHeight
) {
e.preventDefault()
}
})
解決方案完整 Demo
https://github.com/Barrior/ca...