解决react弹窗滚动穿透
经常写H5项目的小伙伴,可能会遇到的的问题———弹窗的滚动穿透。
写了一个自定义hooks 来解决这个小问题。
话不多讲先贴代码
/**
* 阻止弹窗滚动穿透
* @param direction 'vertical' | 'horizontal' 滚动方向
* @param deps 依赖
*/
export function useScrollPreventDefault(direction: 'vertical' | 'horizontal' = 'vertical', deps: any[] = []) {
const parentScrollBox = useRef<HTMLDivElement>(null);
const scrollBox = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!scrollBox.current) {
return;
}
const _scrollBox = scrollBox.current;
let initialPageX = 0;
let initialPageY = 0;
const handleTouchStart = (e: TouchEvent) => {
initialPageX = e.changedTouches[0].pageX;
initialPageY = e.changedTouches[0].pageY;
};
const handleTouchMove = (e: TouchEvent) => {
// 阻止事件冒泡到父元素导致滚动失效
e.stopPropagation();
};
const handlePreventTouchMove = (e: TouchEvent) => {
// 禁止多指滚动操作
if (e.changedTouches.length > 1) {
e.preventDefault();
} else {
const deltaX = e.changedTouches[0].pageX - initialPageX;
const deltaY = e.changedTouches[0].pageY - initialPageY;
// 横向滚动
if (direction === 'horizontal') {
if (
e.cancelable &&
((deltaX > 0 && _scrollBox.scrollLeft <= 0) ||
(deltaX < 0 && _scrollBox.scrollLeft + _scrollBox.clientWidth >= _scrollBox.scrollWidth))
) {
e.preventDefault();
}
if (e.cancelable && Math.abs(deltaX) < Math.abs(deltaY)) {
e.preventDefault();
}
}
// 垂直滚动
if (direction === 'vertical') {
if (
e.cancelable &&
((deltaY > 0 && _scrollBox.scrollTop <= 0) ||
(deltaY < 0 && _scrollBox.scrollTop + _scrollBox.clientHeight >= _scrollBox.scrollHeight))
) {
e.preventDefault();
}
if (e.cancelable && Math.abs(deltaX) > Math.abs(deltaY)) {
e.preventDefault();
}
}
}
};
_scrollBox.addEventListener('touchstart', handleTouchStart);
_scrollBox.addEventListener('touchmove', handleTouchMove);
_scrollBox.addEventListener('touchmove', handlePreventTouchMove);
return () => {
_scrollBox.removeEventListener('touchstart', handleTouchStart);
_scrollBox.removeEventListener('touchmove', handleTouchMove);
_scrollBox.removeEventListener('touchmove', handlePreventTouchMove);
};
}, [...deps]);
useEffect(() => {
if (!parentScrollBox.current) {
return;
}
const _parentScrollBox = parentScrollBox.current;
const handleTouchMove = (e: TouchEvent) => {
e.preventDefault();
};
_parentScrollBox.addEventListener('touchmove', handleTouchMove);
return () => {
_parentScrollBox.removeEventListener('touchmove', handleTouchMove);
};
}, [...deps]);
return {
parentScrollBox,
scrollBox,
};
}
demo使用
先创建一个demo
.testModal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
}
.testModal__content {
width: 100%;
height: 1000rpx;
position: absolute;
left: 0;
bottom: 0;
background-color: #fff;
}
.testModal__nav {
width: 100%;
height: 80rpx;
overflow-x: scroll;
overflow-y: hidden;
position: relative;
}
.testModal__navWrap {
width: auto;
height: 100%;
display: flex;
align-items: center;
box-sizing: border-box;
border: 2rpx solid red;
position: absolute;
left: 0;
top: 0;
}
.testModal__navItem {
width: 150rpx;
height: 100%;
flex-shrink: 0;
display: inline-flex;
justify-content: center;
align-items: center;
}
.testModal_detail {
width: 100%;
height: 300rpx;
overflow-x: hidden;
overflow-y: auto;
position: relative;
margin-top: 30rpx;
}
.testModal__detailWrap {
width: 100%;
height: auto;
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
border: 2rpx solid red;
}
function TestModal() {
const { parentScrollBox, scrollBox } = useScrollPreventDefault('vertical');
const { scrollBox: scrollContentBox } = useScrollPreventDefault('horizontal');
return (
<div className="testModal" ref={parentScrollBox}>
<div className="testModal__content">
<div className="testModal__nav" ref={scrollBox}>
<div className="testModal__navWrap">
{new Array(10).fill('').map((item, idx) => {
return <div className="testModal__navItem">{`${idx}`.repeat(3)}</div>;
})}
</div>
</div>
<div className="testModal_detail" ref={scrollContentBox}>
<div className="testModal__detailWrap">
{new Array(20).fill('').map((item, idx) => {
return <div className="testModal__detailItem">{`${idx}`.repeat(3)}</div>;
})}
</div>
</div>
</div>
</div>
);
}