Skip to content

Commit

Permalink
Update usePreventScroll hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Temzasse committed Oct 27, 2024
1 parent 47fc296 commit 11f7d8b
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 57 deletions.
1 change: 0 additions & 1 deletion example/components/ContentHeight.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, useRef } from 'react';
import { styled } from 'styled-components';
import { Sheet, type SheetRef } from 'react-modal-sheet';
import { motion } from 'framer-motion';

import { Button } from './common';

Expand Down
130 changes: 74 additions & 56 deletions src/use-prevent-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ function preventScrollStandard() {
//
// 1. Prevent default on `touchmove` events that are not in a scrollable element. This prevents touch scrolling
// on the window.
// 2. Prevent default on `touchmove` events inside a scrollable element when the scroll position is at the
// top or bottom. This avoids the whole page scrolling instead, but does prevent overscrolling.
// 2. Set `overscroll-behavior: contain` on nested scrollable regions so they do not scroll the page when at
// the top or bottom. Work around a bug where this does not work when the element does not actually overflow
// by preventing default in a `touchmove` event.
// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves.
// 4. When focusing an input, apply a transform to trick Safari into thinking the input is at the top
// of the page, which prevents it from scrolling the page. After the input is focused, scroll the element
Expand All @@ -103,56 +104,56 @@ function preventScrollStandard() {
// to navigate to an input with the next/previous buttons that's outside a modal.
function preventScrollMobileSafari() {
let scrollable: Element | undefined;
let lastY = 0;
let restoreScrollableStyles: any;

const onTouchStart = (e: TouchEvent) => {
// Store the nearest scrollable parent element from the element that the user touched.
scrollable = getScrollParent(e.target as Element);
scrollable = getScrollParent(e.target as Element, true);
if (
scrollable === document.documentElement &&
scrollable === document.body
) {
return;
}

lastY = e.changedTouches[0].pageY;
// Prevent scrolling up when at the top and scrolling down when at the bottom
// of a nested scrollable area, otherwise mobile Safari will start scrolling
// the window instead.
if (
scrollable instanceof HTMLElement &&
window.getComputedStyle(scrollable).overscrollBehavior === 'auto'
) {
restoreScrollableStyles = setStyle(
scrollable,
'overscrollBehavior',
'contain'
);
}
};

const onTouchMove = (e: TouchEvent) => {
// In special situations, `onTouchStart` may be called without `onTouchStart` being called.
// (e.g. when the user places a finger on the screen before the <Sheet> is mounted and then moves the finger after it is mounted).
// If `onTouchStart` is not called, `scrollable` is `undefined`. Therefore, such cases are ignored.
if (scrollable === undefined) {
return;
}

// Prevent scrolling the window.
if (
!scrollable ||
scrollable === document.documentElement ||
scrollable === document.body
) {
e.preventDefault();
return;
}

// Prevent scrolling up when at the top and scrolling down when at the bottom
// of a nested scrollable area, otherwise mobile Safari will start scrolling
// the window instead. Unfortunately, this disables bounce scrolling when at
// the top but it's the best we can do.
const y = e.changedTouches[0].pageY;
const scrollTop = scrollable.scrollTop;
const bottom = scrollable.scrollHeight - scrollable.clientHeight;

// Fix for: https://github.com/adobe/react-spectrum/pull/3780/files
if (bottom === 0) {
return;
}

if ((scrollTop <= 0 && y > lastY) || (scrollTop >= bottom && y < lastY)) {
// overscroll-behavior should prevent scroll chaining, but currently does not
// if the element doesn't actually overflow. https://bugs.webkit.org/show_bug.cgi?id=243452
// This checks that both the width and height do not overflow, otherwise we might
// block horizontal scrolling too. In that case, adding `touch-action: pan-x` to
// the element will prevent vertical page scrolling. We can't add that automatically
// because it must be set before the touchstart event.
if (
scrollable.scrollHeight === scrollable.clientHeight &&
scrollable.scrollWidth === scrollable.clientWidth
) {
e.preventDefault();
}

lastY = y;
};

const onTouchEnd = (e: TouchEvent) => {
Expand All @@ -161,6 +162,7 @@ function preventScrollMobileSafari() {
// Apply this change if we're not already focused on the target element
if (willOpenKeyboard(target) && target !== document.activeElement) {
e.preventDefault();
setupStyles();

// Apply a transform to trick Safari into thinking the input is at the top of the page
// so it doesn't try to scroll it into view. When tapping on an input, this needs to
Expand All @@ -171,11 +173,18 @@ function preventScrollMobileSafari() {
target.style.transform = '';
});
}

if (restoreScrollableStyles) {
restoreScrollableStyles();
}
};

const onFocus = (e: FocusEvent) => {
const target = e.target as HTMLElement;

if (willOpenKeyboard(target)) {
setupStyles();

// Transform also needs to be applied in the focus event in cases where focus moves
// other than tapping on an input directly, e.g. the next/previous buttons in the
// software keyboard. In these cases, it seems applying the transform in the focus event
Expand All @@ -198,9 +207,7 @@ function preventScrollMobileSafari() {
// measure the correct position to scroll to.
visualViewport.addEventListener(
'resize',
() => {
scrollIntoView(target);
},
() => scrollIntoView(target),
{ once: true }
);
}
Expand All @@ -209,30 +216,42 @@ function preventScrollMobileSafari() {
}
};

const onWindowScroll = () => {
// Last resort. If the window scrolled, scroll it back to the top.
// It should always be at the top because the body will have a negative margin (see below).
window.scrollTo(0, 0);
};
let restoreStyles: any = null;

// Record the original scroll position so we can restore it.
// Then apply a negative margin to the body to offset it by the scroll position. This will
// enable us to scroll the window to the top, which is required for the rest of this to work.
const scrollX = window.pageXOffset;
const scrollY = window.pageYOffset;
const setupStyles = () => {
if (restoreStyles) {
return;
}

const restoreStyles = chain(
setStyle(
document.documentElement,
'paddingRight',
`${window.innerWidth - document.documentElement.clientWidth}px`
),
setStyle(document.documentElement, 'overflow', 'hidden'),
setStyle(document.body, 'marginTop', `-${scrollY}px`)
);
const onWindowScroll = () => {
// Last resort. If the window scrolled, scroll it back to the top.
// It should always be at the top because the body will have a negative margin (see below).
window.scrollTo(0, 0);
};

// Scroll to the top. The negative margin on the body will make this appear the same.
window.scrollTo(0, 0);
// Record the original scroll position so we can restore it.
// Then apply a negative margin to the body to offset it by the scroll position. This will
// enable us to scroll the window to the top, which is required for the rest of this to work.
const scrollX = window.pageXOffset;
const scrollY = window.pageYOffset;

restoreStyles = chain(
addEvent(window, 'scroll', onWindowScroll),
setStyle(
document.documentElement,
'paddingRight',
`${window.innerWidth - document.documentElement.clientWidth}px`
),
setStyle(document.documentElement, 'overflow', 'hidden'),
setStyle(document.body, 'marginTop', `-${scrollY}px`),
() => {
window.scrollTo(scrollX, scrollY);
}
);

// Scroll to the top. The negative margin on the body will make this appear the same.
window.scrollTo(0, 0);
};

const removeEvents = chain(
addEvent(document, 'touchstart', onTouchStart, {
Expand All @@ -247,15 +266,14 @@ function preventScrollMobileSafari() {
passive: false,
capture: true,
}),
addEvent(document, 'focus', onFocus, true),
addEvent(window, 'scroll', onWindowScroll)
addEvent(document, 'focus', onFocus, true)
);

return () => {
// Restore styles and scroll the page back to where it was.
restoreStyles();
restoreScrollableStyles?.();
restoreStyles?.();
removeEvents();
window.scrollTo(scrollX, scrollY);
};
}

Expand Down

0 comments on commit 11f7d8b

Please sign in to comment.