UNPKG

react-modal-sheet

Version:

Flexible bottom sheet component for your React apps

1,059 lines (1,048 loc) 32.3 kB
import { createPortal } from 'react-dom'; import React2, { createContext, forwardRef, useRef, useEffect, useLayoutEffect, useImperativeHandle, useMemo, Children, cloneElement, useState, useCallback, useContext } from 'react'; import { useMotionValue, useReducedMotion, useTransform, animate, motion, AnimatePresence } from 'motion/react'; import { transform } from 'motion'; // src/sheet.tsx // src/constants.ts var MAX_HEIGHT = "calc(100% - env(safe-area-inset-top) - 34px)"; var IS_SSR = typeof window === "undefined"; var DEFAULT_TWEEN_CONFIG = { ease: "easeOut", duration: 0.2 }; var REDUCED_MOTION_TWEEN_CONFIG = { ease: "linear", duration: 0.01 }; var DEFAULT_DRAG_CLOSE_THRESHOLD = 0.6; var DEFAULT_DRAG_VELOCITY_THRESHOLD = 500; // src/utils.ts function getSheetHeight(sheetRef) { const sheetEl = sheetRef.current; if (!sheetEl) { console.warn( "Sheet height is not available because the sheet element is not mounted yet." ); return 0; } return Math.round(sheetEl.getBoundingClientRect().height); } function getSnapPoints({ snapPointsProp, sheetHeight }) { const snapPointValues = snapPointsProp.map((point) => { if (point > 0 && point <= 1) { return Math.round(point * sheetHeight); } return point < 0 ? sheetHeight + point : point; }); console.assert( inDescendingOrder(snapPointValues) || sheetHeight === 0, `Snap points need to be in descending order got: [${snapPointsProp.join(", ")}]` ); return snapPointValues; } function getClosestSnapPoint({ snapPoints, currentY, sheetHeight, detent }) { const snapInverse = snapPoints.map( (p) => sheetHeight - Math.min(p, sheetHeight) ); if (detent === "content-height" && !snapInverse.includes(0)) { snapInverse.unshift(0); } const snapTo = getClosest(snapInverse, currentY); const snapIndex = snapInverse.indexOf(snapTo); const snapY = validateSnapTo({ snapTo: getClosest(snapInverse, currentY), sheetHeight }); return { snapY, snapIndex }; } function getClosest(nums, goal) { let closest = nums[0]; let minDifference = Math.abs(nums[0] - goal); for (let i = 1; i < nums.length; i++) { const difference = Math.abs(nums[i] - goal); if (difference < minDifference) { closest = nums[i]; minDifference = difference; } } return closest; } function inDescendingOrder(arr) { for (let i = 0; i < arr.length; i++) { if (arr[i + 1] > arr[i]) return false; } return true; } function getSnapY({ sheetHeight, snapPoints, snapIndex }) { const snapPoint = snapPoints[snapIndex]; if (snapPoint === void 0) { console.warn( `Invalid snap index ${snapIndex}. Snap points are: [${snapPoints.join(", ")}]` ); return null; } const y = validateSnapTo({ snapTo: sheetHeight - snapPoint, sheetHeight }); return y; } function validateSnapTo({ snapTo, sheetHeight }) { if (snapTo < 0) { console.warn( `Snap point is out of bounds. Sheet height is ${sheetHeight} but snap point is ${sheetHeight + Math.abs(snapTo)}.` ); } return Math.max(Math.round(snapTo), 0); } function mergeRefs(refs) { return (value) => { refs.forEach((ref) => { if (typeof ref === "function") { ref(value); } else if (ref) { ref.current = value; } }); }; } function isTouchDevice() { if (IS_SSR) return false; return "ontouchstart" in window || navigator.maxTouchPoints > 0; } function testPlatform(re) { var _a; return typeof window !== "undefined" && window.navigator != null ? re.test( // @ts-expect-error ((_a = window.navigator.userAgentData) == null ? void 0 : _a.platform) || window.navigator.platform ) : false; } function cached(fn) { let res = null; return () => { if (res == null) { res = fn(); } return res; }; } var isMac = cached(function() { return testPlatform(/^Mac/i); }); var isIPhone = cached(function() { return testPlatform(/^iPhone/i); }); var isIPad = cached(function() { return testPlatform(/^iPad/i) || isMac() && navigator.maxTouchPoints > 1; }); var isIOS = cached(function() { return isIPhone() || isIPad(); }); var SheetContext = createContext( void 0 ); function useSheetContext() { const context = useContext(SheetContext); if (!context) throw new Error("Sheet context error"); return context; } var SheetScrollerContext = createContext(void 0); function SheetScrollerContextProvider({ children }) { const sheetContext = useSheetContext(); const [disableDrag, setDisableDrag] = useState(!!sheetContext.disableDrag); function setDragEnabled() { if (!sheetContext.disableDrag) setDisableDrag(false); } function setDragDisabled() { if (!disableDrag) setDisableDrag(true); } return /* @__PURE__ */ React2.createElement( SheetScrollerContext.Provider, { value: { disableDrag, setDragEnabled, setDragDisabled } }, children ); } function useSheetScrollerContext() { const context = useContext(SheetScrollerContext); if (!context) throw new Error("Sheet scroller context error"); return context; } // src/styles.ts var styles = { wrapper: { position: "fixed", top: 0, bottom: 0, left: 0, right: 0, overflow: "hidden", pointerEvents: "none" }, backdrop: { zIndex: 1, position: "fixed", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: "rgba(0, 0, 0, 0.2)", touchAction: "none", border: "none", userSelect: "none", WebkitTapHighlightColor: "transparent" }, container: { zIndex: 2, position: "absolute", left: 0, bottom: 0, width: "100%", backgroundColor: "#fff", borderTopRightRadius: "8px", borderTopLeftRadius: "8px", boxShadow: "0px -2px 16px rgba(0, 0, 0, 0.3)", display: "flex", flexDirection: "column", pointerEvents: "auto" }, headerWrapper: { width: "100%" }, header: { height: "40px", width: "100%", position: "relative", display: "flex", alignItems: "center", justifyContent: "center" }, indicator: { width: "18px", height: "4px", borderRadius: "99px", backgroundColor: "#ddd" }, content: { flexGrow: 1, display: "flex", flexDirection: "column", minHeight: "0px", position: "relative" }, scroller: { height: "100%", overflowY: "auto", overscrollBehaviorY: "none" } }; var useIsomorphicLayoutEffect = IS_SSR ? useEffect : useLayoutEffect; function useSafeAreaInsets() { const [insets] = useState(() => { const fallback = { top: 0, left: 0, right: 0, bottom: 0 }; if (IS_SSR) return fallback; const root = document.querySelector(":root"); if (!root) return fallback; root.style.setProperty("--rms-sat", "env(safe-area-inset-top)"); root.style.setProperty("--rms-sal", "env(safe-area-inset-left)"); root.style.setProperty("--rms-sar", "env(safe-area-inset-right)"); root.style.setProperty("--rms-sab", "env(safe-area-inset-bottom)"); const computedStyle = getComputedStyle(root); const sat = getComputedValue(computedStyle, "--rms-sat"); const sal = getComputedValue(computedStyle, "--rms-sal"); const sar = getComputedValue(computedStyle, "--rms-sar"); const sab = getComputedValue(computedStyle, "--rms-sab"); root.style.removeProperty("--rms-sat"); root.style.removeProperty("--rms-sal"); root.style.removeProperty("--rms-sar"); root.style.removeProperty("--rms-sab"); return { top: sat, left: sal, right: sar, bottom: sab }; }); return insets; } function getComputedValue(computed, property) { const strValue = computed.getPropertyValue(property).replace("px", "").trim(); return parseInt(strValue, 10) || 0; } // src/hooks/use-modal-effect.ts function useModalEffect({ y, rootId, sheetRef, snapPointsProp, startThreshold }) { const insetTop = useSafeAreaInsets().top; useIsomorphicLayoutEffect(() => { return () => { if (rootId) cleanupModalEffect(rootId); }; }, []); useIsomorphicLayoutEffect(() => { if (!rootId) return; const root = document.querySelector(`#${rootId}`); if (!root) return; let sheetHeight = 0; const removeStartListener = y.on("animationStart", () => { sheetHeight = getSheetHeight(sheetRef); setupModalEffect(rootId); }); const removeChangeListener = y.on("change", (yValue) => { if (!root) return; let progress = Math.max(0, 1 - yValue / sheetHeight); const snapPoints = snapPointsProp ? getSnapPoints({ snapPointsProp, sheetHeight }) : void 0; const snapThresholdPoint = snapPoints && snapPoints.length > 1 ? snapPoints[1] : void 0; if (snapThresholdPoint !== void 0) { const snapThresholdValue = sheetHeight - Math.min(snapThresholdPoint, sheetHeight); if (yValue <= snapThresholdValue) { progress = (snapThresholdValue - yValue) / snapThresholdValue; } else { progress = 0; } } if (startThreshold !== void 0) { const startThresholdValue = sheetHeight - Math.min(Math.floor(startThreshold * sheetHeight), sheetHeight); if (yValue <= startThresholdValue) { progress = (startThresholdValue - yValue) / startThresholdValue; } else { progress = 0; } } progress = Math.max(0, Math.min(1, progress)); const pageWidth = window.innerWidth; const ty = transform(progress, [0, 1], [0, 24 + insetTop]); const s = transform(progress, [0, 1], [1, (pageWidth - 16) / pageWidth]); const borderRadius = transform(progress, [0, 1], [0, 10]); root.style.transform = `scale(${s}) translate3d(0, ${ty}px, 0)`; root.style.borderTopRightRadius = `${borderRadius}px`; root.style.borderTopLeftRadius = `${borderRadius}px`; }); function onCompleted() { if (y.get() - 5 >= sheetHeight) { cleanupModalEffect(rootId); } } const removeCompleteListener = y.on("animationComplete", onCompleted); const removeCancelListener = y.on("animationCancel", onCompleted); return () => { removeStartListener(); removeChangeListener(); removeCompleteListener(); removeCancelListener(); }; }, [y, rootId, insetTop, startThreshold, snapPointsProp]); } function setupModalEffect(rootId) { const root = document.querySelector(`#${rootId}`); const body = document.querySelector("body"); if (!root) return; body.style.backgroundColor = "#000"; root.style.overflow = "hidden"; root.style.transitionTimingFunction = "cubic-bezier(0.32, 0.72, 0, 1)"; root.style.transitionProperty = "transform, border-radius"; root.style.transitionDuration = "0.5s"; root.style.transformOrigin = "center top"; } function cleanupModalEffect(rootId) { const root = document.querySelector(`#${rootId}`); const body = document.querySelector("body"); if (!root) return; body.style.removeProperty("background-color"); root.style.removeProperty("overflow"); root.style.removeProperty("transition-timing-function"); root.style.removeProperty("transition-property"); root.style.removeProperty("transition-duration"); root.style.removeProperty("transform-origin"); root.style.removeProperty("transform"); root.style.removeProperty("border-top-right-radius"); root.style.removeProperty("border-top-left-radius"); } function useDimensions() { const [dimensions, setDimensions] = useState(() => ({ height: !IS_SSR ? window.innerHeight : 0, width: !IS_SSR ? window.innerWidth : 0 })); useIsomorphicLayoutEffect(() => { function handler() { setDimensions({ height: window.innerHeight, width: window.innerWidth }); } handler(); window.addEventListener("resize", handler); return () => { window.removeEventListener("resize", handler); }; }, []); return dimensions; } function useStableCallback(handler) { const handlerRef = useRef(void 0); useIsomorphicLayoutEffect(() => { handlerRef.current = handler; }); return useCallback((...args) => { const fn = handlerRef.current; return fn == null ? void 0 : fn(...args); }, []); } // src/hooks/use-prevent-scroll.ts var KEYBOARD_BUFFER = 24; function chain(...callbacks) { return (...args) => { for (const callback of callbacks) { if (typeof callback === "function") { callback(...args); } } }; } var visualViewport = typeof document !== "undefined" && window.visualViewport; function isScrollable(node, checkForOverflow) { if (!node) { return false; } const style = window.getComputedStyle(node); let isScrollable2 = /(auto|scroll)/.test( style.overflow + style.overflowX + style.overflowY ); if (isScrollable2 && checkForOverflow) { isScrollable2 = node.scrollHeight !== node.clientHeight || node.scrollWidth !== node.clientWidth; } return isScrollable2; } function getScrollParent(node, checkForOverflow) { let scrollableNode = node; if (isScrollable(scrollableNode, checkForOverflow)) { scrollableNode = scrollableNode.parentElement; } while (scrollableNode && !isScrollable(scrollableNode, checkForOverflow)) { scrollableNode = scrollableNode.parentElement; } return scrollableNode || document.scrollingElement || document.documentElement; } var nonTextInputTypes = /* @__PURE__ */ new Set([ "checkbox", "radio", "range", "color", "file", "image", "button", "submit", "reset" ]); var preventScrollCount = 0; var restore; function usePreventScroll(options = {}) { const { isDisabled } = options; useIsomorphicLayoutEffect(() => { if (isDisabled) { return; } preventScrollCount++; if (preventScrollCount === 1) { if (isIOS()) { restore = preventScrollMobileSafari(); } else { restore = preventScrollStandard(); } } return () => { preventScrollCount--; if (preventScrollCount === 0) { restore == null ? void 0 : restore(); } }; }, [isDisabled]); } function preventScrollStandard() { return chain( setStyle( document.documentElement, "paddingRight", `${window.innerWidth - document.documentElement.clientWidth}px` ), setStyle(document.documentElement, "overflow", "hidden") ); } function preventScrollMobileSafari() { let scrollable; let lastY = 0; const onTouchStart = (e) => { var _a; const target = (_a = e.composedPath()) == null ? void 0 : _a[0]; scrollable = getScrollParent(target, true); if (scrollable === document.documentElement && scrollable === document.body) { return; } lastY = e.changedTouches[0].pageY; }; const onTouchMove = (e) => { if (scrollable === void 0) { return; } if (!scrollable || scrollable === document.documentElement || scrollable === document.body) { e.preventDefault(); return; } const y = e.changedTouches[0].pageY; const scrollTop = scrollable.scrollTop; const bottom = scrollable.scrollHeight - scrollable.clientHeight; if (bottom === 0) { return; } if (scrollTop <= 0 && y > lastY || scrollTop >= bottom && y < lastY) { e.preventDefault(); } lastY = y; }; const onTouchEnd = (e) => { var _a; const target = (_a = e.composedPath()) == null ? void 0 : _a[0]; if (willOpenKeyboard(target) && target !== document.activeElement) { e.preventDefault(); target.style.transform = "translateY(-2000px)"; target.focus(); requestAnimationFrame(() => { target.style.transform = ""; }); } }; const onFocus = (e) => { var _a; const target = (_a = e.composedPath()) == null ? void 0 : _a[0]; if (willOpenKeyboard(target)) { target.style.transform = "translateY(-2000px)"; requestAnimationFrame(() => { target.style.transform = ""; if (visualViewport) { if (visualViewport.height < window.innerHeight) { requestAnimationFrame(() => { scrollIntoView(target); }); } else { visualViewport.addEventListener( "resize", () => scrollIntoView(target), { once: true } ); } } }); } }; const onWindowScroll = () => { window.scrollTo(0, 0); }; const scrollX = window.pageXOffset; const scrollY = window.pageYOffset; const restoreStyles = chain( setStyle( document.documentElement, "paddingRight", `${window.innerWidth - document.documentElement.clientWidth}px` ), setStyle(document.documentElement, "overflow", "hidden"), setStyle(document.body, "marginTop", `-${scrollY}px`) ); window.scrollTo(0, 0); const removeEvents = chain( 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 () => { restoreStyles(); removeEvents(); window.scrollTo(scrollX, scrollY); }; } function setStyle(element, style, value) { const cur = element.style[style]; element.style[style] = value; return () => { element.style[style] = cur; }; } function addEvent(target, event, handler, options) { target.addEventListener(event, handler, options); return () => { target.removeEventListener(event, handler, options); }; } function scrollIntoView(target) { const root = document.scrollingElement || document.documentElement; while (target && target !== root) { const scrollable = getScrollParent(target); if (scrollable !== document.documentElement && scrollable !== document.body && scrollable !== target) { const scrollableTop = scrollable.getBoundingClientRect().top; const targetTop = target.getBoundingClientRect().top; const targetBottom = target.getBoundingClientRect().bottom; const keyboardHeight = scrollable.getBoundingClientRect().bottom + KEYBOARD_BUFFER; if (targetBottom > keyboardHeight) { scrollable.scrollTop += targetTop - scrollableTop; } } target = scrollable.parentElement; } } function willOpenKeyboard(target) { return target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable; } // src/sheet.tsx var Sheet = forwardRef( ({ onOpenStart, onOpenEnd, onClose, onCloseStart, onCloseEnd, onSnap, children, disableScrollLocking = false, isOpen, snapPoints: snapPointsProp, rootId, modalEffectRootId, mountPoint, style, detent = "full-height", initialSnap = 0, disableDrag = false, prefersReducedMotion = false, tweenConfig = DEFAULT_TWEEN_CONFIG, dragVelocityThreshold = DEFAULT_DRAG_VELOCITY_THRESHOLD, dragCloseThreshold = DEFAULT_DRAG_CLOSE_THRESHOLD, modalEffectThreshold, ...rest }, ref) => { const sheetRef = useRef(null); const indicatorRotation = useMotionValue(0); const { height: windowHeight } = useDimensions(); const shouldReduceMotion = useReducedMotion(); const reduceMotion = Boolean(prefersReducedMotion || shouldReduceMotion); const animationOptions = { type: "tween", ...reduceMotion ? REDUCED_MOTION_TWEEN_CONFIG : tweenConfig }; const y = useMotionValue(windowHeight); const zIndex = useTransform( y, (value) => value + 2 >= windowHeight ? -1 : (style == null ? void 0 : style.zIndex) ?? 9999 ); const visibility = useTransform( y, (value) => value + 2 >= windowHeight ? "hidden" : "visible" ); const callbacks = useRef({ onOpenStart, onOpenEnd, onCloseStart, onCloseEnd }); useIsomorphicLayoutEffect(() => { callbacks.current = { onOpenStart, onOpenEnd, onCloseStart, onCloseEnd }; }); const onDrag = useStableCallback((_, info) => { const currentY = y.get(); if (snapPointsProp) { const sheetHeight = getSheetHeight(sheetRef); const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight }); const topSnapPoint = sheetHeight - snapPoints[0]; if (info.delta.y < 0 && currentY <= topSnapPoint) { y.set(topSnapPoint); return; } } const velocity = y.getVelocity(); if (velocity > 0) indicatorRotation.set(10); if (velocity < 0) indicatorRotation.set(-10); y.set(Math.max(currentY + info.delta.y, 0)); }); const onDragStart = useStableCallback(() => { const focusedElement = document.activeElement; if (!focusedElement || !sheetRef.current) return; const isInput = focusedElement.tagName === "INPUT" || focusedElement.tagName === "TEXTAREA"; if (isInput && sheetRef.current.contains(focusedElement)) { focusedElement.blur(); } }); const onDragEnd = useStableCallback((_, { velocity }) => { if (velocity.y > dragVelocityThreshold) { onClose(); } else { const sheetHeight = getSheetHeight(sheetRef); const currentY = y.get(); let yTo = 0; const snapPoints = snapPointsProp ? getSnapPoints({ snapPointsProp, sheetHeight }) : void 0; if (snapPoints) { const { snapY, snapIndex } = getClosestSnapPoint({ snapPoints, currentY, sheetHeight, detent }); yTo = snapY; onSnap == null ? void 0 : onSnap(snapIndex); } else if (currentY / sheetHeight > dragCloseThreshold) { yTo = sheetHeight; } animate(y, yTo, animationOptions); if (yTo + 1 >= sheetHeight) { onClose(); } } indicatorRotation.set(0); }); useEffect(() => { if (snapPointsProp && onSnap) { onSnap(isOpen ? initialSnap : snapPointsProp.length - 1); } }, [isOpen]); useEffect(() => { if (!isOpen) return; let yTo = 0; if (snapPointsProp) { const snapIndex = initialSnap; const sheetHeight = getSheetHeight(sheetRef); const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight }); const snapY = getSnapY({ sheetHeight, snapPoints, snapIndex }); if (snapY !== null) { yTo = snapY; } } animate(y, yTo, animationOptions); }, [isOpen]); useImperativeHandle(ref, () => ({ y, snapTo: (snapIndex) => { if (!snapPointsProp) { console.warn("Snapping is not possible without `snapPoints` prop."); return; } const sheetHeight = getSheetHeight(sheetRef); const snapPoints = getSnapPoints({ snapPointsProp, sheetHeight }); const snapY = getSnapY({ sheetHeight, snapPoints, snapIndex }); if (snapY !== null) { animate(y, snapY, animationOptions); onSnap == null ? void 0 : onSnap(snapIndex); if (snapY + 1 >= sheetHeight) { onClose(); } } } })); if (rootId) { console.warn( "The `rootId` prop is deprecated and will be removed in the next major version. Use `modalEffectRootId` instead." ); } useModalEffect({ y, sheetRef, snapPointsProp, rootId: rootId || modalEffectRootId, startThreshold: modalEffectThreshold }); usePreventScroll({ isDisabled: disableScrollLocking || !isOpen }); const dragProps = useMemo(() => { const dragProps2 = { drag: "y", dragElastic: 0, dragMomentum: false, dragPropagation: false, onDrag, onDragStart, onDragEnd }; return disableDrag ? void 0 : dragProps2; }, [disableDrag, windowHeight]); const context = { y, sheetRef, isOpen, initialSnap, detent, indicatorRotation, callbacks, dragProps, windowHeight, animationOptions, reduceMotion, disableDrag }; const sheet = /* @__PURE__ */ React2.createElement(SheetContext.Provider, { value: context }, /* @__PURE__ */ React2.createElement( motion.div, { ...rest, ref, style: { ...styles.wrapper, zIndex, visibility, ...style } }, /* @__PURE__ */ React2.createElement(AnimatePresence, null, isOpen ? /* @__PURE__ */ React2.createElement(SheetScrollerContextProvider, null, Children.map( children, (child, i) => cloneElement(child, { key: `sheet-child-${i}` }) )) : null) )); if (IS_SSR) return sheet; return createPortal(sheet, mountPoint ?? document.body); } ); Sheet.displayName = "Sheet"; function useEventCallbacks(isOpen, callbacks) { const prevOpen = usePrevious(isOpen); const didOpen = useRef(false); const handleAnimationComplete = useCallback(() => { var _a, _b, _c, _d; if (!didOpen.current) { (_b = (_a = callbacks.current).onOpenEnd) == null ? void 0 : _b.call(_a); didOpen.current = true; } else { (_d = (_c = callbacks.current).onCloseEnd) == null ? void 0 : _d.call(_c); didOpen.current = false; } }, [isOpen, prevOpen]); useEffect(() => { var _a, _b, _c, _d; if (!prevOpen && isOpen) { (_b = (_a = callbacks.current).onOpenStart) == null ? void 0 : _b.call(_a); } else if (!isOpen && prevOpen) { (_d = (_c = callbacks.current).onCloseStart) == null ? void 0 : _d.call(_c); } }, [isOpen, prevOpen]); return { handleAnimationComplete }; } function usePrevious(state) { const ref = useRef(void 0); useEffect(() => { ref.current = state; }); return ref.current; } // src/SheetContainer.tsx var SheetContainer = forwardRef( ({ children, style, className = "", ...rest }, ref) => { const { y, isOpen, callbacks, sheetRef, windowHeight, detent, animationOptions } = useSheetContext(); const { handleAnimationComplete } = useEventCallbacks(isOpen, callbacks); const height = detent === "full-height" ? MAX_HEIGHT : void 0; const maxHeight = detent === "content-height" ? MAX_HEIGHT : void 0; return /* @__PURE__ */ React2.createElement( motion.div, { ...rest, ref: mergeRefs([sheetRef, ref]), className: `react-modal-sheet-container ${className}`, style: { ...styles.container, height, maxHeight, ...style, y }, initial: { y: windowHeight }, exit: { y: windowHeight, transition: animationOptions }, onAnimationComplete: handleAnimationComplete }, children ); } ); SheetContainer.displayName = "SheetContainer"; var constraints = { bottom: 0, top: 0, left: 0, right: 0 }; function useDragConstraints() { const constraintsRef = useRef(null); const onMeasureDragConstraints = useCallback(() => constraints, []); return { constraintsRef, onMeasureDragConstraints }; } // src/SheetContent.tsx var SheetContent = forwardRef( ({ children, style, disableDrag, className = "", ...rest }, ref) => { const sheetContext = useSheetContext(); const sheetScrollerContext = useSheetScrollerContext(); const { constraintsRef, onMeasureDragConstraints } = useDragConstraints(); const dragProps = disableDrag || sheetScrollerContext.disableDrag ? void 0 : sheetContext.dragProps; return /* @__PURE__ */ React2.createElement( motion.div, { ...rest, ref: mergeRefs([ref, constraintsRef]), className: `react-modal-sheet-content ${className}`, style: { ...styles.content, ...style }, ...dragProps, dragConstraints: constraintsRef, onMeasureDragConstraints }, children ); } ); SheetContent.displayName = "SheetContent"; var SheetHeader = forwardRef( ({ children, style, disableDrag, ...rest }, ref) => { const { indicatorRotation, dragProps } = useSheetContext(); const { constraintsRef, onMeasureDragConstraints } = useDragConstraints(); const _dragProps = disableDrag ? void 0 : dragProps; const indicator1Transform = useTransform( indicatorRotation, (r) => `translateX(2px) rotate(${r}deg)` ); const indicator2Transform = useTransform( indicatorRotation, (r) => `translateX(-2px) rotate(${ -1 * r}deg)` ); return /* @__PURE__ */ React2.createElement( motion.div, { ...rest, ref: mergeRefs([ref, constraintsRef]), style: { ...styles.headerWrapper, ...style }, ..._dragProps, dragConstraints: constraintsRef, onMeasureDragConstraints }, children || /* @__PURE__ */ React2.createElement("div", { className: "react-modal-sheet-header", style: styles.header }, /* @__PURE__ */ React2.createElement( motion.span, { className: "react-modal-sheet-drag-indicator", style: { ...styles.indicator, transform: indicator1Transform } } ), /* @__PURE__ */ React2.createElement( motion.span, { className: "react-modal-sheet-drag-indicator", style: { ...styles.indicator, transform: indicator2Transform } } )) ); } ); SheetHeader.displayName = "SheetHeader"; var isClickable = (props) => !!props.onClick || !!props.onTap; var SheetBackdrop = forwardRef( ({ style = {}, className = "", ...rest }, ref) => { const Comp = isClickable(rest) ? motion.button : motion.div; const pointerEvents = isClickable(rest) ? "auto" : "none"; return /* @__PURE__ */ React2.createElement( Comp, { ...rest, ref, className: `react-modal-sheet-backdrop ${className}`, style: { ...styles.backdrop, ...style, pointerEvents }, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } } ); } ); SheetBackdrop.displayName = "SheetBackdrop"; var SheetScroller = forwardRef( ({ autoPadding = true, disableScroll = false, draggableAt = "top", children, style: styleProp, className = "", ...rest }, ref) => { const { y } = useSheetContext(); const { setDragEnabled, setDragDisabled } = useSheetScrollerContext(); function determineDragState(element) { if (disableScroll) { setDragEnabled(); return; } const { scrollTop, scrollHeight, clientHeight } = element; const isScrollable2 = scrollHeight > clientHeight; if (!isScrollable2) return; const isAtTop = scrollTop <= 0; const isAtBottom = scrollHeight - scrollTop === clientHeight; const shouldEnable = draggableAt === "top" && isAtTop || draggableAt === "bottom" && isAtBottom || draggableAt === "both" && (isAtTop || isAtBottom); if (shouldEnable) { setDragEnabled(); } else { setDragDisabled(); } } function onScroll(event) { determineDragState(event.currentTarget); if (rest.onScroll) rest.onScroll(event); } function onTouchStart(event) { determineDragState(event.currentTarget); if (rest.onTouchStart) rest.onTouchStart(event); } const scrollProps = isTouchDevice() ? { onScroll, onTouchStart } : void 0; const style = { ...styles.scroller, ...styleProp }; if (autoPadding) { style.paddingBottom = y; } if (disableScroll) { style.overflowY = "hidden"; } return /* @__PURE__ */ React2.createElement( motion.div, { ...rest, ref, className: `react-modal-sheet-scroller ${className}`, style, ...scrollProps }, children ); } ); SheetScroller.displayName = "SheetScroller"; // src/index.tsx var Sheet2 = Object.assign(Sheet, { Container: SheetContainer, Header: SheetHeader, Content: SheetContent, Backdrop: SheetBackdrop, Scroller: SheetScroller }); export { Sheet2 as Sheet }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map