Stories

Detail Return Return

小程序或移動端『滾動穿透』與『滾動溢出』解決方案 - Stories Detail

滾動穿透

滾動穿透.gif

問題描述

在移動端 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 的一個標準。

描述.jpg

大意是説,在 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()
})

滾動溢出

滾動溢出.gif

問題描述

如上錄屏所示,彈窗內也含有滾動元素,在滾動元素滾到底部或頂部時,再往下或往上滾動,也會觸發頁面的滾動,這種現象稱之為滾動鏈(scroll chaining), 但是感覺滾動溢出(overscroll)這個名字更言辭達意。

❌ 解決方案A (overscroll-behavior)

overscroll-behavior 是 CSS 的一個特性,允許控制瀏覽器滾動到邊界的表現,它有如下幾個值。

描述
auto 默認效果,元素的滾動可以傳播到祖先元素。
contain 阻止滾動鏈,滾動不會傳播到祖先元素,但是會顯示節點自身的局部效果。例如 Android 上過度滾動的發光效果或 iOS 上的橡皮筋效果。
none 與 contain 相同,但是會阻止自身的過度效果。

所以可以這樣解決問題:

.content {
  overscroll-behavior: none;
}

簡潔乾淨高性能,不過 Safari 全系不支持,兼容性如下,有沒有感覺 Safari 就是現代版的 IE(偶然聽路人説的)!
兼容性.jpg

✅ 解決方案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...

Add a new Comments

Some HTML is okay.