javascripthtmlwindow-resizereflowhtml5-fullscreen

Wait for reflow to end after requestFullscreen


Ok guys and gals, here's a tricky one for all you wizards...

I'm working on an immersive web app that overrides the default touch scrolling behavior on mobile devices. The content is divided into pages that use 100% of the viewport and navigation is handled by swiping up and down between pages.

On the first swipe I call requestFullscreen() on the body element which, of course, causes a reflow as the viewport resizes. The problem is that I also want that first swipe to trigger the custom scrolling behavior but I'm using Element.nextElementSibling.scrollIntoView({ block : start, behavior : 'smooth' }) and until the reflow is finished the top edge of the next page (an HTMLSectionElement) is already visible so the scroll doesn't happen.

If I use setTimeout to wait about 600ms until the reflow is finished the scroll effect works as expected but I'm not happy with this hacky workaround and I'd prefer to use a more elegant async solution.

I first tried triggering the scroll effect from inside the the resolve executor of the Promise returned by requestFullscreen but that didn't help. This promise resolves very early on in the execution flow.

Then I tried from inside a fullscreenchange event handler. No luck here either as this event is fired immediately before the fullscreen change happens.

Lastly I tried from inside a window resize event handler but this fires before the reflow happens. I added a requestIdleCallback here too but it didn't make any difference.

So my question is... Is there any reliable way to detect the end of a reflow operation? Or alternatively... does anybody have a better Plan B than giving up on using scrollIntoView and coding my own scroll effect into a window resize handler.


Solution

  • Ok future googlers, I'm back a couple years later with a REAL, non-hacky answer to this problem.

    The trick is to use a two-step requestAnimationFrame chain inside a ResizeObserver. The first callback is triggered right before the reflow occurs. You can use this callback this to make any last-second changes to the DOM. Then, inside this callback, we use requestAnimationFrame again to specify another callback which will happen after the paint from the previous frame.

    You can verify the timing of this method by uncommenting the debugger; lines in the example code below.

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="description" content="React to element size changes after layout reflow has occured.">
        <title> Reflow Observer Test </title>
        <style>
          * { box-sizing: border-box; }
          html, body { height: 100vh; margin: 0; padding: 0; }
          body { background: grey; }
          body:fullscreen { background: orange;}
          body:fullscreen::backdrop { background: transparent; }
          #toggle { margin: 1rem; }
        </style>
      </head>
      <body>
        <button id="toggle"> Toggle Fullscreen </button>
        <script>
          let resizableNode = document.body;
          
          resizableNode.addEventListener('reflow', event => {
            console.log(event.timeStamp, 'do stuff after reflow here');
          });
          
          let observer = new ResizeObserver(entries => {
            for (let entry of entries) {
              requestAnimationFrame(timestamp => {
                // console.log(timestamp, 'about to reflow'); debugger;
                requestAnimationFrame(timestamp => {
                  // console.log(timestamp, 'reflow complete'); debugger;
                  entry.target?.dispatchEvent(new CustomEvent('reflow'));
                });
              });
            }
          });
          
          observer.observe(resizableNode);
          
          function toggleFullscreen(element) {
            if (document.fullscreenElement) {
              document.exitFullscreen();
            } else {
              element.requestFullscreen();
            }
          }
          
          document.getElementById('toggle').addEventListener('click', event => {
            toggleFullscreen(resizableNode);
          });
        </script>
      </body>
    </html>
    

    In this example I'm using going fullscreen on the body element as my trigger but this could easily applied to any resize operation on any DOM node.