230 lines
13 KiB
Plaintext
230 lines
13 KiB
Plaintext
var $59kHH$reactariautils = require("@react-aria/utils");
|
|
|
|
|
|
function $parcel$export(e, n, v, s) {
|
|
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
|
|
}
|
|
|
|
$parcel$export(module.exports, "usePreventScroll", () => $5c2f5cd01815d369$export$ee0f7cc6afcd1c18);
|
|
/*
|
|
* Copyright 2020 Adobe. All rights reserved.
|
|
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
* governing permissions and limitations under the License.
|
|
*/
|
|
const $5c2f5cd01815d369$var$visualViewport = typeof document !== 'undefined' && window.visualViewport;
|
|
// HTML input types that do not cause the software keyboard to appear.
|
|
const $5c2f5cd01815d369$var$nonTextInputTypes = new Set([
|
|
'checkbox',
|
|
'radio',
|
|
'range',
|
|
'color',
|
|
'file',
|
|
'image',
|
|
'button',
|
|
'submit',
|
|
'reset'
|
|
]);
|
|
// The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
|
|
let $5c2f5cd01815d369$var$preventScrollCount = 0;
|
|
let $5c2f5cd01815d369$var$restore;
|
|
function $5c2f5cd01815d369$export$ee0f7cc6afcd1c18(options = {}) {
|
|
let { isDisabled: isDisabled } = options;
|
|
(0, $59kHH$reactariautils.useLayoutEffect)(()=>{
|
|
if (isDisabled) return;
|
|
$5c2f5cd01815d369$var$preventScrollCount++;
|
|
if ($5c2f5cd01815d369$var$preventScrollCount === 1) {
|
|
if ((0, $59kHH$reactariautils.isIOS)()) $5c2f5cd01815d369$var$restore = $5c2f5cd01815d369$var$preventScrollMobileSafari();
|
|
else $5c2f5cd01815d369$var$restore = $5c2f5cd01815d369$var$preventScrollStandard();
|
|
}
|
|
return ()=>{
|
|
$5c2f5cd01815d369$var$preventScrollCount--;
|
|
if ($5c2f5cd01815d369$var$preventScrollCount === 0) $5c2f5cd01815d369$var$restore();
|
|
};
|
|
}, [
|
|
isDisabled
|
|
]);
|
|
}
|
|
// For most browsers, all we need to do is set `overflow: hidden` on the root element, and
|
|
// add some padding to prevent the page from shifting when the scrollbar is hidden.
|
|
function $5c2f5cd01815d369$var$preventScrollStandard() {
|
|
return (0, $59kHH$reactariautils.chain)($5c2f5cd01815d369$var$setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`), $5c2f5cd01815d369$var$setStyle(document.documentElement, 'overflow', 'hidden'));
|
|
}
|
|
// Mobile Safari is a whole different beast. Even with overflow: hidden,
|
|
// it still scrolls the page in many situations:
|
|
//
|
|
// 1. When the bottom toolbar and address bar are collapsed, page scrolling is always allowed.
|
|
// 2. When the keyboard is visible, the viewport does not resize. Instead, the keyboard covers part of
|
|
// it, so it becomes scrollable.
|
|
// 3. When tapping on an input, the page always scrolls so that the input is centered in the visual viewport.
|
|
// This may cause even fixed position elements to scroll off the screen.
|
|
// 4. When using the next/previous buttons in the keyboard to navigate between inputs, the whole page always
|
|
// scrolls, even if the input is inside a nested scrollable element that could be scrolled instead.
|
|
//
|
|
// In order to work around these cases, and prevent scrolling without jankiness, we do a few things:
|
|
//
|
|
// 1. Prevent default on `touchmove` events that are not in a scrollable element. This prevents touch scrolling
|
|
// on the window.
|
|
// 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
|
|
// into view ourselves, without scrolling the whole page.
|
|
// 5. Offset the body by the scroll position using a negative margin and scroll to the top. This should appear the
|
|
// same visually, but makes the actual scroll position always zero. This is required to make all of the
|
|
// above work or Safari will still try to scroll the page when focusing an input.
|
|
// 6. As a last resort, handle window scroll events, and scroll back to the top. This can happen when attempting
|
|
// to navigate to an input with the next/previous buttons that's outside a modal.
|
|
function $5c2f5cd01815d369$var$preventScrollMobileSafari() {
|
|
let scrollable;
|
|
let restoreScrollableStyles;
|
|
let onTouchStart = (e)=>{
|
|
// Store the nearest scrollable parent element from the element that the user touched.
|
|
scrollable = (0, $59kHH$reactariautils.getScrollParent)(e.target, true);
|
|
if (scrollable === document.documentElement && scrollable === document.body) 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.
|
|
if (scrollable instanceof HTMLElement && window.getComputedStyle(scrollable).overscrollBehavior === 'auto') restoreScrollableStyles = $5c2f5cd01815d369$var$setStyle(scrollable, 'overscrollBehavior', 'contain');
|
|
};
|
|
let onTouchMove = (e)=>{
|
|
// Prevent scrolling the window.
|
|
if (!scrollable || scrollable === document.documentElement || scrollable === document.body) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
// 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();
|
|
};
|
|
let onTouchEnd = (e)=>{
|
|
let target = e.target;
|
|
// Apply this change if we're not already focused on the target element
|
|
if ($5c2f5cd01815d369$var$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
|
|
// be done before the "focus" event, so we have to focus the element ourselves.
|
|
target.style.transform = 'translateY(-2000px)';
|
|
target.focus();
|
|
requestAnimationFrame(()=>{
|
|
target.style.transform = '';
|
|
});
|
|
}
|
|
if (restoreScrollableStyles) restoreScrollableStyles();
|
|
};
|
|
let onFocus = (e)=>{
|
|
let target = e.target;
|
|
if ($5c2f5cd01815d369$var$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
|
|
// is good enough, whereas when tapping an input, it must be done before the focus event. 🤷♂️
|
|
target.style.transform = 'translateY(-2000px)';
|
|
requestAnimationFrame(()=>{
|
|
target.style.transform = '';
|
|
// This will have prevented the browser from scrolling the focused element into view,
|
|
// so we need to do this ourselves in a way that doesn't cause the whole page to scroll.
|
|
if ($5c2f5cd01815d369$var$visualViewport) {
|
|
if ($5c2f5cd01815d369$var$visualViewport.height < window.innerHeight) // If the keyboard is already visible, do this after one additional frame
|
|
// to wait for the transform to be removed.
|
|
requestAnimationFrame(()=>{
|
|
$5c2f5cd01815d369$var$scrollIntoView(target);
|
|
});
|
|
else // Otherwise, wait for the visual viewport to resize before scrolling so we can
|
|
// measure the correct position to scroll to.
|
|
$5c2f5cd01815d369$var$visualViewport.addEventListener('resize', ()=>$5c2f5cd01815d369$var$scrollIntoView(target), {
|
|
once: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
let restoreStyles = null;
|
|
let setupStyles = ()=>{
|
|
if (restoreStyles) return;
|
|
let 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);
|
|
};
|
|
// 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.
|
|
let scrollX = window.pageXOffset;
|
|
let scrollY = window.pageYOffset;
|
|
restoreStyles = (0, $59kHH$reactariautils.chain)($5c2f5cd01815d369$var$addEvent(window, 'scroll', onWindowScroll), $5c2f5cd01815d369$var$setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`), $5c2f5cd01815d369$var$setStyle(document.documentElement, 'overflow', 'hidden'), $5c2f5cd01815d369$var$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);
|
|
};
|
|
let removeEvents = (0, $59kHH$reactariautils.chain)($5c2f5cd01815d369$var$addEvent(document, 'touchstart', onTouchStart, {
|
|
passive: false,
|
|
capture: true
|
|
}), $5c2f5cd01815d369$var$addEvent(document, 'touchmove', onTouchMove, {
|
|
passive: false,
|
|
capture: true
|
|
}), $5c2f5cd01815d369$var$addEvent(document, 'touchend', onTouchEnd, {
|
|
passive: false,
|
|
capture: true
|
|
}), $5c2f5cd01815d369$var$addEvent(document, 'focus', onFocus, true));
|
|
return ()=>{
|
|
// Restore styles and scroll the page back to where it was.
|
|
restoreScrollableStyles === null || restoreScrollableStyles === void 0 ? void 0 : restoreScrollableStyles();
|
|
restoreStyles === null || restoreStyles === void 0 ? void 0 : restoreStyles();
|
|
removeEvents();
|
|
};
|
|
}
|
|
// Sets a CSS property on an element, and returns a function to revert it to the previous value.
|
|
function $5c2f5cd01815d369$var$setStyle(element, style, value) {
|
|
let cur = element.style[style];
|
|
element.style[style] = value;
|
|
return ()=>{
|
|
element.style[style] = cur;
|
|
};
|
|
}
|
|
// Adds an event listener to an element, and returns a function to remove it.
|
|
function $5c2f5cd01815d369$var$addEvent(target, event, handler, options) {
|
|
// internal function, so it's ok to ignore the difficult to fix type error
|
|
// @ts-ignore
|
|
target.addEventListener(event, handler, options);
|
|
return ()=>{
|
|
// @ts-ignore
|
|
target.removeEventListener(event, handler, options);
|
|
};
|
|
}
|
|
function $5c2f5cd01815d369$var$scrollIntoView(target) {
|
|
let root = document.scrollingElement || document.documentElement;
|
|
let nextTarget = target;
|
|
while(nextTarget && nextTarget !== root){
|
|
// Find the parent scrollable element and adjust the scroll position if the target is not already in view.
|
|
let scrollable = (0, $59kHH$reactariautils.getScrollParent)(nextTarget);
|
|
if (scrollable !== document.documentElement && scrollable !== document.body && scrollable !== nextTarget) {
|
|
let scrollableTop = scrollable.getBoundingClientRect().top;
|
|
let targetTop = nextTarget.getBoundingClientRect().top;
|
|
if (targetTop > scrollableTop + nextTarget.clientHeight) scrollable.scrollTop += targetTop - scrollableTop;
|
|
}
|
|
nextTarget = scrollable.parentElement;
|
|
}
|
|
}
|
|
function $5c2f5cd01815d369$var$willOpenKeyboard(target) {
|
|
return target instanceof HTMLInputElement && !$5c2f5cd01815d369$var$nonTextInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
|
|
}
|
|
|
|
|
|
//# sourceMappingURL=usePreventScroll.main.js.map
|