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

[useScrollLock] Avoid scrollbar layout shift issues #604

Merged
merged 31 commits into from
Sep 17, 2024

Conversation

atomiks
Copy link
Contributor

@atomiks atomiks commented Sep 12, 2024

Closes #603


  • This applies the internal iOS technique to all browsers aside from iOS, removing the need for scrollbar compensation.
  • iOS implementation is pending

Problem 1

This currently breaks the positioning of (anchored) floating elements. Edit: works if using the fixed positioning strategy instead of absolute + fixed in latest Floating UI version.

This raises some minor interoperability concerns with extensions and third party widgets that use outdated versions of Floating UI, Popper (assuming it's broken there) or custom positioning methods that don't handle this edge case (where html has an offset).

Problem 2

The forced overflow of the <html> scrollbar appears on top of backdrops, which looks a bit unsightly if it's not a custom scrollbar

Screenshot 2024-09-12 at 4 29 06 PM

@atomiks atomiks added the core Infrastructure work going on behind the scenes label Sep 12, 2024
@mui-bot
Copy link

mui-bot commented Sep 12, 2024

Netlify deploy preview

https://deploy-preview-604--base-ui.netlify.app/

Generated by 🚫 dangerJS against a9d020e

@atomiks
Copy link
Contributor Author

atomiks commented Sep 12, 2024

Problem 1: floating-ui/floating-ui#3045

Copy link
Contributor

@vladmoroz vladmoroz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, this raises the concern of interoperability with extensions and third party widgets that use outdated versions of Floating UI, Popper (assuming it's broken there) or custom positioning methods that don't handle this edge case (where html has an offset).

I think this is fine given that Base UI will take some time to get real traction, and a newer Floating UI version will be around during this time. Given that Floating UI is a dependency, I think we should recommend to use a matching Floating UI version in the docs either way.

The forced overflow of the scrollbar appears on top of backdrops, which looks a bit unsightly if it's not a custom scrollbar

I agree this is not ideal. I'd take that over any layout shift though.

packages/mui-base/src/utils/useScrollLock.ts Outdated Show resolved Hide resolved
When the body has an overflow-y: scroll style, layout shift would still occur
Copy link
Contributor

@vladmoroz vladmoroz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, desktop looks fantastic.

Checking iOS now and I can get it to do weird jerky right after zooming in or out... Is there any hope we can nail this?

RPReplay_Final1726222510.mov

@vladmoroz
Copy link
Contributor

Doing another test:

  • On an iOS device, I can't seem to scroll or zoom at all
  • There's a shift when resizing the viewport in Chrome with scrollbar-gutter: stable. This used to works fine in 51fd682
Screen.Recording.2024-09-13.at.15.59.42.mov

@atomiks
Copy link
Contributor Author

atomiks commented Sep 13, 2024

On an iOS device, I can't seem to scroll or zoom at all

You mean once the scroll lock is activated and using pinch-zoom? I can also reproduce this. I suppose it's a limitation of react-aria's solution then, unfortunately. If you mean scrolling or zooming while it's not activated, I can't reproduce that.

There's a shift when resizing the viewport in Chrome with scrollbar-gutter: stable. This used to works fine in 51fd682

Will have to check that.

@vladmoroz
Copy link
Contributor

You mean once the scroll lock is activated and using pinch-zoom? I can also reproduce this. I suppose it's a limitation of react-aria's solution then, unfortunately

Wow I'm shocked that that's what they have... if you open a dialog zoomed in, your only way to get out of it is basically to close the page.

@atomiks
Copy link
Contributor Author

atomiks commented Sep 14, 2024

I'll try to complete the touch-action prototype I had which still allowed pinch-zooming. The logic to find the scrollable container is possible with DOM traversal on a touch event

@oliviertassinari
Copy link
Member

oliviertassinari commented Sep 14, 2024

@atomiks To reproduce, you need to empty the page to not have enough content to create a scrollbar:

Screen.Recording.2024-09-15.at.01.23.39.mov

https://deploy-preview-604--base-ui.netlify.app/components/react-dialog/


@joshwcomeau Cool hack in https://www.joshwcomeau.com/css/has/#global-detection-6 by the way:

SCR-20240915-cfmj

but we can't use this here 😄.

Copy link
Contributor

@vladmoroz vladmoroz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phew, looks great. Added a slight comment on the scrollbar-gutter edge case, otherwise good to go.

I'm super excited about this, reliable scroll locking without layout shifts was such a pain with Radix and other libs.


For posterity, here's what I was testing across browsers

html with overflow: scroll

  • Scrollable page
  • Non-scrollable page
  • Reducing viewport size to make the page scrollable
  • Expanding viewport size to make the page not scrollable

html with scrollbar-gutter: stable

  • Scrollable page
  • Non-scrollable page
  • Reducing viewport size to make the page scrollable
  • Expanding viewport size to make the page not scrollable

body with overflow: scroll

  • Scrollable page
  • Non-scrollable page
  • Reducing viewport size to make the page scrollable
  • Expanding viewport size to make the page not scrollable

Default browser styles on body and html

  • Scrollable page
  • Non-scrollable page
  • Reducing viewport size to make the page scrollable
  • Expanding viewport size to make the page not scrollable

not cleanest feeling UX. I guess the idea is that it's easier for developers not to have to add .mui-fixed like class names? I would feel better like this though:

@oliviertassinari check this out:

Screen.Recording.2024-09-16.at.12.02.34.mov
Screen.Recording.2024-09-16.at.12.05.34.mov

Developers consistently forget about hard scrollbars. Even in the teams of the highest calibre, these often end up being completely unhandled.

I agree that it would be ideal if the backdrop covered the scrollbar area. I am confident though that's not what the end user would be looking at or care about, since their attention is going to be on the part of the page that presents new information. Any risk of having a layout shift is not worth it in my opinion.

With other component libraries, we've been living with the common scrollbar compensation method that requires you to use a CSS variable to set a right value for position: fixed elements, and realistically people just forget to do this all the time.

As another example, here is a UI I had built:

Screen.Recording.2024-09-16.at.12.06.29.mov

The header doesn't move because I did handle that, but the input got messed up over time. Maybe it was me, maybe another developer forgot that; it's natural because you don't anticipate any relationship between a random element on the page and an unrelated dialog.

My guiding rule here is that great DX and great UX go hand in hand. Compromises in DX tend to lead to end user issues.

packages/mui-base/src/utils/useScrollLock.ts Outdated Show resolved Hide resolved
@oliviertassinari
Copy link
Member

oliviertassinari commented Sep 16, 2024

Developers consistently forget about hard scrollbars. Even in the teams of the highest calibre, these often end up being completely unhandled.

@vladmoroz My guess it's because everyone on those teams works on macOS, so they don't see it. They forgot that 70% of the end-users are on Windows. And even for developers, it's still a majority https://www.notion.so/mui-org/Product-Analytics-f2e2576203564e6c8dc3429cf305a36f?pvs=4#8a38433d21814a15acfb99ce2eac7d6b.

Any risk of having a layout shift is not worth it in my opinion.

Yeah, hard dilemma. Your rationale makes sense. Not peak of UX but we avoid the worst-case scenario, and since this is what people tend to remember, it's probably for the best: https://www.notion.so/mui-org/DevEx-DX-044f844fbda74826b2dea32a6e6d8a38?pvs=4#2a825570dd744a5a90b75e014100562e (point made by https://github.com/rsms).

I guess in this case, it's critical to have color-scheme: dark set for the top-level scrollbar to be dark in dark mode 😄.


@atomiks overflow-y: scroll on the html works now 👌

Comment on lines +15 to +16
const html = document.documentElement;
const body = document.body;
Copy link
Member

@oliviertassinari oliviertassinari Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, this should only be a fallback. It breaks supports for iframes and popups, we fixed a bunch of those through the years, e.g. mui/material-ui#18701, mui/material-ui#10328. Example of use case: https://www.openfin.co/.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't apply here since it's locking document level of the current script. There's no element to grab an owner of.

Copy link
Member

@oliviertassinari oliviertassinari Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no element to grab an owner of.

This is the point I get to, I don't see how this API can work:

useScrollLock(modal && mounted);

I think we should provide to useScrollLock.ts enough context to know which document it needs to scroll lock. Issue created #629.

Copy link
Contributor

@vladmoroz vladmoroz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS looks good too

I think we got to remove the theme_color from manifest.json because it's super obnoxious when testing iOS; kind of spoils the impression when it doesn't have to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: dialog This is the name of the generic UI component, not the React module! core Infrastructure work going on behind the scenes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[useScrollLock] Layout shift occurs with certain layouts
4 participants