vaul
Version:
Drawer component for React.
1,033 lines (1,020 loc) • 79.1 kB
JavaScript
'use client';
function __insertCSS(code) {
if (!code || typeof document == 'undefined') return
let head = document.head || document.getElementsByTagName('head')[0]
let style = document.createElement('style')
style.type = 'text/css'
head.appendChild(style)
;style.styleSheet ? (style.styleSheet.cssText = code) : style.appendChild(document.createTextNode(code))
}
import * as DialogPrimitive from '@radix-ui/react-dialog';
import * as React from 'react';
import React__default, { useLayoutEffect, useEffect, useMemo } from 'react';
const DrawerContext = React__default.createContext({
drawerRef: {
current: null
},
overlayRef: {
current: null
},
onPress: ()=>{},
onRelease: ()=>{},
onDrag: ()=>{},
onNestedDrag: ()=>{},
onNestedOpenChange: ()=>{},
onNestedRelease: ()=>{},
openProp: undefined,
dismissible: false,
isOpen: false,
isDragging: false,
keyboardIsOpen: {
current: false
},
snapPointsOffset: null,
snapPoints: null,
handleOnly: false,
modal: false,
shouldFade: false,
activeSnapPoint: null,
onOpenChange: ()=>{},
setActiveSnapPoint: ()=>{},
closeDrawer: ()=>{},
direction: 'bottom',
shouldAnimate: {
current: true
},
shouldScaleBackground: false,
setBackgroundColorOnScale: true,
noBodyStyles: false,
container: null,
autoFocus: false
});
const useDrawerContext = ()=>{
const context = React__default.useContext(DrawerContext);
if (!context) {
throw new Error('useDrawerContext must be used within a Drawer.Root');
}
return context;
};
__insertCSS("[data-vaul-drawer]{touch-action:none;will-change:transform;transition:transform .5s cubic-bezier(.32, .72, 0, 1);animation-duration:.5s;animation-timing-function:cubic-bezier(0.32,0.72,0,1)}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=bottom][data-state=open]{animation-name:slideFromBottom}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=bottom][data-state=closed]{animation-name:slideToBottom}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=top][data-state=open]{animation-name:slideFromTop}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=top][data-state=closed]{animation-name:slideToTop}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=left][data-state=open]{animation-name:slideFromLeft}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=left][data-state=closed]{animation-name:slideToLeft}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=right][data-state=open]{animation-name:slideFromRight}[data-vaul-drawer][data-vaul-snap-points=false][data-vaul-drawer-direction=right][data-state=closed]{animation-name:slideToRight}[data-vaul-drawer][data-vaul-snap-points=true][data-vaul-drawer-direction=bottom]{transform:translate3d(0,var(--initial-transform,100%),0)}[data-vaul-drawer][data-vaul-snap-points=true][data-vaul-drawer-direction=top]{transform:translate3d(0,calc(var(--initial-transform,100%) * -1),0)}[data-vaul-drawer][data-vaul-snap-points=true][data-vaul-drawer-direction=left]{transform:translate3d(calc(var(--initial-transform,100%) * -1),0,0)}[data-vaul-drawer][data-vaul-snap-points=true][data-vaul-drawer-direction=right]{transform:translate3d(var(--initial-transform,100%),0,0)}[data-vaul-drawer][data-vaul-delayed-snap-points=true][data-vaul-drawer-direction=top]{transform:translate3d(0,var(--snap-point-height,0),0)}[data-vaul-drawer][data-vaul-delayed-snap-points=true][data-vaul-drawer-direction=bottom]{transform:translate3d(0,var(--snap-point-height,0),0)}[data-vaul-drawer][data-vaul-delayed-snap-points=true][data-vaul-drawer-direction=left]{transform:translate3d(var(--snap-point-height,0),0,0)}[data-vaul-drawer][data-vaul-delayed-snap-points=true][data-vaul-drawer-direction=right]{transform:translate3d(var(--snap-point-height,0),0,0)}[data-vaul-overlay][data-vaul-snap-points=false]{animation-duration:.5s;animation-timing-function:cubic-bezier(0.32,0.72,0,1)}[data-vaul-overlay][data-vaul-snap-points=false][data-state=open]{animation-name:fadeIn}[data-vaul-overlay][data-state=closed]{animation-name:fadeOut}[data-vaul-animate=false]{animation:none!important}[data-vaul-overlay][data-vaul-snap-points=true]{opacity:0;transition:opacity .5s cubic-bezier(.32, .72, 0, 1)}[data-vaul-overlay][data-vaul-snap-points=true]{opacity:1}[data-vaul-drawer]:not([data-vaul-custom-container=true])::after{content:'';position:absolute;background:inherit;background-color:inherit}[data-vaul-drawer][data-vaul-drawer-direction=top]::after{top:initial;bottom:100%;left:0;right:0;height:200%}[data-vaul-drawer][data-vaul-drawer-direction=bottom]::after{top:100%;bottom:initial;left:0;right:0;height:200%}[data-vaul-drawer][data-vaul-drawer-direction=left]::after{left:initial;right:100%;top:0;bottom:0;width:200%}[data-vaul-drawer][data-vaul-drawer-direction=right]::after{left:100%;right:initial;top:0;bottom:0;width:200%}[data-vaul-overlay][data-vaul-snap-points=true]:not([data-vaul-snap-points-overlay=true]):not(\n[data-state=closed]\n){opacity:0}[data-vaul-overlay][data-vaul-snap-points-overlay=true]{opacity:1}[data-vaul-handle]{display:block;position:relative;opacity:.7;background:#e2e2e4;margin-left:auto;margin-right:auto;height:5px;width:32px;border-radius:1rem;touch-action:pan-y}[data-vaul-handle]:active,[data-vaul-handle]:hover{opacity:1}[data-vaul-handle-hitarea]{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:max(100%,2.75rem);height:max(100%,2.75rem);touch-action:inherit}@media (hover:hover) and (pointer:fine){[data-vaul-drawer]{user-select:none}}@media (pointer:fine){[data-vaul-handle-hitarea]:{width:100%;height:100%}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeOut{to{opacity:0}}@keyframes slideFromBottom{from{transform:translate3d(0,var(--initial-transform,100%),0)}to{transform:translate3d(0,0,0)}}@keyframes slideToBottom{to{transform:translate3d(0,var(--initial-transform,100%),0)}}@keyframes slideFromTop{from{transform:translate3d(0,calc(var(--initial-transform,100%) * -1),0)}to{transform:translate3d(0,0,0)}}@keyframes slideToTop{to{transform:translate3d(0,calc(var(--initial-transform,100%) * -1),0)}}@keyframes slideFromLeft{from{transform:translate3d(calc(var(--initial-transform,100%) * -1),0,0)}to{transform:translate3d(0,0,0)}}@keyframes slideToLeft{to{transform:translate3d(calc(var(--initial-transform,100%) * -1),0,0)}}@keyframes slideFromRight{from{transform:translate3d(var(--initial-transform,100%),0,0)}to{transform:translate3d(0,0,0)}}@keyframes slideToRight{to{transform:translate3d(var(--initial-transform,100%),0,0)}}");
function isMobileFirefox() {
const userAgent = navigator.userAgent;
return typeof window !== 'undefined' && (/Firefox/.test(userAgent) && /Mobile/.test(userAgent) || // Android Firefox
/FxiOS/.test(userAgent) // iOS Firefox
);
}
function isMac() {
return testPlatform(/^Mac/);
}
function isIPhone() {
return testPlatform(/^iPhone/);
}
function isSafari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
function isIPad() {
return testPlatform(/^iPad/) || // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
isMac() && navigator.maxTouchPoints > 1;
}
function isIOS() {
return isIPhone() || isIPad();
}
function testPlatform(re) {
return typeof window !== 'undefined' && window.navigator != null ? re.test(window.navigator.platform) : undefined;
}
// This code comes from https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/usePreventScroll.ts
const KEYBOARD_BUFFER = 24;
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
function chain$1(...callbacks) {
return (...args)=>{
for (let callback of callbacks){
if (typeof callback === 'function') {
callback(...args);
}
}
};
}
// @ts-ignore
const visualViewport = typeof document !== 'undefined' && window.visualViewport;
function isScrollable(node) {
let style = window.getComputedStyle(node);
return /(auto|scroll)/.test(style.overflow + style.overflowX + style.overflowY);
}
function getScrollParent(node) {
if (isScrollable(node)) {
node = node.parentElement;
}
while(node && !isScrollable(node)){
node = node.parentElement;
}
return node || document.scrollingElement || document.documentElement;
}
// HTML input types that do not cause the software keyboard to appear.
const 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 preventScrollCount = 0;
let restore;
/**
* Prevents scrolling on the document body on mount, and
* restores it on unmount. Also ensures that content does not
* shift due to the scrollbars disappearing.
*/ function usePreventScroll(options = {}) {
let { isDisabled } = options;
useIsomorphicLayoutEffect(()=>{
if (isDisabled) {
return;
}
preventScrollCount++;
if (preventScrollCount === 1) {
if (isIOS()) {
restore = preventScrollMobileSafari();
}
}
return ()=>{
preventScrollCount--;
if (preventScrollCount === 0) {
restore == null ? void 0 : restore();
}
};
}, [
isDisabled
]);
}
// 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. 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.
// 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 preventScrollMobileSafari() {
let scrollable;
let lastY = 0;
let onTouchStart = (e)=>{
// Store the nearest scrollable parent element from the element that the user touched.
scrollable = getScrollParent(e.target);
if (scrollable === document.documentElement && scrollable === document.body) {
return;
}
lastY = e.changedTouches[0].pageY;
};
let onTouchMove = (e)=>{
// 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.
let y = e.changedTouches[0].pageY;
let scrollTop = scrollable.scrollTop;
let bottom = scrollable.scrollHeight - scrollable.clientHeight;
if (bottom === 0) {
return;
}
if (scrollTop <= 0 && y > lastY || scrollTop >= bottom && y < lastY) {
e.preventDefault();
}
lastY = y;
};
let onTouchEnd = (e)=>{
let target = e.target;
// Apply this change if we're not already focused on the target element
if (isInput(target) && target !== document.activeElement) {
e.preventDefault();
// 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 = '';
});
}
};
let onFocus = (e)=>{
let target = e.target;
if (isInput(target)) {
// 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 (visualViewport) {
if (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(()=>{
scrollIntoView(target);
});
} else {
// Otherwise, wait for the visual viewport to resize before scrolling so we can
// measure the correct position to scroll to.
visualViewport.addEventListener('resize', ()=>scrollIntoView(target), {
once: true
});
}
}
});
}
};
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;
let restoreStyles = chain$1(setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`));
// Scroll to the top. The negative margin on the body will make this appear the same.
window.scrollTo(0, 0);
let removeEvents = chain$1(addEvent(document, 'touchstart', onTouchStart, {
passive: false,
capture: true
}), addEvent(document, 'touchmove', onTouchMove, {
passive: false,
capture: true
}), addEvent(document, 'touchend', onTouchEnd, {
passive: false,
capture: true
}), addEvent(document, 'focus', onFocus, true), addEvent(window, 'scroll', onWindowScroll));
return ()=>{
// Restore styles and scroll the page back to where it was.
restoreStyles();
removeEvents();
window.scrollTo(scrollX, scrollY);
};
}
// Sets a CSS property on an element, and returns a function to revert it to the previous value.
function setStyle(element, style, value) {
// https://github.com/microsoft/TypeScript/issues/17827#issuecomment-391663310
// @ts-ignore
let cur = element.style[style];
// @ts-ignore
element.style[style] = value;
return ()=>{
// @ts-ignore
element.style[style] = cur;
};
}
// Adds an event listener to an element, and returns a function to remove it.
function addEvent(target, event, handler, options) {
// @ts-ignore
target.addEventListener(event, handler, options);
return ()=>{
// @ts-ignore
target.removeEventListener(event, handler, options);
};
}
function scrollIntoView(target) {
let root = document.scrollingElement || document.documentElement;
while(target && target !== root){
// Find the parent scrollable element and adjust the scroll position if the target is not already in view.
let scrollable = getScrollParent(target);
if (scrollable !== document.documentElement && scrollable !== document.body && scrollable !== target) {
let scrollableTop = scrollable.getBoundingClientRect().top;
let targetTop = target.getBoundingClientRect().top;
let targetBottom = target.getBoundingClientRect().bottom;
// Buffer is needed for some edge cases
const keyboardHeight = scrollable.getBoundingClientRect().bottom + KEYBOARD_BUFFER;
if (targetBottom > keyboardHeight) {
scrollable.scrollTop += targetTop - scrollableTop;
}
}
// @ts-ignore
target = scrollable.parentElement;
}
}
function isInput(target) {
return target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
}
// This code comes from https://github.com/radix-ui/primitives/tree/main/packages/react/compose-refs
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/ function setRef(ref, value) {
if (typeof ref === 'function') {
ref(value);
} else if (ref !== null && ref !== undefined) {
ref.current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/ function composeRefs(...refs) {
return (node)=>refs.forEach((ref)=>setRef(ref, node));
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/ function useComposedRefs(...refs) {
// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useCallback(composeRefs(...refs), refs);
}
const cache = new WeakMap();
function set(el, styles, ignoreCache = false) {
if (!el || !(el instanceof HTMLElement)) return;
let originalStyles = {};
Object.entries(styles).forEach(([key, value])=>{
if (key.startsWith('--')) {
el.style.setProperty(key, value);
return;
}
originalStyles[key] = el.style[key];
el.style[key] = value;
});
if (ignoreCache) return;
cache.set(el, originalStyles);
}
function reset(el, prop) {
if (!el || !(el instanceof HTMLElement)) return;
let originalStyles = cache.get(el);
if (!originalStyles) {
return;
}
{
el.style[prop] = originalStyles[prop];
}
}
const isVertical = (direction)=>{
switch(direction){
case 'top':
case 'bottom':
return true;
case 'left':
case 'right':
return false;
default:
return direction;
}
};
function getTranslate(element, direction) {
if (!element) {
return null;
}
const style = window.getComputedStyle(element);
const transform = // @ts-ignore
style.transform || style.webkitTransform || style.mozTransform;
let mat = transform.match(/^matrix3d\((.+)\)$/);
if (mat) {
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d
return parseFloat(mat[1].split(', ')[isVertical(direction) ? 13 : 12]);
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
mat = transform.match(/^matrix\((.+)\)$/);
return mat ? parseFloat(mat[1].split(', ')[isVertical(direction) ? 5 : 4]) : null;
}
function dampenValue(v) {
return 8 * (Math.log(v + 1) - 2);
}
function assignStyle(element, style) {
if (!element) return ()=>{};
const prevStyle = element.style.cssText;
Object.assign(element.style, style);
return ()=>{
element.style.cssText = prevStyle;
};
}
/**
* Receives functions as arguments and returns a new function that calls all.
*/ function chain(...fns) {
return (...args)=>{
for (const fn of fns){
if (typeof fn === 'function') {
// @ts-ignore
fn(...args);
}
}
};
}
const TRANSITIONS = {
DURATION: 0.5,
EASE: [
0.32,
0.72,
0,
1
]
};
const VELOCITY_THRESHOLD = 0.4;
const CLOSE_THRESHOLD = 0.25;
const SCROLL_LOCK_TIMEOUT = 100;
const BORDER_RADIUS = 8;
const NESTED_DISPLACEMENT = 16;
const WINDOW_TOP_OFFSET = 26;
const DRAG_CLASS = 'vaul-dragging';
// This code comes from https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx
function useCallbackRef(callback) {
const callbackRef = React__default.useRef(callback);
React__default.useEffect(()=>{
callbackRef.current = callback;
});
// https://github.com/facebook/react/issues/19240
return React__default.useMemo(()=>(...args)=>callbackRef.current == null ? void 0 : callbackRef.current.call(callbackRef, ...args), []);
}
function useUncontrolledState({ defaultProp, onChange }) {
const uncontrolledState = React__default.useState(defaultProp);
const [value] = uncontrolledState;
const prevValueRef = React__default.useRef(value);
const handleChange = useCallbackRef(onChange);
React__default.useEffect(()=>{
if (prevValueRef.current !== value) {
handleChange(value);
prevValueRef.current = value;
}
}, [
value,
prevValueRef,
handleChange
]);
return uncontrolledState;
}
function useControllableState({ prop, defaultProp, onChange = ()=>{} }) {
const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({
defaultProp,
onChange
});
const isControlled = prop !== undefined;
const value = isControlled ? prop : uncontrolledProp;
const handleChange = useCallbackRef(onChange);
const setValue = React__default.useCallback((nextValue)=>{
if (isControlled) {
const setter = nextValue;
const value = typeof nextValue === 'function' ? setter(prop) : nextValue;
if (value !== prop) handleChange(value);
} else {
setUncontrolledProp(nextValue);
}
}, [
isControlled,
prop,
setUncontrolledProp,
handleChange
]);
return [
value,
setValue
];
}
function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints, drawerRef, overlayRef, fadeFromIndex, onSnapPointChange, direction = 'bottom', container, snapToSequentialPoint }) {
const [activeSnapPoint, setActiveSnapPoint] = useControllableState({
prop: activeSnapPointProp,
defaultProp: snapPoints == null ? void 0 : snapPoints[0],
onChange: setActiveSnapPointProp
});
const [windowDimensions, setWindowDimensions] = React__default.useState(typeof window !== 'undefined' ? {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
} : undefined);
React__default.useEffect(()=>{
function onResize() {
setWindowDimensions({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
});
}
window.addEventListener('resize', onResize);
return ()=>window.removeEventListener('resize', onResize);
}, []);
const isLastSnapPoint = React__default.useMemo(()=>activeSnapPoint === (snapPoints == null ? void 0 : snapPoints[snapPoints.length - 1]) || null, [
snapPoints,
activeSnapPoint
]);
const activeSnapPointIndex = React__default.useMemo(()=>{
var _snapPoints_findIndex;
return (_snapPoints_findIndex = snapPoints == null ? void 0 : snapPoints.findIndex((snapPoint)=>snapPoint === activeSnapPoint)) != null ? _snapPoints_findIndex : null;
}, [
snapPoints,
activeSnapPoint
]);
const shouldFade = snapPoints && snapPoints.length > 0 && (fadeFromIndex || fadeFromIndex === 0) && !Number.isNaN(fadeFromIndex) && snapPoints[fadeFromIndex] === activeSnapPoint || !snapPoints;
const snapPointsOffset = React__default.useMemo(()=>{
const containerSize = container ? {
width: container.getBoundingClientRect().width,
height: container.getBoundingClientRect().height
} : typeof window !== 'undefined' ? {
width: window.innerWidth,
height: window.innerHeight
} : {
width: 0,
height: 0
};
var _snapPoints_map;
return (_snapPoints_map = snapPoints == null ? void 0 : snapPoints.map((snapPoint)=>{
const isPx = typeof snapPoint === 'string';
let snapPointAsNumber = 0;
if (isPx) {
snapPointAsNumber = parseInt(snapPoint, 10);
}
if (isVertical(direction)) {
const height = isPx ? snapPointAsNumber : windowDimensions ? snapPoint * containerSize.height : 0;
if (windowDimensions) {
return direction === 'bottom' ? containerSize.height - height : -containerSize.height + height;
}
return height;
}
const width = isPx ? snapPointAsNumber : windowDimensions ? snapPoint * containerSize.width : 0;
if (windowDimensions) {
return direction === 'right' ? containerSize.width - width : -containerSize.width + width;
}
return width;
})) != null ? _snapPoints_map : [];
}, [
snapPoints,
windowDimensions,
container
]);
const activeSnapPointOffset = React__default.useMemo(()=>activeSnapPointIndex !== null ? snapPointsOffset == null ? void 0 : snapPointsOffset[activeSnapPointIndex] : null, [
snapPointsOffset,
activeSnapPointIndex
]);
const snapToPoint = React__default.useCallback((dimension)=>{
var _snapPointsOffset_findIndex;
const newSnapPointIndex = (_snapPointsOffset_findIndex = snapPointsOffset == null ? void 0 : snapPointsOffset.findIndex((snapPointDim)=>snapPointDim === dimension)) != null ? _snapPointsOffset_findIndex : null;
onSnapPointChange(newSnapPointIndex);
set(drawerRef.current, {
transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
transform: isVertical(direction) ? `translate3d(0, ${dimension}px, 0)` : `translate3d(${dimension}px, 0, 0)`
});
if (snapPointsOffset && newSnapPointIndex !== snapPointsOffset.length - 1 && fadeFromIndex !== undefined && newSnapPointIndex !== fadeFromIndex && newSnapPointIndex < fadeFromIndex) {
set(overlayRef.current, {
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
opacity: '0'
});
} else {
set(overlayRef.current, {
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
opacity: '1'
});
}
setActiveSnapPoint(snapPoints == null ? void 0 : snapPoints[Math.max(newSnapPointIndex, 0)]);
}, [
drawerRef.current,
snapPoints,
snapPointsOffset,
fadeFromIndex,
overlayRef,
setActiveSnapPoint
]);
React__default.useEffect(()=>{
if (activeSnapPoint || activeSnapPointProp) {
var _snapPoints_findIndex;
const newIndex = (_snapPoints_findIndex = snapPoints == null ? void 0 : snapPoints.findIndex((snapPoint)=>snapPoint === activeSnapPointProp || snapPoint === activeSnapPoint)) != null ? _snapPoints_findIndex : -1;
if (snapPointsOffset && newIndex !== -1 && typeof snapPointsOffset[newIndex] === 'number') {
snapToPoint(snapPointsOffset[newIndex]);
}
}
}, [
activeSnapPoint,
activeSnapPointProp,
snapPoints,
snapPointsOffset,
snapToPoint
]);
function onRelease({ draggedDistance, closeDrawer, velocity, dismissible }) {
if (fadeFromIndex === undefined) return;
const currentPosition = direction === 'bottom' || direction === 'right' ? (activeSnapPointOffset != null ? activeSnapPointOffset : 0) - draggedDistance : (activeSnapPointOffset != null ? activeSnapPointOffset : 0) + draggedDistance;
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
const isFirst = activeSnapPointIndex === 0;
const hasDraggedUp = draggedDistance > 0;
if (isOverlaySnapPoint) {
set(overlayRef.current, {
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
});
}
if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
if (dismissible) closeDrawer();
else snapToPoint(snapPointsOffset[0]); // snap to initial point
return;
}
if (!snapToSequentialPoint && velocity > 2 && hasDraggedUp && snapPointsOffset && snapPoints) {
snapToPoint(snapPointsOffset[snapPoints.length - 1]);
return;
}
// Find the closest snap point to the current position
const closestSnapPoint = snapPointsOffset == null ? void 0 : snapPointsOffset.reduce((prev, curr)=>{
if (typeof prev !== 'number' || typeof curr !== 'number') return prev;
return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition) ? curr : prev;
});
const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
// Don't do anything if we swipe upwards while being on the last snap point
if (dragDirection > 0 && isLastSnapPoint && snapPoints) {
snapToPoint(snapPointsOffset[snapPoints.length - 1]);
return;
}
if (isFirst && dragDirection < 0 && dismissible) {
closeDrawer();
}
if (activeSnapPointIndex === null) return;
snapToPoint(snapPointsOffset[activeSnapPointIndex + dragDirection]);
return;
}
snapToPoint(closestSnapPoint);
}
function onDrag({ draggedDistance }) {
if (activeSnapPointOffset === null) return;
const newValue = direction === 'bottom' || direction === 'right' ? activeSnapPointOffset - draggedDistance : activeSnapPointOffset + draggedDistance;
// Don't do anything if we exceed the last(biggest) snap point
if ((direction === 'bottom' || direction === 'right') && newValue < snapPointsOffset[snapPointsOffset.length - 1]) {
return;
}
if ((direction === 'top' || direction === 'left') && newValue > snapPointsOffset[snapPointsOffset.length - 1]) {
return;
}
set(drawerRef.current, {
transform: isVertical(direction) ? `translate3d(0, ${newValue}px, 0)` : `translate3d(${newValue}px, 0, 0)`
});
}
function getPercentageDragged(absDraggedDistance, isDraggingDown) {
if (!snapPoints || typeof activeSnapPointIndex !== 'number' || !snapPointsOffset || fadeFromIndex === undefined) return null;
// If this is true we are dragging to a snap point that is supposed to have an overlay
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
const isOverlaySnapPointOrHigher = activeSnapPointIndex >= fadeFromIndex;
if (isOverlaySnapPointOrHigher && isDraggingDown) {
return 0;
}
// Don't animate, but still use this one if we are dragging away from the overlaySnapPoint
if (isOverlaySnapPoint && !isDraggingDown) return 1;
if (!shouldFade && !isOverlaySnapPoint) return null;
// Either fadeFrom index or the one before
const targetSnapPointIndex = isOverlaySnapPoint ? activeSnapPointIndex + 1 : activeSnapPointIndex - 1;
// Get the distance from overlaySnapPoint to the one before or vice-versa to calculate the opacity percentage accordingly
const snapPointDistance = isOverlaySnapPoint ? snapPointsOffset[targetSnapPointIndex] - snapPointsOffset[targetSnapPointIndex - 1] : snapPointsOffset[targetSnapPointIndex + 1] - snapPointsOffset[targetSnapPointIndex];
const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
if (isOverlaySnapPoint) {
return 1 - percentageDragged;
} else {
return percentageDragged;
}
}
return {
isLastSnapPoint,
activeSnapPoint,
shouldFade,
getPercentageDragged,
setActiveSnapPoint,
activeSnapPointIndex,
onRelease,
onDrag,
snapPointsOffset
};
}
const noop = ()=>()=>{};
function useScaleBackground() {
const { direction, isOpen, shouldScaleBackground, setBackgroundColorOnScale, noBodyStyles } = useDrawerContext();
const timeoutIdRef = React__default.useRef(null);
const initialBackgroundColor = useMemo(()=>document.body.style.backgroundColor, []);
function getScale() {
return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
}
React__default.useEffect(()=>{
if (isOpen && shouldScaleBackground) {
if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
const wrapper = document.querySelector('[data-vaul-drawer-wrapper]') || document.querySelector('[vaul-drawer-wrapper]');
if (!wrapper) return;
chain(setBackgroundColorOnScale && !noBodyStyles ? assignStyle(document.body, {
background: 'black'
}) : noop, assignStyle(wrapper, {
transformOrigin: isVertical(direction) ? 'top' : 'left',
transitionProperty: 'transform, border-radius',
transitionDuration: `${TRANSITIONS.DURATION}s`,
transitionTimingFunction: `cubic-bezier(${TRANSITIONS.EASE.join(',')})`
}));
const wrapperStylesCleanup = assignStyle(wrapper, {
borderRadius: `${BORDER_RADIUS}px`,
overflow: 'hidden',
...isVertical(direction) ? {
transform: `scale(${getScale()}) translate3d(0, calc(env(safe-area-inset-top) + 14px), 0)`
} : {
transform: `scale(${getScale()}) translate3d(calc(env(safe-area-inset-top) + 14px), 0, 0)`
}
});
return ()=>{
wrapperStylesCleanup();
timeoutIdRef.current = window.setTimeout(()=>{
if (initialBackgroundColor) {
document.body.style.background = initialBackgroundColor;
} else {
document.body.style.removeProperty('background');
}
}, TRANSITIONS.DURATION * 1000);
};
}
}, [
isOpen,
shouldScaleBackground,
initialBackgroundColor
]);
}
let previousBodyPosition = null;
/**
* This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
* I won't get into too much detail about what bugs it solves, but so far I've found that setting the body to `position: fixed` is the most reliable way to prevent those bugs.
* Issues that this hook solves:
* https://github.com/emilkowalski/vaul/issues/435
* https://github.com/emilkowalski/vaul/issues/433
* And more that I discovered, but were just not reported.
*/ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
const [activeUrl, setActiveUrl] = React__default.useState(()=>typeof window !== 'undefined' ? window.location.href : '');
const scrollPos = React__default.useRef(0);
const setPositionFixed = React__default.useCallback(()=>{
// All browsers on iOS will return true here.
if (!isSafari()) return;
// If previousBodyPosition is already set, don't set it again.
if (previousBodyPosition === null && isOpen && !noBodyStyles) {
previousBodyPosition = {
position: document.body.style.position,
top: document.body.style.top,
left: document.body.style.left,
height: document.body.style.height,
right: 'unset'
};
// Update the dom inside an animation frame
const { scrollX, innerHeight } = window;
document.body.style.setProperty('position', 'fixed', 'important');
Object.assign(document.body.style, {
top: `${-scrollPos.current}px`,
left: `${-scrollX}px`,
right: '0px',
height: 'auto'
});
window.setTimeout(()=>window.requestAnimationFrame(()=>{
// Attempt to check if the bottom bar appeared due to the position change
const bottomBarHeight = innerHeight - window.innerHeight;
if (bottomBarHeight && scrollPos.current >= innerHeight) {
// Move the content further up so that the bottom bar doesn't hide it
document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
}
}), 300);
}
}, [
isOpen
]);
const restorePositionSetting = React__default.useCallback(()=>{
// All browsers on iOS will return true here.
if (!isSafari()) return;
if (previousBodyPosition !== null && !noBodyStyles) {
// Convert the position from "px" to Int
const y = -parseInt(document.body.style.top, 10);
const x = -parseInt(document.body.style.left, 10);
// Restore styles
Object.assign(document.body.style, previousBodyPosition);
window.requestAnimationFrame(()=>{
if (preventScrollRestoration && activeUrl !== window.location.href) {
setActiveUrl(window.location.href);
return;
}
window.scrollTo(x, y);
});
previousBodyPosition = null;
}
}, [
activeUrl
]);
React__default.useEffect(()=>{
function onScroll() {
scrollPos.current = window.scrollY;
}
onScroll();
window.addEventListener('scroll', onScroll);
return ()=>{
window.removeEventListener('scroll', onScroll);
};
}, []);
React__default.useEffect(()=>{
if (!modal) return;
return ()=>{
if (typeof document === 'undefined') return;
// Another drawer is opened, safe to ignore the execution
const hasDrawerOpened = !!document.querySelector('[data-vaul-drawer]');
if (hasDrawerOpened) return;
restorePositionSetting();
};
}, [
modal,
restorePositionSetting
]);
React__default.useEffect(()=>{
if (nested || !hasBeenOpened) return;
// This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
if (isOpen) {
// avoid for standalone mode (PWA)
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
!isStandalone && setPositionFixed();
if (!modal) {
window.setTimeout(()=>{
restorePositionSetting();
}, 500);
}
} else {
restorePositionSetting();
}
}, [
isOpen,
hasBeenOpened,
activeUrl,
modal,
nested,
setPositionFixed,
restorePositionSetting
]);
return {
restorePositionSetting
};
}
function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRelease: onReleaseProp, snapPoints, shouldScaleBackground = false, setBackgroundColorOnScale = true, closeThreshold = CLOSE_THRESHOLD, scrollLockTimeout = SCROLL_LOCK_TIMEOUT, dismissible = true, handleOnly = false, fadeFromIndex = snapPoints && snapPoints.length - 1, activeSnapPoint: activeSnapPointProp, setActiveSnapPoint: setActiveSnapPointProp, fixed, modal = true, onClose, nested, noBodyStyles = false, direction = 'bottom', defaultOpen = false, disablePreventScroll = true, snapToSequentialPoint = false, preventScrollRestoration = false, repositionInputs = true, onAnimationEnd, container, autoFocus = false }) {
var _drawerRef_current, _drawerRef_current1;
const [isOpen = false, setIsOpen] = useControllableState({
defaultProp: defaultOpen,
prop: openProp,
onChange: (o)=>{
onOpenChange == null ? void 0 : onOpenChange(o);
if (!o && !nested) {
restorePositionSetting();
}
setTimeout(()=>{
onAnimationEnd == null ? void 0 : onAnimationEnd(o);
}, TRANSITIONS.DURATION * 1000);
if (o && !modal) {
if (typeof window !== 'undefined') {
window.requestAnimationFrame(()=>{
document.body.style.pointerEvents = 'auto';
});
}
}
if (!o) {
// This will be removed when the exit animation ends (`500ms`)
document.body.style.pointerEvents = 'auto';
}
}
});
const [hasBeenOpened, setHasBeenOpened] = React__default.useState(false);
const [isDragging, setIsDragging] = React__default.useState(false);
const [justReleased, setJustReleased] = React__default.useState(false);
const overlayRef = React__default.useRef(null);
const openTime = React__default.useRef(null);
const dragStartTime = React__default.useRef(null);
const dragEndTime = React__default.useRef(null);
const lastTimeDragPrevented = React__default.useRef(null);
const isAllowedToDrag = React__default.useRef(false);
const nestedOpenChangeTimer = React__default.useRef(null);
const pointerStart = React__default.useRef(0);
const keyboardIsOpen = React__default.useRef(false);
const shouldAnimate = React__default.useRef(!defaultOpen);
const previousDiffFromInitial = React__default.useRef(0);
const drawerRef = React__default.useRef(null);
const drawerHeightRef = React__default.useRef(((_drawerRef_current = drawerRef.current) == null ? void 0 : _drawerRef_current.getBoundingClientRect().height) || 0);
const drawerWidthRef = React__default.useRef(((_drawerRef_current1 = drawerRef.current) == null ? void 0 : _drawerRef_current1.getBoundingClientRect().width) || 0);
const initialDrawerHeight = React__default.useRef(0);
const onSnapPointChange = React__default.useCallback((activeSnapPointIndex)=>{
// Change openTime ref when we reach the last snap point to prevent dragging for 500ms incase it's scrollable.
if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) openTime.current = new Date();
}, []);
const { activeSnapPoint, activeSnapPointIndex, setActiveSnapPoint, onRelease: onReleaseSnapPoints, snapPointsOffset, onDrag: onDragSnapPoints, shouldFade, getPercentageDragged: getSnapPointsPercentageDragged } = useSnapPoints({
snapPoints,
activeSnapPointProp,
setActiveSnapPointProp,
drawerRef,
fadeFromIndex,
overlayRef,
onSnapPointChange,
direction,
container,
snapToSequentialPoint
});
usePreventScroll({
isDisabled: !isOpen || isDragging || !modal || justReleased || !hasBeenOpened || !repositionInputs || !disablePreventScroll
});
const { restorePositionSetting } = usePositionFixed({
isOpen,
modal,
nested: nested != null ? nested : false,
hasBeenOpened,
preventScrollRestoration,
noBodyStyles
});
function getScale() {
return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
}
function onPress(event) {
var _drawerRef_current, _drawerRef_current1;
if (!dismissible && !snapPoints) return;
if (drawerRef.current && !drawerRef.current.contains(event.target)) return;
drawerHeightRef.current = ((_drawerRef_current = drawerRef.current) == null ? void 0 : _drawerRef_current.getBoundingClientRect().height) || 0;
drawerWidthRef.current = ((_drawerRef_current1 = drawerRef.current) == null ? void 0 : _drawerRef_current1.getBoundingClientRect().width) || 0;
setIsDragging(true);
dragStartTime.current = new Date();
// iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
if (isIOS()) {
window.addEventListener('touchend', ()=>isAllowedToDrag.current = false, {
once: true
});
}
// Ensure we maintain correct pointer capture even when going outside of the drawer
event.target.setPointerCapture(event.pointerId);
pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
}
function shouldDrag(el, isDraggingInDirection) {
var _window_getSelection;
let element = el;
const highlightedText = (_window_getSelection = window.getSelection()) == null ? void 0 : _window_getSelection.toString();
const swipeAmount = drawerRef.current ? getTranslate(drawerRef.current, direction) : null;
const date = new Date();
// Fixes https://github.com/emilkowalski/vaul/issues/483
if (element.tagName === 'SELECT') {
return false;
}
if (element.hasAttribute('data-vaul-no-drag') || element.closest('[data-vaul-no-drag]')) {
return false;
}
if (direction === 'right' || direction === 'left') {
return true;
}
// Allow scrolling when animating
if (openTime.current && date.getTime() - openTime.current.getTime() < 500) {
return false;
}
if (swipeAmount !== null) {
if (direction === 'bottom' ? swipeAmount > 0 : swipeAmount < 0) {
return true;
}
}
// Don't drag if there's highlighted text
if (highlightedText && highlightedText.length > 0) {
return false;
}
// Disallow dragging if drawer was scrolled within `scrollLockTimeout`
if (lastTimeDragPrevented.current && date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout && swipeAmount === 0) {
lastTimeDragPrevented.current = date;
return false;
}
if (isDraggingInDirection) {
lastTimeDragPrevented.current = date;
// We are dragging down so we should allow scrolling
return false;
}
// Keep climbing up the DOM tree as long as there's a parent
while(element){
// Check if the element is scrollable
if (element.scrollHeight > element.clientHeight) {
if (element.scrollTop !== 0) {
lastTimeDragPrevented.current = new Date();
// The element is scrollable and not scrolled to the top, so don't drag
return false;
}
if (element.getAttribute('role') === 'dialog') {
return true;
}
}
// Move up to the parent element
element = element.parentNode;
}
// No scrollable parents not scrolled to the top found, so drag
return true;
}
function onDrag(event) {
if (!drawerRef.current) {
return;
}
//