lenis icon indicating copy to clipboard operation
lenis copied to clipboard

Idea: Auto wrapper and content detection

Open drewbaker opened this issue 2 years ago • 11 comments
trafficstars

We've built similar things to Lenis before, and the way we did it was to detect which element was trying to be scrolled, the same way your browser would do it. So rather than having settings like wrapper and content we would do something like the below code on mouse wheel event.

The big advantage to this approach was we could enable it once, and not have to worry about init/destroy on modals/overlays etc... My guess is 99% of people are using Lenis to replace scrolling on the entire site, not just one scrolling element.

Anyway, I thought this might help and inspire a new wrapper setting of auto perhaps.

/**
 * Figures out if an element has scrollbars
 * https://stackoverflow.com/a/42681820/503546
 * @param {HTMLElement} element
 * @returns {object}
 */
function isScrollable(el) {
  const style = getComputedStyle(el);

  const hidden = style.overflow === "hidden";
  const xHidden = style.overflowX === "hidden";
  const yHidden = style.overflowY === "hidden";

  // If overflow:hidden, then no scorll bars ever
  if (hidden) {
    return {
      x: false,
      y: false
    };
  }

  // Calculate if element is overflowing
  var y1 = el.scrollTop;
  el.scrollTop += 1;
  var y2 = el.scrollTop;
  el.scrollTop -= 1;
  var y3 = el.scrollTop;
  el.scrollTop = y1;
  var x1 = el.scrollLeft;
  el.scrollLeft += 1;
  var x2 = el.scrollLeft;
  el.scrollLeft -= 1;
  var x3 = el.scrollLeft;
  el.scrollLeft = x1;
  let x = x1 !== x2 || x2 !== x3;
  let y = y1 !== y2 || y2 !== y3;

  // Force no scrollbars if set as hidden in CSS
  if (xHidden) {
    x = false;
  }
  if (yHidden) {
    y = false;
  }

  return {
    x,
    y
  };
}

/**
 * Finds the scrollable ancestors of an element in the correct direction
 * @param {HTMLElement} element
 * @returns {HTMLElement}
 */
function getScrollParent(direction, element) {
  // Check upwards to find out if that is a scrollable
  // element in the correct direction
  for(var parent = element; parent; parent = parent.parentElement) {
    if(getScrollable(direction, parent))  return parent;
  }
}

drewbaker avatar Feb 05 '23 16:02 drewbaker

I see the benefits and i like this philosophy of universality, to not having to care about init/destroy, However i have one concern: getComputedStyle function triggers reflow. It means with this script you'll trigger reflow for all tested elements on every wheel event, which is bad regarding performance.

source: https://gist.github.com/paulirish/5d52fb081b3570c81e3a

clementroche avatar Feb 05 '23 16:02 clementroche

Thank you x100 for that link! OMG such a good resource.

Yeah I wonder if there is a way to achieve the same thing, but without that function. Or perhaps just not on every scroll event… I’ll think on it.

drewbaker avatar Feb 05 '23 16:02 drewbaker

another ressource: https://stackoverflow.com/questions/71033358/why-doesnt-window-getcomputedstyle-invoke-recalculate-styles-and-reflow

clementroche avatar Feb 05 '23 17:02 clementroche

@paulirish says this:

Reflow only has a cost if the document has changed and invalidated the style or layout. Typically, this is because the DOM was changed (classes modified, nodes added/removed, even adding a psuedo-class like :focus).

So perhaps using ‘getComputedStyle’ like this, and at the start of a scroll event (raf) when the layout is the same as before might actually have no cost. Only one way to find out…

drewbaker avatar Feb 05 '23 17:02 drewbaker

The SO question is...... weird. I don't know why you'd call getComputedStyle and then do nothing with the result. I didnt test the same thing, but.. I guess its cool that the browser made gCS lazy so it only does the work on the getter of the gCS property you want..

Anyway the other important thing to know is that reflow cost is real only if there's an invalidation.. for example:

elem.classList.add('foo');
w = elem.offsetWidth; // reflow!
elem.classList.add('bar');
w = elem.offsetWidth; // reflow!
text = elem.textContent;   // just a getter, nothing changed.
w = elem.offsetWidth; // totally free
w = elem.offsetHeight; // totally free
elem.textContent = 'oh hi'; // oops the style has definitely been invalidated
w = elem.offsetWidth; // reflow!

