A scheme of preventing page scrolling by masking

Posted by HiddenS3crets on Mon, 10 Jan 2022 12:50:50 +0100

A scheme of preventing page scrolling by masking

Pop up window is a common way of interaction, and the mask is an essential element of pop-up window. It is used to separate the page from the pop-up window block and temporarily block the interaction of the page. However, when the mask appears, the page at the bottom of the mask will start to scroll if it is not processed. In fact, we don't want it to scroll, so we need to prevent this behavior. When the mask pops up, the page scrolling under the mask is prohibited, which can also be called the problem of scrolling penetration. This paper introduces some common solutions.

realization

First, we need to implement an example of the effect of scrolling under the mask. When we click the pop-up button to display the mask, and then scroll the mouse, we can see that the pages under the mask can still scroll. If you scroll inside the mask and continue to scroll when the scroll bar in the mask scrolls to the bottom, the pages under the mask can also scroll. This interaction is chaotic. The test environment of the content in this paper is Chrome 96.0.4664.110.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>A scheme of preventing page scrolling by masking</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">Popup</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

This scheme is a common scheme, that is, when opening the mask, add overflow: hidden; to the body;, This style is removed when the mask is closed, such as the login pop-up window of Sino and the Modal dialog box of antd. The advantage of this scheme is simple and convenient. It only needs to add css style without complex logic. The disadvantage is that the adaptability of the mobile terminal is poor. Some Android models and safari cannot prevent the bottom page from scrolling. In addition, some models may need to add overflow: hidden to the root node < HTML >; The style is effective. In addition, because the content of the page is actually cropped, the scroll bar will disappear when setting this style, and the scroll bar will appear when removing the style, so there will be a certain flicker visually. Of course, the style of the scroll bar can also be customized, but the scroll bar style is another compatibility problem, And also because of cutting.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>A scheme of preventing page scrolling by masking</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">Popup</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

The effect of the above scheme on the mobile terminal is not very ideal. If it needs to be processed on the mobile terminal, you can use the touch event on the mobile terminal to prevent the default behavior. Of course, this is the way applicable to the mobile terminal. In addition, if the mobile phone is connected to the mouse through Bluetooth or adapter line, it is another matter. If the mask content does not have a scroll bar, the above method is no problem, but if the mask content has a scroll bar, it can no longer move. Therefore, if there are elements in the mask that need to scroll, we need to use Js to control its logic, but the logic control is more complex. We can judge the event of the event Target element. If the target of the touch is the non scrollable area of the pop-up window, that is, the background mask, the default event will be disabled. Otherwise, there will be no control. Later, there will be a problem. It needs to be judged that scrolling is prohibited when scrolling to the top and bottom. Otherwise, if you touch the upper and lower ends, the scroll bar of the scrollable area of the pop-up window will reach the top or bottom and still penetrate the body, Make the body scroll with the pop-up window, so the complexity of logic is relatively high.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>A scheme of preventing page scrolling by masking</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">Popup</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; // Record the first press ` 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); //The position of the mouse during this movement is used for calculation
                let scrollTop = scrollerContainer.scrollTop; // Current scrolling distance
                let scrollHeight = scrollerContainer.scrollHeight; // Height of scrollable area
                let containerHeight = scrollerContainer.clientHeight; //Height of visual area
                if (scrollTop <= 0 && newTargetY - targetY > 0) { // To the top
                    console.log("To the top");
                    if(e.cancelable)  e.preventDefault(); // You must judge 'cancelable', otherwise it is prone to 'error' that scrolling is in progress and cannot be cancelled`
                } else if (scrollTop >= scrollHeight - containerHeight && newTargetY - targetY < 0 ) { // to the end
                    console.log("to the end");
                    if(e.cancelable) e.preventDefault(); // You must judge 'cancelable', otherwise it is prone to 'error' that scrolling is in progress and cannot be cancelled`
                }
            }
            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

This is the commonly used scheme at present. To prevent the page from scrolling, you can fix it in the view, that is, position: fixed, so that it cannot scroll. Release it when the mask is closed. Of course, there are some details to consider. After fixing the page in the view, the content will go back to the top. Here we need to record the top value used to synchronize, In this way, we can get a more perfect scheme compatible with mobile terminal and PC terminal. Of course, for the api compatibility of browser, we use document documentElement. Scrolltop control or window pageYOffset + window. The scrollto control needs to be adapted separately. In the example, in order to demonstrate that pop-up will not cause the view to reset to the top, the pop-up button is moved to the bottom.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>A scheme of preventing page scrolling by masking</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">Popup</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; // Record when button is pressed ` 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>

One question per day

https://github.com/WindrunnerMax/EveryDay

reference resources

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

Topics: html