@samline/drawer 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ function __insertCSS(code) {
10
10
 
11
11
  import * as DialogPrimitive from '@radix-ui/react-dialog';
12
12
  import * as React from 'react';
13
- import React__default, { useLayoutEffect, useEffect, useMemo } from 'react';
13
+ import React__default, { useRef, useLayoutEffect, useEffect, useMemo } from 'react';
14
14
  import { createRoot } from 'react-dom/client';
15
15
 
16
16
  const DEFAULT_OPTIONS = {
@@ -193,30 +193,38 @@ const nonTextInputTypes = new Set([
193
193
  'submit',
194
194
  'reset'
195
195
  ]);
196
- // The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
197
- let preventScrollCount = 0;
198
- let restore;
196
+ const activePreventScrollLocks = new Set();
197
+ let activePreventScrollRestore = null;
198
+ function acquirePreventScrollLock(lockId) {
199
+ activePreventScrollLocks.add(lockId);
200
+ if (activePreventScrollLocks.size === 1 && isIOS()) {
201
+ activePreventScrollRestore = preventScrollMobileSafari();
202
+ }
203
+ }
204
+ function releasePreventScrollLock(lockId) {
205
+ activePreventScrollLocks.delete(lockId);
206
+ if (activePreventScrollLocks.size === 0) {
207
+ activePreventScrollRestore == null ? void 0 : activePreventScrollRestore();
208
+ activePreventScrollRestore = null;
209
+ }
210
+ }
199
211
  /**
200
212
  * Prevents scrolling on the document body on mount, and
201
213
  * restores it on unmount. Also ensures that content does not
202
214
  * shift due to the scrollbars disappearing.
203
215
  */ function usePreventScroll(options = {}) {
204
216
  let { isDisabled } = options;
217
+ const lockIdRef = useRef();
218
+ if (!lockIdRef.current) {
219
+ lockIdRef.current = Symbol('drawer-prevent-scroll-lock');
220
+ }
205
221
  useIsomorphicLayoutEffect(()=>{
206
222
  if (isDisabled) {
207
223
  return;
208
224
  }
209
- preventScrollCount++;
210
- if (preventScrollCount === 1) {
211
- if (isIOS()) {
212
- restore = preventScrollMobileSafari();
213
- }
214
- }
225
+ acquirePreventScrollLock(lockIdRef.current);
215
226
  return ()=>{
216
- preventScrollCount--;
217
- if (preventScrollCount === 0) {
218
- restore == null ? void 0 : restore();
219
- }
227
+ releasePreventScrollLock(lockIdRef.current);
220
228
  };
221
229
  }, [
222
230
  isDisabled
@@ -951,12 +959,23 @@ function useScaleBackground() {
951
959
  const { direction, isOpen, shouldScaleBackground, setBackgroundColorOnScale, noBodyStyles } = useDrawerContext();
952
960
  const timeoutIdRef = React__default.useRef(null);
953
961
  const initialBackgroundColor = useMemo(()=>document.body.style.backgroundColor, []);
962
+ React__default.useEffect(()=>{
963
+ return ()=>{
964
+ if (timeoutIdRef.current !== null) {
965
+ window.clearTimeout(timeoutIdRef.current);
966
+ timeoutIdRef.current = null;
967
+ }
968
+ };
969
+ }, []);
954
970
  function getScale() {
955
971
  return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
956
972
  }
957
973
  React__default.useEffect(()=>{
958
974
  if (isOpen && shouldScaleBackground) {
959
- if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
975
+ if (timeoutIdRef.current !== null) {
976
+ clearTimeout(timeoutIdRef.current);
977
+ timeoutIdRef.current = null;
978
+ }
960
979
  const wrapper = document.querySelector('[data-drawer-wrapper]');
961
980
  if (!wrapper) return;
962
981
  chain(setBackgroundColorOnScale && !noBodyStyles ? assignStyle(document.body, {
@@ -984,6 +1003,7 @@ function useScaleBackground() {
984
1003
  } else {
985
1004
  document.body.style.removeProperty('background');
986
1005
  }
1006
+ timeoutIdRef.current = null;
987
1007
  }, TRANSITIONS.DURATION * 1000);
988
1008
  };
989
1009
  }
@@ -995,6 +1015,14 @@ function useScaleBackground() {
995
1015
  }
996
1016
 
997
1017
  let previousBodyPosition = null;
1018
+ const activeBodyPositionLocks = new Set();
1019
+ let bodyPositionTimeoutId = null;
1020
+ function clearBodyPositionTimeout() {
1021
+ if (bodyPositionTimeoutId !== null) {
1022
+ window.clearTimeout(bodyPositionTimeoutId);
1023
+ bodyPositionTimeoutId = null;
1024
+ }
1025
+ }
998
1026
  /**
999
1027
  * This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
1000
1028
  * 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.
@@ -1002,9 +1030,18 @@ let previousBodyPosition = null;
1002
1030
  */ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
1003
1031
  const [activeUrl, setActiveUrl] = React__default.useState(()=>typeof window !== 'undefined' ? window.location.href : '');
1004
1032
  const scrollPos = React__default.useRef(0);
1033
+ const lockIdRef = React__default.useRef();
1034
+ if (!lockIdRef.current) {
1035
+ lockIdRef.current = Symbol('drawer-body-position-lock');
1036
+ }
1005
1037
  const setPositionFixed = React__default.useCallback(()=>{
1006
1038
  // All browsers on iOS will return true here.
1007
1039
  if (!isSafari()) return;
1040
+ const lockId = lockIdRef.current;
1041
+ if (activeBodyPositionLocks.has(lockId)) {
1042
+ return;
1043
+ }
1044
+ activeBodyPositionLocks.add(lockId);
1008
1045
  // If previousBodyPosition is already set, don't set it again.
1009
1046
  if (previousBodyPosition === null && isOpen && !noBodyStyles) {
1010
1047
  previousBodyPosition = {
@@ -1023,7 +1060,8 @@ let previousBodyPosition = null;
1023
1060
  right: '0px',
1024
1061
  height: 'auto'
1025
1062
  });
1026
- window.setTimeout(()=>window.requestAnimationFrame(()=>{
1063
+ clearBodyPositionTimeout();
1064
+ bodyPositionTimeoutId = window.setTimeout(()=>window.requestAnimationFrame(()=>{
1027
1065
  // Attempt to check if the bottom bar appeared due to the position change
1028
1066
  const bottomBarHeight = innerHeight - window.innerHeight;
1029
1067
  if (bottomBarHeight && scrollPos.current >= innerHeight) {
@@ -1033,12 +1071,19 @@ let previousBodyPosition = null;
1033
1071
  }), 300);
1034
1072
  }
1035
1073
  }, [
1036
- isOpen
1074
+ isOpen,
1075
+ noBodyStyles
1037
1076
  ]);
1038
1077
  const restorePositionSetting = React__default.useCallback(()=>{
1039
1078
  // All browsers on iOS will return true here.
1040
1079
  if (!isSafari()) return;
1080
+ const lockId = lockIdRef.current;
1081
+ activeBodyPositionLocks.delete(lockId);
1082
+ if (activeBodyPositionLocks.size > 0) {
1083
+ return;
1084
+ }
1041
1085
  if (previousBodyPosition !== null && !noBodyStyles) {
1086
+ clearBodyPositionTimeout();
1042
1087
  // Convert the position from "px" to Int
1043
1088
  const y = -parseInt(document.body.style.top, 10);
1044
1089
  const x = -parseInt(document.body.style.left, 10);
@@ -1054,7 +1099,9 @@ let previousBodyPosition = null;
1054
1099
  previousBodyPosition = null;
1055
1100
  }
1056
1101
  }, [
1057
- activeUrl
1102
+ activeUrl,
1103
+ noBodyStyles,
1104
+ setActiveUrl
1058
1105
  ]);
1059
1106
  React__default.useEffect(()=>{
1060
1107
  function onScroll() {
@@ -1406,6 +1453,12 @@ function getDragPermission({ targetTagName, hasNoDragAttribute, direction, timeS
1406
1453
 
1407
1454
  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 }) {
1408
1455
  var _drawerRef_current, _drawerRef_current1;
1456
+ const animationEndTimeoutRef = React__default.useRef(null);
1457
+ const nonModalPointerEventsRafRef = React__default.useRef(null);
1458
+ const shouldAnimateRafRef = React__default.useRef(null);
1459
+ const snapPointsResetTimeoutRef = React__default.useRef(null);
1460
+ const justReleasedTimeoutRef = React__default.useRef(null);
1461
+ const touchEndHandlerRef = React__default.useRef(null);
1409
1462
  const [isOpen = false, setIsOpen] = useControllableState({
1410
1463
  defaultProp: defaultOpen,
1411
1464
  prop: openProp,
@@ -1414,13 +1467,20 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1414
1467
  if (!o && !nested) {
1415
1468
  restorePositionSetting();
1416
1469
  }
1417
- setTimeout(()=>{
1470
+ if (animationEndTimeoutRef.current !== null) {
1471
+ window.clearTimeout(animationEndTimeoutRef.current);
1472
+ }
1473
+ animationEndTimeoutRef.current = window.setTimeout(()=>{
1418
1474
  onAnimationEnd == null ? void 0 : onAnimationEnd(o);
1419
1475
  }, TRANSITIONS.DURATION * 1000);
1420
1476
  if (o && !modal) {
1421
1477
  if (typeof window !== 'undefined') {
1422
- window.requestAnimationFrame(()=>{
1478
+ if (nonModalPointerEventsRafRef.current !== null) {
1479
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1480
+ }
1481
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1423
1482
  document.body.style.pointerEvents = 'auto';
1483
+ nonModalPointerEventsRafRef.current = null;
1424
1484
  });
1425
1485
  }
1426
1486
  }
@@ -1529,7 +1589,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1529
1589
  dragStartTime.current = new Date();
1530
1590
  // iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
1531
1591
  if (isIOS()) {
1532
- window.addEventListener('touchend', ()=>isAllowedToDrag.current = false, {
1592
+ if (touchEndHandlerRef.current) {
1593
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1594
+ }
1595
+ const handleTouchEnd = ()=>{
1596
+ isAllowedToDrag.current = false;
1597
+ touchEndHandlerRef.current = null;
1598
+ };
1599
+ touchEndHandlerRef.current = handleTouchEnd;
1600
+ window.addEventListener('touchend', handleTouchEnd, {
1533
1601
  once: true
1534
1602
  });
1535
1603
  }
@@ -1646,9 +1714,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1646
1714
  }
1647
1715
  }
1648
1716
  React__default.useEffect(()=>{
1649
- window.requestAnimationFrame(()=>{
1717
+ shouldAnimateRafRef.current = window.requestAnimationFrame(()=>{
1650
1718
  shouldAnimate.current = true;
1651
1719
  });
1720
+ return ()=>{
1721
+ if (shouldAnimateRafRef.current !== null) {
1722
+ window.cancelAnimationFrame(shouldAnimateRafRef.current);
1723
+ shouldAnimateRafRef.current = null;
1724
+ }
1725
+ };
1652
1726
  }, []);
1653
1727
  React__default.useEffect(()=>{
1654
1728
  var _window_visualViewport;
@@ -1698,10 +1772,14 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1698
1772
  if (!fromWithin) {
1699
1773
  setIsOpen(false);
1700
1774
  }
1701
- setTimeout(()=>{
1775
+ if (snapPointsResetTimeoutRef.current !== null) {
1776
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1777
+ }
1778
+ snapPointsResetTimeoutRef.current = window.setTimeout(()=>{
1702
1779
  if (snapPoints) {
1703
1780
  setActiveSnapPoint(snapPoints[0]);
1704
1781
  }
1782
+ snapPointsResetTimeoutRef.current = null;
1705
1783
  }, TRANSITIONS.DURATION * 1000); // seconds to ms
1706
1784
  }
1707
1785
  function resetDrawer() {
@@ -1775,8 +1853,12 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1775
1853
  if (releaseResult.shouldPreventFocus) {
1776
1854
  // `justReleased` is needed to prevent the drawer from focusing on an input when the drag ends, as it's not the intent most of the time.
1777
1855
  setJustReleased(true);
1778
- setTimeout(()=>{
1856
+ if (justReleasedTimeoutRef.current !== null) {
1857
+ window.clearTimeout(justReleasedTimeoutRef.current);
1858
+ }
1859
+ justReleasedTimeoutRef.current = window.setTimeout(()=>{
1779
1860
  setJustReleased(false);
1861
+ justReleasedTimeoutRef.current = null;
1780
1862
  }, 200);
1781
1863
  }
1782
1864
  if (releaseResult.action === 'close') {
@@ -1853,12 +1935,45 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1853
1935
  });
1854
1936
  }
1855
1937
  }
1938
+ React__default.useEffect(()=>{
1939
+ return ()=>{
1940
+ if (animationEndTimeoutRef.current !== null) {
1941
+ window.clearTimeout(animationEndTimeoutRef.current);
1942
+ }
1943
+ if (nonModalPointerEventsRafRef.current !== null) {
1944
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1945
+ }
1946
+ if (snapPointsResetTimeoutRef.current !== null) {
1947
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1948
+ }
1949
+ if (justReleasedTimeoutRef.current !== null) {
1950
+ window.clearTimeout(justReleasedTimeoutRef.current);
1951
+ }
1952
+ if (nestedOpenChangeTimer.current) {
1953
+ window.clearTimeout(nestedOpenChangeTimer.current);
1954
+ }
1955
+ if (touchEndHandlerRef.current) {
1956
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1957
+ touchEndHandlerRef.current = null;
1958
+ }
1959
+ };
1960
+ }, []);
1856
1961
  React__default.useEffect(()=>{
1857
1962
  if (!modal) {
1858
1963
  // Need to do this manually unfortunately
1859
- window.requestAnimationFrame(()=>{
1964
+ if (nonModalPointerEventsRafRef.current !== null) {
1965
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1966
+ }
1967
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1860
1968
  document.body.style.pointerEvents = 'auto';
1969
+ nonModalPointerEventsRafRef.current = null;
1861
1970
  });
1971
+ return ()=>{
1972
+ if (nonModalPointerEventsRafRef.current !== null) {
1973
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1974
+ nonModalPointerEventsRafRef.current = null;
1975
+ }
1976
+ };
1862
1977
  }
1863
1978
  }, [
1864
1979
  modal
@@ -1942,15 +2057,25 @@ const Content = /*#__PURE__*/ React__default.forwardRef(function({ onPointerDown
1942
2057
  const pointerStartRef = React__default.useRef(null);
1943
2058
  const lastKnownPointerEventRef = React__default.useRef(null);
1944
2059
  const wasBeyondThePointRef = React__default.useRef(false);
2060
+ const delayedSnapPointsRafRef = React__default.useRef(null);
1945
2061
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1946
2062
  useScaleBackground();
1947
2063
  React__default.useEffect(()=>{
1948
2064
  if (hasSnapPoints) {
1949
- window.requestAnimationFrame(()=>{
2065
+ delayedSnapPointsRafRef.current = window.requestAnimationFrame(()=>{
1950
2066
  setDelayedSnapPoints(true);
2067
+ delayedSnapPointsRafRef.current = null;
1951
2068
  });
1952
2069
  }
1953
- }, []);
2070
+ return ()=>{
2071
+ if (delayedSnapPointsRafRef.current !== null) {
2072
+ window.cancelAnimationFrame(delayedSnapPointsRafRef.current);
2073
+ delayedSnapPointsRafRef.current = null;
2074
+ }
2075
+ };
2076
+ }, [
2077
+ hasSnapPoints
2078
+ ]);
1954
2079
  function handleOnPointerUp(event) {
1955
2080
  pointerStartRef.current = null;
1956
2081
  wasBeyondThePointRef.current = false;
@@ -2051,6 +2176,7 @@ const DOUBLE_TAP_TIMEOUT = 120;
2051
2176
  const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle = false, children, ...rest }, ref) {
2052
2177
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag } = useDrawerContext();
2053
2178
  const closeTimeoutIdRef = React__default.useRef(null);
2179
+ const cycleTimeoutIdRef = React__default.useRef(null);
2054
2180
  const shouldCancelInteractionRef = React__default.useRef(false);
2055
2181
  function handleStartCycle() {
2056
2182
  // Stop if this is the second click of a double click
@@ -2058,7 +2184,7 @@ const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle =
2058
2184
  handleCancelInteraction();
2059
2185
  return;
2060
2186
  }
2061
- window.setTimeout(()=>{
2187
+ cycleTimeoutIdRef.current = window.setTimeout(()=>{
2062
2188
  handleCycleSnapPoints();
2063
2189
  }, DOUBLE_TAP_TIMEOUT);
2064
2190
  }
@@ -2091,9 +2217,19 @@ const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle =
2091
2217
  function handleCancelInteraction() {
2092
2218
  if (closeTimeoutIdRef.current) {
2093
2219
  window.clearTimeout(closeTimeoutIdRef.current);
2220
+ closeTimeoutIdRef.current = null;
2221
+ }
2222
+ if (cycleTimeoutIdRef.current) {
2223
+ window.clearTimeout(cycleTimeoutIdRef.current);
2224
+ cycleTimeoutIdRef.current = null;
2094
2225
  }
2095
2226
  shouldCancelInteractionRef.current = false;
2096
2227
  }
2228
+ React__default.useEffect(()=>{
2229
+ return ()=>{
2230
+ handleCancelInteraction();
2231
+ };
2232
+ }, []);
2097
2233
  return /*#__PURE__*/ React__default.createElement("div", {
2098
2234
  onClick: handleStartCycle,
2099
2235
  onPointerCancel: handleCancelInteraction,