@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.
@@ -362,30 +362,38 @@ const nonTextInputTypes = new Set([
362
362
  'submit',
363
363
  'reset'
364
364
  ]);
365
- // The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
366
- let preventScrollCount = 0;
367
- let restore;
365
+ const activePreventScrollLocks = new Set();
366
+ let activePreventScrollRestore = null;
367
+ function acquirePreventScrollLock(lockId) {
368
+ activePreventScrollLocks.add(lockId);
369
+ if (activePreventScrollLocks.size === 1 && isIOS()) {
370
+ activePreventScrollRestore = preventScrollMobileSafari();
371
+ }
372
+ }
373
+ function releasePreventScrollLock(lockId) {
374
+ activePreventScrollLocks.delete(lockId);
375
+ if (activePreventScrollLocks.size === 0) {
376
+ activePreventScrollRestore == null ? void 0 : activePreventScrollRestore();
377
+ activePreventScrollRestore = null;
378
+ }
379
+ }
368
380
  /**
369
381
  * Prevents scrolling on the document body on mount, and
370
382
  * restores it on unmount. Also ensures that content does not
371
383
  * shift due to the scrollbars disappearing.
372
384
  */ function usePreventScroll(options = {}) {
373
385
  let { isDisabled } = options;
386
+ const lockIdRef = React.useRef();
387
+ if (!lockIdRef.current) {
388
+ lockIdRef.current = Symbol('drawer-prevent-scroll-lock');
389
+ }
374
390
  useIsomorphicLayoutEffect(()=>{
375
391
  if (isDisabled) {
376
392
  return;
377
393
  }
378
- preventScrollCount++;
379
- if (preventScrollCount === 1) {
380
- if (isIOS()) {
381
- restore = preventScrollMobileSafari();
382
- }
383
- }
394
+ acquirePreventScrollLock(lockIdRef.current);
384
395
  return ()=>{
385
- preventScrollCount--;
386
- if (preventScrollCount === 0) {
387
- restore == null ? void 0 : restore();
388
- }
396
+ releasePreventScrollLock(lockIdRef.current);
389
397
  };
390
398
  }, [
391
399
  isDisabled
@@ -1026,12 +1034,23 @@ function useScaleBackground() {
1026
1034
  const { direction, isOpen, shouldScaleBackground, setBackgroundColorOnScale, noBodyStyles } = useDrawerContext();
1027
1035
  const timeoutIdRef = React__namespace.default.useRef(null);
1028
1036
  const initialBackgroundColor = React.useMemo(()=>document.body.style.backgroundColor, []);
1037
+ React__namespace.default.useEffect(()=>{
1038
+ return ()=>{
1039
+ if (timeoutIdRef.current !== null) {
1040
+ window.clearTimeout(timeoutIdRef.current);
1041
+ timeoutIdRef.current = null;
1042
+ }
1043
+ };
1044
+ }, []);
1029
1045
  function getScale() {
1030
1046
  return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
1031
1047
  }
1032
1048
  React__namespace.default.useEffect(()=>{
1033
1049
  if (isOpen && shouldScaleBackground) {
1034
- if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
1050
+ if (timeoutIdRef.current !== null) {
1051
+ clearTimeout(timeoutIdRef.current);
1052
+ timeoutIdRef.current = null;
1053
+ }
1035
1054
  const wrapper = document.querySelector('[data-drawer-wrapper]');
1036
1055
  if (!wrapper) return;
1037
1056
  chain$1(setBackgroundColorOnScale && !noBodyStyles ? assignStyle(document.body, {
@@ -1059,6 +1078,7 @@ function useScaleBackground() {
1059
1078
  } else {
1060
1079
  document.body.style.removeProperty('background');
1061
1080
  }
1081
+ timeoutIdRef.current = null;
1062
1082
  }, TRANSITIONS.DURATION * 1000);
1063
1083
  };
1064
1084
  }
@@ -1070,6 +1090,14 @@ function useScaleBackground() {
1070
1090
  }
1071
1091
 
1072
1092
  let previousBodyPosition = null;
1093
+ const activeBodyPositionLocks = new Set();
1094
+ let bodyPositionTimeoutId = null;
1095
+ function clearBodyPositionTimeout() {
1096
+ if (bodyPositionTimeoutId !== null) {
1097
+ window.clearTimeout(bodyPositionTimeoutId);
1098
+ bodyPositionTimeoutId = null;
1099
+ }
1100
+ }
1073
1101
  /**
1074
1102
  * This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
1075
1103
  * 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.
@@ -1077,9 +1105,18 @@ let previousBodyPosition = null;
1077
1105
  */ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
1078
1106
  const [activeUrl, setActiveUrl] = React__namespace.default.useState(()=>typeof window !== 'undefined' ? window.location.href : '');
1079
1107
  const scrollPos = React__namespace.default.useRef(0);
1108
+ const lockIdRef = React__namespace.default.useRef();
1109
+ if (!lockIdRef.current) {
1110
+ lockIdRef.current = Symbol('drawer-body-position-lock');
1111
+ }
1080
1112
  const setPositionFixed = React__namespace.default.useCallback(()=>{
1081
1113
  // All browsers on iOS will return true here.
1082
1114
  if (!isSafari()) return;
1115
+ const lockId = lockIdRef.current;
1116
+ if (activeBodyPositionLocks.has(lockId)) {
1117
+ return;
1118
+ }
1119
+ activeBodyPositionLocks.add(lockId);
1083
1120
  // If previousBodyPosition is already set, don't set it again.
1084
1121
  if (previousBodyPosition === null && isOpen && !noBodyStyles) {
1085
1122
  previousBodyPosition = {
@@ -1098,7 +1135,8 @@ let previousBodyPosition = null;
1098
1135
  right: '0px',
1099
1136
  height: 'auto'
1100
1137
  });
1101
- window.setTimeout(()=>window.requestAnimationFrame(()=>{
1138
+ clearBodyPositionTimeout();
1139
+ bodyPositionTimeoutId = window.setTimeout(()=>window.requestAnimationFrame(()=>{
1102
1140
  // Attempt to check if the bottom bar appeared due to the position change
1103
1141
  const bottomBarHeight = innerHeight - window.innerHeight;
1104
1142
  if (bottomBarHeight && scrollPos.current >= innerHeight) {
@@ -1108,12 +1146,19 @@ let previousBodyPosition = null;
1108
1146
  }), 300);
1109
1147
  }
1110
1148
  }, [
1111
- isOpen
1149
+ isOpen,
1150
+ noBodyStyles
1112
1151
  ]);
1113
1152
  const restorePositionSetting = React__namespace.default.useCallback(()=>{
1114
1153
  // All browsers on iOS will return true here.
1115
1154
  if (!isSafari()) return;
1155
+ const lockId = lockIdRef.current;
1156
+ activeBodyPositionLocks.delete(lockId);
1157
+ if (activeBodyPositionLocks.size > 0) {
1158
+ return;
1159
+ }
1116
1160
  if (previousBodyPosition !== null && !noBodyStyles) {
1161
+ clearBodyPositionTimeout();
1117
1162
  // Convert the position from "px" to Int
1118
1163
  const y = -parseInt(document.body.style.top, 10);
1119
1164
  const x = -parseInt(document.body.style.left, 10);
@@ -1129,7 +1174,9 @@ let previousBodyPosition = null;
1129
1174
  previousBodyPosition = null;
1130
1175
  }
1131
1176
  }, [
1132
- activeUrl
1177
+ activeUrl,
1178
+ noBodyStyles,
1179
+ setActiveUrl
1133
1180
  ]);
1134
1181
  React__namespace.default.useEffect(()=>{
1135
1182
  function onScroll() {
@@ -1425,6 +1472,12 @@ function getDragPermission({ targetTagName, hasNoDragAttribute, direction, timeS
1425
1472
 
1426
1473
  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 }) {
1427
1474
  var _drawerRef_current, _drawerRef_current1;
1475
+ const animationEndTimeoutRef = React__namespace.default.useRef(null);
1476
+ const nonModalPointerEventsRafRef = React__namespace.default.useRef(null);
1477
+ const shouldAnimateRafRef = React__namespace.default.useRef(null);
1478
+ const snapPointsResetTimeoutRef = React__namespace.default.useRef(null);
1479
+ const justReleasedTimeoutRef = React__namespace.default.useRef(null);
1480
+ const touchEndHandlerRef = React__namespace.default.useRef(null);
1428
1481
  const [isOpen = false, setIsOpen] = useControllableState({
1429
1482
  defaultProp: defaultOpen,
1430
1483
  prop: openProp,
@@ -1433,13 +1486,20 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1433
1486
  if (!o && !nested) {
1434
1487
  restorePositionSetting();
1435
1488
  }
1436
- setTimeout(()=>{
1489
+ if (animationEndTimeoutRef.current !== null) {
1490
+ window.clearTimeout(animationEndTimeoutRef.current);
1491
+ }
1492
+ animationEndTimeoutRef.current = window.setTimeout(()=>{
1437
1493
  onAnimationEnd == null ? void 0 : onAnimationEnd(o);
1438
1494
  }, TRANSITIONS.DURATION * 1000);
1439
1495
  if (o && !modal) {
1440
1496
  if (typeof window !== 'undefined') {
1441
- window.requestAnimationFrame(()=>{
1497
+ if (nonModalPointerEventsRafRef.current !== null) {
1498
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1499
+ }
1500
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1442
1501
  document.body.style.pointerEvents = 'auto';
1502
+ nonModalPointerEventsRafRef.current = null;
1443
1503
  });
1444
1504
  }
1445
1505
  }
@@ -1548,7 +1608,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1548
1608
  dragStartTime.current = new Date();
1549
1609
  // iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
1550
1610
  if (isIOS()) {
1551
- window.addEventListener('touchend', ()=>isAllowedToDrag.current = false, {
1611
+ if (touchEndHandlerRef.current) {
1612
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1613
+ }
1614
+ const handleTouchEnd = ()=>{
1615
+ isAllowedToDrag.current = false;
1616
+ touchEndHandlerRef.current = null;
1617
+ };
1618
+ touchEndHandlerRef.current = handleTouchEnd;
1619
+ window.addEventListener('touchend', handleTouchEnd, {
1552
1620
  once: true
1553
1621
  });
1554
1622
  }
@@ -1665,9 +1733,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1665
1733
  }
1666
1734
  }
1667
1735
  React__namespace.default.useEffect(()=>{
1668
- window.requestAnimationFrame(()=>{
1736
+ shouldAnimateRafRef.current = window.requestAnimationFrame(()=>{
1669
1737
  shouldAnimate.current = true;
1670
1738
  });
1739
+ return ()=>{
1740
+ if (shouldAnimateRafRef.current !== null) {
1741
+ window.cancelAnimationFrame(shouldAnimateRafRef.current);
1742
+ shouldAnimateRafRef.current = null;
1743
+ }
1744
+ };
1671
1745
  }, []);
1672
1746
  React__namespace.default.useEffect(()=>{
1673
1747
  var _window_visualViewport;
@@ -1717,10 +1791,14 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1717
1791
  if (!fromWithin) {
1718
1792
  setIsOpen(false);
1719
1793
  }
1720
- setTimeout(()=>{
1794
+ if (snapPointsResetTimeoutRef.current !== null) {
1795
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1796
+ }
1797
+ snapPointsResetTimeoutRef.current = window.setTimeout(()=>{
1721
1798
  if (snapPoints) {
1722
1799
  setActiveSnapPoint(snapPoints[0]);
1723
1800
  }
1801
+ snapPointsResetTimeoutRef.current = null;
1724
1802
  }, TRANSITIONS.DURATION * 1000); // seconds to ms
1725
1803
  }
1726
1804
  function resetDrawer() {
@@ -1794,8 +1872,12 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1794
1872
  if (releaseResult.shouldPreventFocus) {
1795
1873
  // `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.
1796
1874
  setJustReleased(true);
1797
- setTimeout(()=>{
1875
+ if (justReleasedTimeoutRef.current !== null) {
1876
+ window.clearTimeout(justReleasedTimeoutRef.current);
1877
+ }
1878
+ justReleasedTimeoutRef.current = window.setTimeout(()=>{
1798
1879
  setJustReleased(false);
1880
+ justReleasedTimeoutRef.current = null;
1799
1881
  }, 200);
1800
1882
  }
1801
1883
  if (releaseResult.action === 'close') {
@@ -1872,12 +1954,45 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1872
1954
  });
1873
1955
  }
1874
1956
  }
1957
+ React__namespace.default.useEffect(()=>{
1958
+ return ()=>{
1959
+ if (animationEndTimeoutRef.current !== null) {
1960
+ window.clearTimeout(animationEndTimeoutRef.current);
1961
+ }
1962
+ if (nonModalPointerEventsRafRef.current !== null) {
1963
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1964
+ }
1965
+ if (snapPointsResetTimeoutRef.current !== null) {
1966
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1967
+ }
1968
+ if (justReleasedTimeoutRef.current !== null) {
1969
+ window.clearTimeout(justReleasedTimeoutRef.current);
1970
+ }
1971
+ if (nestedOpenChangeTimer.current) {
1972
+ window.clearTimeout(nestedOpenChangeTimer.current);
1973
+ }
1974
+ if (touchEndHandlerRef.current) {
1975
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1976
+ touchEndHandlerRef.current = null;
1977
+ }
1978
+ };
1979
+ }, []);
1875
1980
  React__namespace.default.useEffect(()=>{
1876
1981
  if (!modal) {
1877
1982
  // Need to do this manually unfortunately
1878
- window.requestAnimationFrame(()=>{
1983
+ if (nonModalPointerEventsRafRef.current !== null) {
1984
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1985
+ }
1986
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1879
1987
  document.body.style.pointerEvents = 'auto';
1988
+ nonModalPointerEventsRafRef.current = null;
1880
1989
  });
1990
+ return ()=>{
1991
+ if (nonModalPointerEventsRafRef.current !== null) {
1992
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1993
+ nonModalPointerEventsRafRef.current = null;
1994
+ }
1995
+ };
1881
1996
  }
1882
1997
  }, [
1883
1998
  modal
@@ -1961,15 +2076,25 @@ const Content = /*#__PURE__*/ React__namespace.default.forwardRef(function({ onP
1961
2076
  const pointerStartRef = React__namespace.default.useRef(null);
1962
2077
  const lastKnownPointerEventRef = React__namespace.default.useRef(null);
1963
2078
  const wasBeyondThePointRef = React__namespace.default.useRef(false);
2079
+ const delayedSnapPointsRafRef = React__namespace.default.useRef(null);
1964
2080
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1965
2081
  useScaleBackground();
1966
2082
  React__namespace.default.useEffect(()=>{
1967
2083
  if (hasSnapPoints) {
1968
- window.requestAnimationFrame(()=>{
2084
+ delayedSnapPointsRafRef.current = window.requestAnimationFrame(()=>{
1969
2085
  setDelayedSnapPoints(true);
2086
+ delayedSnapPointsRafRef.current = null;
1970
2087
  });
1971
2088
  }
1972
- }, []);
2089
+ return ()=>{
2090
+ if (delayedSnapPointsRafRef.current !== null) {
2091
+ window.cancelAnimationFrame(delayedSnapPointsRafRef.current);
2092
+ delayedSnapPointsRafRef.current = null;
2093
+ }
2094
+ };
2095
+ }, [
2096
+ hasSnapPoints
2097
+ ]);
1973
2098
  function handleOnPointerUp(event) {
1974
2099
  pointerStartRef.current = null;
1975
2100
  wasBeyondThePointRef.current = false;
@@ -2070,6 +2195,7 @@ const DOUBLE_TAP_TIMEOUT = 120;
2070
2195
  const Handle = /*#__PURE__*/ React__namespace.default.forwardRef(function({ preventCycle = false, children, ...rest }, ref) {
2071
2196
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag } = useDrawerContext();
2072
2197
  const closeTimeoutIdRef = React__namespace.default.useRef(null);
2198
+ const cycleTimeoutIdRef = React__namespace.default.useRef(null);
2073
2199
  const shouldCancelInteractionRef = React__namespace.default.useRef(false);
2074
2200
  function handleStartCycle() {
2075
2201
  // Stop if this is the second click of a double click
@@ -2077,7 +2203,7 @@ const Handle = /*#__PURE__*/ React__namespace.default.forwardRef(function({ prev
2077
2203
  handleCancelInteraction();
2078
2204
  return;
2079
2205
  }
2080
- window.setTimeout(()=>{
2206
+ cycleTimeoutIdRef.current = window.setTimeout(()=>{
2081
2207
  handleCycleSnapPoints();
2082
2208
  }, DOUBLE_TAP_TIMEOUT);
2083
2209
  }
@@ -2110,9 +2236,19 @@ const Handle = /*#__PURE__*/ React__namespace.default.forwardRef(function({ prev
2110
2236
  function handleCancelInteraction() {
2111
2237
  if (closeTimeoutIdRef.current) {
2112
2238
  window.clearTimeout(closeTimeoutIdRef.current);
2239
+ closeTimeoutIdRef.current = null;
2240
+ }
2241
+ if (cycleTimeoutIdRef.current) {
2242
+ window.clearTimeout(cycleTimeoutIdRef.current);
2243
+ cycleTimeoutIdRef.current = null;
2113
2244
  }
2114
2245
  shouldCancelInteractionRef.current = false;
2115
2246
  }
2247
+ React__namespace.default.useEffect(()=>{
2248
+ return ()=>{
2249
+ handleCancelInteraction();
2250
+ };
2251
+ }, []);
2116
2252
  return /*#__PURE__*/ React__namespace.default.createElement("div", {
2117
2253
  onClick: handleStartCycle,
2118
2254
  onPointerCancel: handleCancelInteraction,