Fixed: Modal disappearing on mobile Discover page

The existing iOS scroll lock applied `position: fixed` to <body>,
which reset window.scrollY to 0. react-virtualized's WindowScroller
responded by unmounting the row that owned the just-opened modal,
so the modal flashed then vanished and the page jumped to top.

Replace the body-mutating lock with a non-passive touchmove listener
scoped to everything outside #portal-root, so modal content still
scrolls but body touch-scroll is blocked without changing scroll state.
This commit is contained in:
weretere 2026-04-19 19:33:11 -05:00
parent 9226876792
commit a13d2e6ff2
3 changed files with 25 additions and 15 deletions

View file

@ -30,12 +30,6 @@
overflow: hidden !important;
}
.modalOpenIOS {
position: fixed;
right: 0;
left: 0;
}
/*
* Sizes
*/

View file

@ -10,7 +10,6 @@ interface CssExports {
'modalBackdrop': string;
'modalContainer': string;
'modalOpen': string;
'modalOpenIOS': string;
'small': string;
}
export const cssExports: CssExports;

View file

@ -12,7 +12,7 @@ import FocusLock from 'react-focus-lock';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { Size } from 'Helpers/Props/sizes';
import { isIOS } from 'Utilities/browser';
import { isMobile } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { setScrollLock } from 'Utilities/scrollLock';
import ModalError from './ModalError';
@ -29,6 +29,21 @@ function removeFromOpenModals(id: string) {
}
}
// Mobile scroll lock: block touchmove outside the modal portal. Avoids
// mutating body position/overflow — doing so resets window.scrollY to 0,
// which causes react-virtualized WindowScroller to unmount the row that
// owns the just-opened modal (Discover page regression).
function preventTouchScroll(event: TouchEvent) {
let target = event.target as HTMLElement | null;
while (target) {
if (target.id === 'portal-root') {
return;
}
target = target.parentElement;
}
event.preventDefault();
}
function findEventTarget(event: TouchEvent | MouseEvent) {
if ('changedTouches' in event) {
const changedTouches = event.changedTouches;
@ -70,7 +85,6 @@ function Modal({
}: ModalProps) {
const backgroundRef = useRef<HTMLDivElement>(null);
const isBackdropPressed = useRef(false);
const bodyScrollTop = useRef(0);
const wasOpen = usePrevious(isOpen);
const modalId = useId();
@ -125,10 +139,14 @@ function Modal({
openModals.push(modalId);
if (openModals.length === 1) {
if (isIOS()) {
if (isMobile()) {
// Don't mutate body position/overflow on mobile — it resets
// window.scrollY which makes WindowScroller unmount this row's
// modal. Use a touchmove listener to block scroll instead.
setScrollLock(true);
bodyScrollTop.current = document.body.scrollTop;
elementClass(document.body).add(styles.modalOpenIOS);
document.addEventListener('touchmove', preventTouchScroll, {
passive: false,
});
} else {
elementClass(document.body).add(styles.modalOpen);
}
@ -139,9 +157,8 @@ function Modal({
if (openModals.length === 0) {
setScrollLock(false);
if (isIOS()) {
elementClass(document.body).remove(styles.modalOpenIOS);
document.body.scrollTop = bodyScrollTop.current;
if (isMobile()) {
document.removeEventListener('touchmove', preventTouchScroll);
} else {
elementClass(document.body).remove(styles.modalOpen);
}