Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: Auto wrapper and content detection #117

Open
drewbaker opened this issue Feb 5, 2023 · 11 comments
Open

Idea: Auto wrapper and content detection #117

drewbaker opened this issue Feb 5, 2023 · 11 comments
Labels

Comments

@drewbaker
Copy link

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;
  }
}

@clementroche
Copy link
Member

clementroche commented Feb 5, 2023

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

@drewbaker
Copy link
Author

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.

@clementroche
Copy link
Member

@drewbaker
Copy link
Author

@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…

@paulirish
Copy link

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
Copy link

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!

@clementroche
Copy link
Member

clementroche commented Feb 7, 2023

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.

@paulirish
Copy link

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!

@drewbaker
Copy link
Author

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
Copy link
Author

drewbaker commented Feb 11, 2023

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...

@clementroche
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants