diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index c0f63b95b4..d300be1f39 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -19,6 +19,7 @@ import initialState, { title as siteTitle } from 'flavours/glitch/initial_state' import { IntlProvider } from 'flavours/glitch/locales'; import { store } from 'flavours/glitch/store'; import { isProduction } from 'flavours/glitch/utils/environment'; +import { BodyScrollLock } from 'flavours/glitch/features/ui/components/body_scroll_lock'; const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`; @@ -63,6 +64,7 @@ export default class Mastodon extends PureComponent { + diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index 6fccd3c48e..88091f6013 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -14,7 +14,6 @@ import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import { Video } from 'flavours/glitch/features/video'; import { IntlProvider } from 'flavours/glitch/locales'; import { createPollFromServerJSON } from 'flavours/glitch/models/poll'; -import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; @@ -34,9 +33,6 @@ export default class MediaContainer extends PureComponent { }; handleOpenMedia = (media, index, lang) => { - document.body.classList.add('with-modals--active'); - document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media, index, lang }); }; @@ -45,16 +41,10 @@ export default class MediaContainer extends PureComponent { const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); - document.body.classList.add('with-modals--active'); - document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media: mediaList, lang, options }); }; handleCloseMedia = () => { - document.body.classList.remove('with-modals--active'); - document.documentElement.style.marginRight = '0'; - this.setState({ media: null, index: null, diff --git a/app/javascript/flavours/glitch/features/ui/components/body_scroll_lock.tsx b/app/javascript/flavours/glitch/features/ui/components/body_scroll_lock.tsx new file mode 100644 index 0000000000..fab5319378 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/body_scroll_lock.tsx @@ -0,0 +1,71 @@ +import { useLayoutEffect, useEffect, useState } from 'react'; + +import { createAppSelector, useAppSelector } from 'flavours/glitch/store'; +import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; + +const getShouldLockBodyScroll = createAppSelector( + [ + (state) => state.navigation.open, + (state) => state.modal.get('stack').size > 0, + ], + (isMobileMenuOpen: boolean, isModalOpen: boolean) => { + return isMobileMenuOpen || isModalOpen; + }, +); + +/** + * This component locks scrolling on the `body` element when + * `getShouldLockBodyScroll` returns true. + * + * The scrollbar width is taken into account and written to + * a CSS custom property `--root-scrollbar-width` + */ + +export const BodyScrollLock: React.FC = () => { + const shouldLockBodyScroll = useAppSelector(getShouldLockBodyScroll); + + useLayoutEffect(() => { + document.body.classList.toggle('with-modals--active', shouldLockBodyScroll); + }, [shouldLockBodyScroll]); + + const [scrollbarWidth, setScrollbarWidth] = useState(() => + getScrollbarWidth(), + ); + + useEffect(() => { + const handleResize = () => { + setScrollbarWidth(getScrollbarWidth()); + }; + window.addEventListener('resize', handleResize, { passive: true }); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + // Inject style element to make scrollbar width available + // as CSS custom property + useLayoutEffect(() => { + const nonce = document + .querySelector('meta[name=style-nonce]') + ?.getAttribute('content'); + + if (nonce) { + const styleEl = document.createElement('style'); + styleEl.nonce = nonce; + styleEl.innerHTML = ` + :root { + --root-scrollbar-width: ${scrollbarWidth}px; + } + `; + document.head.appendChild(styleEl); + + return () => { + document.head.removeChild(styleEl); + }; + } + + return () => ''; + }, [scrollbarWidth]); + + return null; +}; diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index 2c85f46241..7c7068be65 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -21,7 +21,6 @@ import { IgnoreNotificationsModal, AnnualReportModal, } from 'flavours/glitch/features/ui/util/async-components'; -import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; import BundleContainer from '../containers/bundle_container'; @@ -98,16 +97,6 @@ export default class ModalRoot extends PureComponent { backgroundColor: null, }; - componentDidUpdate () { - if (this.props.type) { - document.body.classList.add('with-modals--active'); - document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - } else { - document.body.classList.remove('with-modals--active'); - document.documentElement.style.marginRight = '0'; - } - } - setBackgroundColor = color => { this.setState({ backgroundColor: color }); }; diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index 96806b8ceb..7c741fd0e9 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -68,6 +68,7 @@ body { &.with-modals--active { overflow-y: hidden; overscroll-behavior: none; + margin-right: var(--root-scrollbar-width, 0); } } diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 916805c11f..52f1e46ce4 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -2959,6 +2959,11 @@ a.account__display-name { background: var(--background-color); backdrop-filter: var(--background-filter); border-top: 1px solid var(--background-border-color); + box-sizing: border-box; + + .with-modals--active & { + padding-right: var(--root-scrollbar-width); + } .layout-multiple-columns & { display: none; diff --git a/app/javascript/flavours/glitch/utils/scrollbar.ts b/app/javascript/flavours/glitch/utils/scrollbar.ts index d505df1244..268236c217 100644 --- a/app/javascript/flavours/glitch/utils/scrollbar.ts +++ b/app/javascript/flavours/glitch/utils/scrollbar.ts @@ -1,8 +1,9 @@ import { isMobile } from '../is_mobile'; -let cachedScrollbarWidth: number | null = null; - -const getActualScrollbarWidth = () => { +export const getScrollbarWidth = () => { + if (isMobile(window.innerWidth)) { + return 0; + } const outer = document.createElement('div'); outer.style.visibility = 'hidden'; outer.style.overflow = 'scroll'; @@ -16,16 +17,3 @@ const getActualScrollbarWidth = () => { return scrollbarWidth; }; - -export const getScrollbarWidth = () => { - if (cachedScrollbarWidth !== null) { - return cachedScrollbarWidth; - } - - const scrollbarWidth = isMobile(window.innerWidth) - ? 0 - : getActualScrollbarWidth(); - cachedScrollbarWidth = scrollbarWidth; - - return scrollbarWidth; -};