From 5cf3e6f8d6706476b938970c39f7a3169ff33754 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Thu, 9 May 2024 14:33:57 +0200 Subject: [PATCH] Fix Chromium scrollbar-induced layout shifts This commit addresses an issue in Chromium on Linux and Windows where the appearance of a vertical scrollbar causes unexpected horizontal layout shifts. This behavior typically occurs when window is resized, a card is opened or a script is selected, resulting in content being pushed to the left. The solution implemented involves using `scrollbar-gutter: stable` to ensure space is always allocated for the scrollbar, thus preventing any shift in the page layout. This fix primarily affects Chromium-based browsers on Linux and Windows. It has no impact on Firefox on any platform, or any browser on macOS (including Chromium). Because these render the scrollbar as an overlay, and do not suffer from this issue. Steps to reproduce the issue using Chromium brower on Linux/Windows: 1. Open the app width big enough height where vertical scrollbar is not visible. 2. Resize the window to a height that triggers a vertical scrollbar. 3. Notice the layout shift as the body content moves to the right. Changes: - Add CSS mixins and styles to handle scrollbar gutter allocation with a fallback. - Add support for modal dialog background lock to handle `scrollbar-gutter: stable;` in calculations to avoid layout shift when a modal is open. - Add E2E tost to avoid regression. - Update DevToolkit to accommodate new scrollbar spacing. --- src/presentation/assets/styles/_mixins.scss | 4 ++ .../assets/styles/base/_index.scss | 5 ++ .../base/_prevent-scrollbar-layout-shift.scss | 19 ++++++++ .../components/DevToolkit/DevToolkit.vue | 11 ++++- .../DevToolkit/UseScrollbarGutterWidth.ts | 40 ++++++++++++++++ .../ScrollLock/ScrollDomStateAccessor.ts | 2 + .../ScrollLock/UseLockBodyBackgroundScroll.ts | 46 +++++++++++++++---- .../WindowScrollDomStateAccessor.ts | 4 ++ tests/e2e/no-unintended-layout-shifts.cy.ts | 16 ++++++- tests/e2e/support/assert/layout-stability.ts | 30 ++++++++++-- .../scenarios/viewport-test-scenarios.ts | 18 ++++++-- 11 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 src/presentation/assets/styles/base/_prevent-scrollbar-layout-shift.scss create mode 100644 src/presentation/components/DevToolkit/UseScrollbarGutterWidth.ts diff --git a/src/presentation/assets/styles/_mixins.scss b/src/presentation/assets/styles/_mixins.scss index 20211bc26..1c55ba5c2 100644 --- a/src/presentation/assets/styles/_mixins.scss +++ b/src/presentation/assets/styles/_mixins.scss @@ -133,3 +133,7 @@ font-family: $font-family-main; font-size: $font-size-absolute-normal; } + +@mixin allocate-scrollbar-space { + +} diff --git a/src/presentation/assets/styles/base/_index.scss b/src/presentation/assets/styles/base/_index.scss index 45edc2bb6..081a99b63 100644 --- a/src/presentation/assets/styles/base/_index.scss +++ b/src/presentation/assets/styles/base/_index.scss @@ -13,11 +13,16 @@ @use "_code-styling" as *; @use "_margin-padding" as *; @use "_link-styling" as *; +@use "_prevent-scrollbar-layout-shift" as *; * { box-sizing: border-box; } +html { + @include prevent-scrollbar-layout-shift; +} + body { background: $color-background; @include base-font-style; diff --git a/src/presentation/assets/styles/base/_prevent-scrollbar-layout-shift.scss b/src/presentation/assets/styles/base/_prevent-scrollbar-layout-shift.scss new file mode 100644 index 000000000..73796c538 --- /dev/null +++ b/src/presentation/assets/styles/base/_prevent-scrollbar-layout-shift.scss @@ -0,0 +1,19 @@ +// This mixin prevents layout shifts caused by the appearance of a vertical scrollbar +// in Chromium-based browsers on Linux and Windows. +// It creates a reserved space for the scrollbar, ensuring content remains stable and does +// not shift horizontally when the scrollbar appears. +@mixin prevent-scrollbar-layout-shift { + scrollbar-gutter: stable; + + @supports not (scrollbar-gutter: stable) { // https://caniuse.com/mdn-css_properties_scrollbar-gutter + // Safari workaround: Shift content to accommodate non-overlay scrollbar. + // An issue: On small screens, the appearance of the scrollbar can shift content, due to limited space for + // both content and scrollbar. + $full-width-including-scrollbar: 100vw; + $full-width-excluding-scrollbar: 100%; + $scrollbar-width: calc($full-width-including-scrollbar - $full-width-excluding-scrollbar); + padding-inline-start: $scrollbar-width; // Allows both right-to-left (RTL) and left-to-right (LTR) text direction support + } + + // More details: https://web.archive.org/web/20240509122237/https://stackoverflow.com/questions/1417934/how-to-prevent-scrollbar-from-repositioning-web-page +} diff --git a/src/presentation/components/DevToolkit/DevToolkit.vue b/src/presentation/components/DevToolkit/DevToolkit.vue index dbdd5510e..b130bf8c4 100644 --- a/src/presentation/components/DevToolkit/DevToolkit.vue +++ b/src/presentation/components/DevToolkit/DevToolkit.vue @@ -31,6 +31,7 @@ import { defineComponent, ref } from 'vue'; import { injectKey } from '@/presentation/injectionSymbols'; import FlatButton from '@/presentation/components/Shared/FlatButton.vue'; import { dumpNames } from './DumpNames'; +import { useScrollbarGutterWidth } from './UseScrollbarGutterWidth'; export default defineComponent({ components: { @@ -39,6 +40,7 @@ export default defineComponent({ setup() { const { log } = injectKey((keys) => keys.useLogger); const isOpen = ref(true); + const scrollbarGutterWidth = useScrollbarGutterWidth(); const devActions: readonly DevAction[] = [ { @@ -58,6 +60,7 @@ export default defineComponent({ devActions, isOpen, close, + scrollbarGutterWidth, }; }, }); @@ -71,10 +74,14 @@ interface DevAction {