Analysis of rolling penetration at mobile end

Time:2020-2-13

Rolling penetration is a very common problem in the development of mobile terminal, which produces strange interaction behavior and affects the user experience. At the same time, it makes our products look less “professional”. Although many products choose to tolerate this kind of behavior, as engineers who pursue the ultimate, they should understand why and how to solve it.

What is rolling penetration

In the development of mobile terminal, we can’t avoid such operations as pop-up window and adding floating layer on the page. One of the most common scenes is that there is a mask layer on the whole page, with all kinds of things painted on it. We don’t discuss what it is. It’s hard to implement such a mask layer even if it’s a little white at the beginning of writing the front end. But there’s a problem here. If you don’t do anything with the mask layer, when you slide on it, you will find that the page under the mask layer is scrolling, which is very interesting. As in the following example, one is calledmaskThe length and width are screen size mask layers. When we slide on the top, the content below is also following the scrolling, that is to say, scrolling “penetrates” to the bottom, which is scrolling – chaining.

Analysis of rolling penetration at mobile end

The bottom of the mask layer of the upper demo is a content container that gradually turns blue, but when sliding the upper mask layer, the bottom also follows the scroll, which is just the simplest scene. We will discuss more complex situations later.

Why does it happen

At present, there will be a lot of articles about how to solve this problem in Google search and roll penetration, but they are all telling you how to solve this problem and how to hack this interaction exception. It doesn’t tell the reader why this behavior occurs, or even think it’s a bug in the browser. It’s hard for me to understand, because even if we solve the problem, we don’t know what the root of the problem is.

lump-sum illusion

There is a mistake that we set up a mask layer the same size as the screen to cover the following content. In principle, we should be able to block all the events below, that is, it is impossible to trigger the scrolling of the following content. So let’s take a look at the specification and when it will trigger scrolling.

// https://www.w3.org/TR/2016/WD…
When asked to run the scroll steps for a Document doc, run these steps:

  1. For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps:
  • If target is a Document, fire an event named scroll that bubbles at target.
  • Otherwise, fire an event named scroll at target.
  1. Empty doc’s pending scroll event targets.

Through the specification, we can understand that the first rolling target can be document and the element in it. Secondly, the scroll event on element does not bubble, and the scroll event on document bubbles.

So if we want to solve the problem by preventing its rolling event bubble on the scroll node, it is not feasible! Because it doesn’t bubble at all, it can’t touch the parent nodes of DOM tree to trigger their scrolling.

So how does the problem arise? In fact, the specification only states when the browser should scroll, not when it should not. The browser implements the specification correctly, and roll through is not a bug of the browser. We add a mask layer to the page without affecting the generation of document scrolling events. According to the specification, if the target node can’t scroll, it will try to scroll on the document. That is to say, although the mask layer can’t scroll, the browser will trigger the scrolling of the document at this time, which results in the scrolling of the document below. That is to say, if the document cannot be scrolled, there will be no such problem. This leads to the first solution: set the document to overflow hidden.

How to solve it?

overflow hidden

Since the scrolling is due to the fact that the document exceeds one screen, it’s good to let the part beyond it drop, so when the mask layer is popped up, you can set a class for HTML and body Tags:

.modal--open {
  height: 100%;
  overflow: hidden;
}

In this way, the height of the document is the same as that of the screen, so there will be no scrolling. But this will lead to a new problem. If there is a certain rolling height before the document, the previous rolling distance will be invalid and the document will roll back to the top. Isn’t it worth the loss? But we can record the previous scrolling details before adding class and set the scrolling distance back when closing the mask layer. This problem can be solved at a low implementation cost, but if the mask layer is transparent, the user will still see the lower page after the loss of distance after pop-up, obviously this is not a perfect solution.

prevent touch event

Another way is to prevent the touch event of the mask layer and pop-up window from triggering the scroll event on the mobile terminal. However, there is no touch event on the PC, and scroll event can still be triggered. As we said above, scroll event is an element that can scroll. Here we solve the problem of mobile terminal, for example:

Analysis of rolling penetration at mobile end

<div id="app">
  <div class="mask">mask</div>
  <div class="dialog">dialog</div>
</div>
const $mask = document.querySelector(".mask");
const $dialog = document.querySelector(".dialog");
const preventTouchMove = $el => {
  $el.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    { passive: false }
  );
};
preventTouchMove($mask);
preventTouchMove($dialog);

Above, we use prevent touch move to prevent the touch event of the page and further page scrolling. In the last parameter of addeventlistener, we set passive display to false, which is intentional here. As for passive event listener, here’s another topic we won’t expand. It’s the improvements made by the browser to optimize the scrolling performance. For details, you can see that the website uses passive event listener to improve the scrolling performance. Since passive event listener will be turned on by default at the beginning of chrome 56, it can’t be used directly in touch events Preventdefault. You need to set the passive option to false first.

Here we have solved the problem of ordinary pop-up window on the page, but if the content of dialog can be scrolled, so that the touch event is blocked, the content will not scroll normally, so further optimization is needed.

Further optimization

Now the scene is that our pop-up window can be scrolled, so we can’t directly block its touch event. After removing it, we find that there will be new problems. The mask layer is blocked from rolling down the touch event, but the pop-up layer, modal, is scrollable. When touching modal, the content can be scrolled normally. However, when the modal scrolls to the top or the bottom, the scrolling of document can still be triggered. The effect is as follows:

Analysis of rolling penetration at mobile end

We see that when modal scrolls at the top, it can still drag the document below. In this way, we can only monitor the user gestures. If the modal has already slipped to the bottom or the top and has to slide up or down, we also need to prevent the touch event of the modal. Simply implement afuckScrollChainingFunction:

function fuckScrollChaining($mask, $modal) {
  const listenerOpts = { passive: false };
  $mask.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    listenerOpts
  );
  const modalHeight = $modal.clientHeight;
  const modalScrollHeight = $modal.scrollHeight;
  let startY = 0;

  $modal.addEventListener("touchstart", e => {
    startY = e.touches[0].pageY;
  });
  $modal.addEventListener(
    "touchmove",
    e => {
      let endY = e.touches[0].pageY;
      let delta = endY - startY;

      if (
        ($modal.scrollTop === 0 && delta > 0) ||
        ($modal.scrollTop + modalHeight === modalScrollHeight && delta < 0)
      ) {
        e.preventDefault();
      }
    },
    listenerOpts
  );
}

The complete implementation is here, so no matter whether the content of the pop-up layer can be scrolled or not, the document below will not follow the scrolling.

Welcome to discuss https://github.com/jiavan/blo