fix: Prevent scrolling behind menus and modals in Safari iOS (#35183)
This commit is contained in:
		@@ -1,71 +1,30 @@
 | 
			
		||||
import { useLayoutEffect, useEffect, useState } from 'react';
 | 
			
		||||
import { useLayoutEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import { createAppSelector, useAppSelector } from 'mastodon/store';
 | 
			
		||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
 | 
			
		||||
 | 
			
		||||
const getShouldLockBodyScroll = createAppSelector(
 | 
			
		||||
  [
 | 
			
		||||
    (state) => state.navigation.open,
 | 
			
		||||
    (state) => state.modal.get('stack').size > 0,
 | 
			
		||||
  ],
 | 
			
		||||
  (isMobileMenuOpen: boolean, isModalOpen: boolean) => {
 | 
			
		||||
    return isMobileMenuOpen || isModalOpen;
 | 
			
		||||
  },
 | 
			
		||||
  (isMobileMenuOpen: boolean, isModalOpen: boolean) =>
 | 
			
		||||
    isMobileMenuOpen || isModalOpen,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This component locks scrolling on the `body` element when
 | 
			
		||||
 * This component locks scrolling on the body 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);
 | 
			
		||||
    document.documentElement.classList.toggle(
 | 
			
		||||
      'has-modal',
 | 
			
		||||
      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;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import { isMobile } from '../is_mobile';
 | 
			
		||||
 | 
			
		||||
export const getScrollbarWidth = () => {
 | 
			
		||||
  if (isMobile(window.innerWidth)) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  const outer = document.createElement('div');
 | 
			
		||||
  outer.style.visibility = 'hidden';
 | 
			
		||||
  outer.style.overflow = 'scroll';
 | 
			
		||||
  document.body.appendChild(outer);
 | 
			
		||||
 | 
			
		||||
  const inner = document.createElement('div');
 | 
			
		||||
  outer.appendChild(inner);
 | 
			
		||||
 | 
			
		||||
  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
 | 
			
		||||
  outer.remove();
 | 
			
		||||
 | 
			
		||||
  return scrollbarWidth;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,6 +1,20 @@
 | 
			
		||||
@use 'variables' as *;
 | 
			
		||||
@use 'functions' as *;
 | 
			
		||||
 | 
			
		||||
html.has-modal {
 | 
			
		||||
  &,
 | 
			
		||||
  body {
 | 
			
		||||
    touch-action: none;
 | 
			
		||||
    overscroll-behavior: none;
 | 
			
		||||
    -webkit-overflow-scrolling: auto;
 | 
			
		||||
    scrollbar-gutter: stable;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  body {
 | 
			
		||||
    overflow: hidden !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  font-family: $font-sans-serif, sans-serif;
 | 
			
		||||
  background: var(--background-color);
 | 
			
		||||
@@ -64,21 +78,6 @@ body {
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      padding-bottom: env(safe-area-inset-bottom);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.with-modals--active {
 | 
			
		||||
      overflow-y: hidden;
 | 
			
		||||
      overscroll-behavior: none;
 | 
			
		||||
      margin-right: var(--root-scrollbar-width, 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.with-modals {
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
 | 
			
		||||
    &--active {
 | 
			
		||||
      overflow-y: hidden;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.player {
 | 
			
		||||
 
 | 
			
		||||
@@ -2896,10 +2896,6 @@ a.account__display-name {
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
@@ -3170,7 +3166,7 @@ a.account__display-name {
 | 
			
		||||
    .navigation-panel {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      border-inline-start: 1px solid var(--background-border-color);
 | 
			
		||||
      height: 100vh;
 | 
			
		||||
      height: 100dvh;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .navigation-panel__banner,
 | 
			
		||||
@@ -3228,6 +3224,7 @@ a.account__display-name {
 | 
			
		||||
    .navigation-panel {
 | 
			
		||||
      width: 284px;
 | 
			
		||||
      overflow-y: auto;
 | 
			
		||||
      scrollbar-width: thin;
 | 
			
		||||
 | 
			
		||||
      &__menu {
 | 
			
		||||
        flex-shrink: 0;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user