react-modal-sheet
Version:
Flexible bottom sheet component for your React apps
1,065 lines (1,051 loc) • 32.8 kB
JavaScript
var reactDom = require('react-dom');
var React2 = require('react');
var react = require('motion/react');
var motion = require('motion');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var React2__default = /*#__PURE__*/_interopDefault(React2);
// 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 = React2.createContext(
void 0
);
function useSheetContext() {
const context = React2.useContext(SheetContext);
if (!context) throw new Error("Sheet context error");
return context;
}
var SheetScrollerContext = React2.createContext(void 0);
function SheetScrollerContextProvider({
children
}) {
const sheetContext = useSheetContext();
const [disableDrag, setDisableDrag] = React2.useState(!!sheetContext.disableDrag);
function setDragEnabled() {
if (!sheetContext.disableDrag) setDisableDrag(false);
}
function setDragDisabled() {
if (!disableDrag) setDisableDrag(true);
}
return /* @__PURE__ */ React2__default.default.createElement(
SheetScrollerContext.Provider,
{
value: { disableDrag, setDragEnabled, setDragDisabled }
},
children
);
}
function useSheetScrollerContext() {
const context = React2.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 ? React2.useEffect : React2.useLayoutEffect;
function useSafeAreaInsets() {
const [insets] = React2.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 = motion.transform(progress, [0, 1], [0, 24 + insetTop]);
const s = motion.transform(progress, [0, 1], [1, (pageWidth - 16) / pageWidth]);
const borderRadius = motion.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] = React2.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 = React2.useRef(void 0);
useIsomorphicLayoutEffect(() => {
handlerRef.current = handler;
});
return React2.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 = React2.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 = React2.useRef(null);
const indicatorRotation = react.useMotionValue(0);
const { height: windowHeight } = useDimensions();
const shouldReduceMotion = react.useReducedMotion();
const reduceMotion = Boolean(prefersReducedMotion || shouldReduceMotion);
const animationOptions = {
type: "tween",
...reduceMotion ? REDUCED_MOTION_TWEEN_CONFIG : tweenConfig
};
const y = react.useMotionValue(windowHeight);
const zIndex = react.useTransform(
y,
(value) => value + 2 >= windowHeight ? -1 : (style == null ? void 0 : style.zIndex) ?? 9999
);
const visibility = react.useTransform(
y,
(value) => value + 2 >= windowHeight ? "hidden" : "visible"
);
const callbacks = React2.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;
}
react.animate(y, yTo, animationOptions);
if (yTo + 1 >= sheetHeight) {
onClose();
}
}
indicatorRotation.set(0);
});
React2.useEffect(() => {
if (snapPointsProp && onSnap) {
onSnap(isOpen ? initialSnap : snapPointsProp.length - 1);
}
}, [isOpen]);
React2.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;
}
}
react.animate(y, yTo, animationOptions);
}, [isOpen]);
React2.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) {
react.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 = React2.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__default.default.createElement(SheetContext.Provider, { value: context }, /* @__PURE__ */ React2__default.default.createElement(
react.motion.div,
{
...rest,
ref,
style: { ...styles.wrapper, zIndex, visibility, ...style }
},
/* @__PURE__ */ React2__default.default.createElement(react.AnimatePresence, null, isOpen ? /* @__PURE__ */ React2__default.default.createElement(SheetScrollerContextProvider, null, React2.Children.map(
children,
(child, i) => React2.cloneElement(child, { key: `sheet-child-${i}` })
)) : null)
));
if (IS_SSR) return sheet;
return reactDom.createPortal(sheet, mountPoint ?? document.body);
}
);
Sheet.displayName = "Sheet";
function useEventCallbacks(isOpen, callbacks) {
const prevOpen = usePrevious(isOpen);
const didOpen = React2.useRef(false);
const handleAnimationComplete = React2.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]);
React2.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 = React2.useRef(void 0);
React2.useEffect(() => {
ref.current = state;
});
return ref.current;
}
// src/SheetContainer.tsx
var SheetContainer = React2.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__default.default.createElement(
react.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 = React2.useRef(null);
const onMeasureDragConstraints = React2.useCallback(() => constraints, []);
return { constraintsRef, onMeasureDragConstraints };
}
// src/SheetContent.tsx
var SheetContent = React2.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__default.default.createElement(
react.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 = React2.forwardRef(
({ children, style, disableDrag, ...rest }, ref) => {
const { indicatorRotation, dragProps } = useSheetContext();
const { constraintsRef, onMeasureDragConstraints } = useDragConstraints();
const _dragProps = disableDrag ? void 0 : dragProps;
const indicator1Transform = react.useTransform(
indicatorRotation,
(r) => `translateX(2px) rotate(${r}deg)`
);
const indicator2Transform = react.useTransform(
indicatorRotation,
(r) => `translateX(-2px) rotate(${ -1 * r}deg)`
);
return /* @__PURE__ */ React2__default.default.createElement(
react.motion.div,
{
...rest,
ref: mergeRefs([ref, constraintsRef]),
style: { ...styles.headerWrapper, ...style },
..._dragProps,
dragConstraints: constraintsRef,
onMeasureDragConstraints
},
children || /* @__PURE__ */ React2__default.default.createElement("div", { className: "react-modal-sheet-header", style: styles.header }, /* @__PURE__ */ React2__default.default.createElement(
react.motion.span,
{
className: "react-modal-sheet-drag-indicator",
style: { ...styles.indicator, transform: indicator1Transform }
}
), /* @__PURE__ */ React2__default.default.createElement(
react.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 = React2.forwardRef(
({ style = {}, className = "", ...rest }, ref) => {
const Comp = isClickable(rest) ? react.motion.button : react.motion.div;
const pointerEvents = isClickable(rest) ? "auto" : "none";
return /* @__PURE__ */ React2__default.default.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 = React2.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__default.default.createElement(
react.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
});
exports.Sheet = Sheet2;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map
;