滚动穿透与滚动溢出
滚动穿透
问题描述:
在移动端开发弹框时(小程序也会出现),当弹框下的页面超过一屏时它下面的内容也会跟着一起滑动,看起来好像事件穿透到了下面的DOM元素一样
问题原因:
通过事件的表象,可以推测是文档的滚动事件被触发了,那我们就禁用滚动事件就好办了。
案例伪代码:
解决方案A✅ e.preventDefault()
小程序——脚本语法,通过e.preventDefault()阻止元素的touch-move事件。(H5同理,监听touch-move事件,阻止其touch-move事件)
<view :class="['asian-popup', classNames]" :style="{ 'z-index': zIndex }" @touchmove="utils.disableScrollEvent" v-if="visible" > <view class="asian-popup-mask" /> <view :class="['asian-popup-content', 'asian-popup-' + position]"> <slot /> </view> </view> function disableScrollEvent(event) { event.preventDefault(); } function enableScrollEvent(event) { event.stopPropagation(); } export default { disableScrollEvent, enableScrollEvent, };
解决方案B✅ (touch-action)
默认情况下,平移(滚动)和缩放手势由浏览器专门处理,但是可以通过 CSS 特性 touch-action 来改变触摸手势的行为。摘取几个 touch-action 的值如下。
值 |
描述 |
auto |
启用浏览器处理所有平移和缩放手势。 |
none |
禁用浏览器处理所有平移和缩放手势。 |
manipulation |
启用平移和缩放手势,但禁用其他非标准手势,例如双击缩放。 |
pinch-zoom |
启用页面的多指平移和缩放。 |
于是在 popup 元素上设置该属性,禁用元素(及其不可滚动的后代)上的所有手势就可以解决该问题了。
<view :class="['asian-popup', classNames]" :style="{ 'z-index': zIndex,touchAction:'none' }" v-if="visible" > <view class="asian-popup-mask" /> <view :class="['asian-popup-content', 'asian-popup-' + position]"> <slot /> </view> </view>
滚动溢出
问题描述:
弹窗内也含有滚动元素,在滚动元素滚到底部或顶部时,再往下或往上滚动,也会触发页面的滚动,这种现象称之为滚动链
解决方案-A✅ (disable-lower/upper-scroll)
支付宝小程序官方提供的 scroll-view 组件,使用 disable-lower-scroll 与 disable-upper-scroll 属性可以解决问题.
背后的原理是,当组件滚动到底部或顶部时,通过调用 event.preventDefault 阻止了所有滚动,从而页面滚动也不会触发了,而在滚动之间则不做处理。
<view :class="['asian-popup', classNames]" :style="{ 'z-index': zIndex }" @touchmove="utils.disableScrollEvent" v-if="visible" > <view class="asian-popup-mask" /> <view :class="['asian-popup-content', 'asian-popup-' + position]"> <scroll-view @touchmove="utils.enableScrollEvent" :style="{maxHeight:contentMaxHeight}" :scroll-y="true" disable-lower-scroll="out-of-bounds" disable-upper-scroll="out-of-bounds" > <slot /> </scroll-view> </view> </view>
解决方案完整 Demo
小程序使用sjs
<template> <view :class="['asian-popup', classNames]" :style="{ 'z-index': zIndex }" @touchmove="utils.disableScrollEvent" v-if="visible" > <view class="asian-popup-mask" /> <view :class="['asian-popup-content', 'asian-popup-' + position]"> <scroll-view @touchmove="utils.enableScrollEvent" :style="{maxHeight:contentMaxHeight}" :scroll-y="true" disable-lower-scroll="out-of-bounds" disable-upper-scroll="out-of-bounds" > <slot /> </scroll-view> </view> </view> </template> <script module="utils" lang="sjs" src="./index.sjs"></script> <script lang="ts"> import { Component, Vue, Prop } from 'vue-property-decorator'; @Component({ components: {}, }) export default class Popup extends Vue { @Prop({ type: String, default: '' }) private classNames; @Prop({ type: String, default: 'center' }) private position; @Prop({ type: Number, default: 998 }) private zIndex; @Prop({ type: Boolean, default: true }) private animations; @Prop({ type: Number, default: 300 }) private duration; @Prop({ type: Boolean, default: true }) private visible; @Prop({ type: String, default: '500rpx' }) private contentMaxHeight } </script> <style lang="less"> @popupPrefix: asian-popup; .@{popupPrefix} { position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 998; &-mask { width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.55); } &-content { position: fixed; background-color: #fff; z-index: 2; padding: 24rpx; display: flex; flex-direction: column; height: auto; } &-top { top: 0; left: 0; right: 0; animation-name: amd-popup-top; border-radius: 0 0 8rpx 8rpx; } &-right { top: 0; right: 0; bottom: 0; animation-name: amd-popup-right; width: 500rpx; } &-bottom { left: 0; right: 0; bottom: 0; animation-name: amd-popup-bottom; border-radius: 8rpx 8rpx 0 0; } &-left { top: 0; left: 0; bottom: 0; animation-name: amd-popup-left; width: 500rpx; } &-center { min-width: 500rpx; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0); animation-name: amd-popup-center; border-radius: 8rpx; } } @keyframes amd-popup-top { 0% { top: -100%; } 100% { top: 0; } } @keyframes amd-popup-bottom { 0% { bottom: -100%; } 100% { bottom: 0; } } @keyframes amd-popup-left { 0% { left: -100%; } 100% { left: 0; } } @keyframes amd-popup-right { 0% { right: -100%; } 100% { right: 0; } } @keyframes amd-popup-center { 0% { transform: translate3d(-50%, -50%, 0) scale(0.1); opacity: 0; } 70% { transform: translate3d(-50%, -50%, 0) scale(1.2); opacity: 1; } 80% { transform: translate3d(-50%, -50%, 0) scale(0.95); } 85% { transform: translate3d(-50%, -50%, 0) scale(1.1); opacity: 0.9; } 95% { transform: translate3d(-50%, -50%, 0) scale(0.97); opacity: 1; } 100% { transform: translate3d(-50%, -50%, 0) scale(1); } } </style>
function disableScrollEvent(event) { event.preventDefault(); } function enableScrollEvent(event) { event.stopPropagation(); } export default { disableScrollEvent, enableScrollEvent, };
使用touch-action: none
<template> <view :class="['asian-popup', classNames]" :style="{ 'z-index': zIndex }" v-if="visible" > <view class="asian-popup-mask" /> <view :class="['asian-popup-content', 'asian-popup-' + position]"> <scroll-view :style="{maxHeight:contentMaxHeight}" :scroll-y="true" disable-lower-scroll="out-of-bounds" disable-upper-scroll="out-of-bounds" > <slot /> </scroll-view> </view> </view> </template> <script lang="ts"> import { Component, Vue, Prop } from 'vue-property-decorator'; @Component({ components: {}, }) export default class Popup extends Vue { @Prop({ type: String, default: '' }) private classNames; @Prop({ type: String, default: 'center' }) private position; @Prop({ type: Number, default: 998 }) private zIndex; @Prop({ type: Boolean, default: true }) private animations; @Prop({ type: Number, default: 300 }) private duration; @Prop({ type: Boolean, default: true }) private visible; @Prop({ type: String, default: '500rpx' }) private contentMaxHeight } </script> <style lang="less"> @popupPrefix: asian-popup; .@{popupPrefix} { position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 998; touch-action: none; &-mask { width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.55); } &-content { position: fixed; background-color: #fff; z-index: 2; padding: 24rpx; display: flex; flex-direction: column; height: auto; } &-top { top: 0; left: 0; right: 0; animation-name: amd-popup-top; border-radius: 0 0 8rpx 8rpx; } &-right { top: 0; right: 0; bottom: 0; animation-name: amd-popup-right; width: 500rpx; } &-bottom { left: 0; right: 0; bottom: 0; animation-name: amd-popup-bottom; border-radius: 8rpx 8rpx 0 0; } &-left { top: 0; left: 0; bottom: 0; animation-name: amd-popup-left; width: 500rpx; } &-center { min-width: 500rpx; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0); animation-name: amd-popup-center; border-radius: 8rpx; } } @keyframes amd-popup-top { 0% { top: -100%; } 100% { top: 0; } } @keyframes amd-popup-bottom { 0% { bottom: -100%; } 100% { bottom: 0; } } @keyframes amd-popup-left { 0% { left: -100%; } 100% { left: 0; } } @keyframes amd-popup-right { 0% { right: -100%; } 100% { right: 0; } } @keyframes amd-popup-center { 0% { transform: translate3d(-50%, -50%, 0) scale(0.1); opacity: 0; } 70% { transform: translate3d(-50%, -50%, 0) scale(1.2); opacity: 1; } 80% { transform: translate3d(-50%, -50%, 0) scale(0.95); } 85% { transform: translate3d(-50%, -50%, 0) scale(1.1); opacity: 0.9; } 95% { transform: translate3d(-50%, -50%, 0) scale(0.97); opacity: 1; } 100% { transform: translate3d(-50%, -50%, 0) scale(1); } } </style>