This also means that if you're evaluating JS in an event handler.. and you know the DOM hasn't been mutated at all yet in this frame... Then the recalc/layout from last frame is STILL good! it hasn't been invalidated with any changes. That's the idea behind https://github.com/wilsonpage/fastdom

But ... I have to admit I think going this route is playing with fire. You NEED to profile because it's very easy to accidentally add one invalidation. Or you can adopt https://github.com/wilsonpage/strictdom via fastdom-strict

paulirish avatar Feb 07 '23 01:02 paulirish

All that said, I'd suggest you look into https://bugs.chromium.org/p/chromium/issues/detail?id=916117 and https://scrolltimeline-playground.glitch.me/

And the newer css side: https://bugs.chromium.org/p/chromium/issues/detail?id=1074052

Both are the declaration mechanism to tell the browser how to animate on scroll. I believe the Web Animation API side has shipped in Chrome, dunno about other browsers. Looks like the css syntax side still remains behind an experiment.

Going that direction will guarantee the best performance. Whereas the current approach of juggling onmousewheel/onscroll/raf etc will always be challenging to work with the browser and hit 60fps in all situations. g'luck!

paulirish avatar Feb 07 '23 01:02 paulirish

Thanks you for your answer @paulirish, in-depth explainations like yours are gold. We wish we could use this native API but it's still not implemented in most of browsers (all examples I've found use this polyfill). Also there is another problem this lib solves which is scroll sync with WebGL, since scroll runs in a seperate thread they can't be perfectly synched without using a third party lib such as Lenis or locomotive-scroll.

@drewbaker, about the getComputedStyle trick i wouldn't take the risk since there is no way for Lenis to know if this will cause a reflow or not, even less on every wheel event.

clementroche avatar Feb 07 '23 10:02 clementroche

We wish we could use this native API but it's still not implemented in most of browsers (all examples I've found use this polyfill).

gotcha yah. i know it's not gonna be as powerful.. so.. yeah. :/

fwiw that polyfill is authored by one of the spec editors. even outside of scroll stuff, he's authored some of the best vanilla performant components around.. eg https://github.com/flackr/web-demos https://github.com/GoogleChromeLabs/ui-element-samples

anyway. cheers!

paulirish avatar Feb 07 '23 22:02 paulirish

So good to read this @paulirish thanks for the tips...

I'll add my two-cents about the missing features of a browser and why we like Lenis... It's because clients see a site like Locomotive Scroll and require us to build something that has the same "gravity" with the scroll as that site. No matter the amount of explaining about how it's bad practice to manipulate the browsers scroll, they don't care. The amount of requests we get for "gravity", "can you slow down the scroll", "make the scroll feel more heavy" is the bane of my existence. I don't think the scroll-timeline API is going to help that (it will be fantastic for the parallax style effects though, which is a huge ask for us at the moment).

So the missing browser API for me is being able to set the scroll easing function... that would be HUGE for us. I get allt he programmer arguments for why that would be terrible and produce some really janky sites, but it's a big part of the use case for Lenis, Locomotive Scroll, and a ton of other libraries.

drewbaker avatar Feb 11 '23 11:02 drewbaker

Yeah I wonder if there is a way to achieve the same thing, but without that function. Or perhaps just not on every scroll event… I’ll think on it.

Thinking more about this... what if you allowed this:

<body data-lenis-container>
      <section/>
      <section/>
      
      <div class="modal" data-lenis-container="{direction:'horizontal'}">
           <section/>
           <section/>      
      </div>
</body>

Then you could do something like this in your JS to get the scrolling parent always...

addEventListener('wheel', (event) => {
      event.target.closest('[data-lenis-container]')
});

The main thing want to achieve here is an easy way to use this with modals, or scrolling carousels, etc...

Not sure how this would work when scrolling over an iFrame...

drewbaker avatar Feb 11 '23 12:02 drewbaker

Ok but you can already do it by creating a new Lenis instances based on data-lenis on page creation. As a developer you have control over content management, how content is loaded, added or removed, Lenis doesn't.

clementroche avatar Feb 28 '23 10:02 clementroche