fix: Prevent scrolling behind menus and modals in Safari iOS (#35183)

This commit is contained in:
diondiondion 2025-06-25 21:22:11 +02:00 committed by GitHub
parent dbb20f76a7
commit c1ef1f62d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 24 additions and 88 deletions

View File

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

View File

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

View File

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

View File

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