@samline/drawer 2.0.4 → 2.0.6

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() {
@@ -1423,8 +1470,15 @@ function getDragPermission({ targetTagName, hasNoDragAttribute, direction, timeS
1423
1470
  };
1424
1471
  }
1425
1472
 
1473
+ const useSafeLayoutEffect = typeof window === 'undefined' ? React__namespace.default.useEffect : React__namespace.default.useLayoutEffect;
1426
1474
  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
1475
  var _drawerRef_current, _drawerRef_current1;
1476
+ const animationEndTimeoutRef = React__namespace.default.useRef(null);
1477
+ const nonModalPointerEventsRafRef = React__namespace.default.useRef(null);
1478
+ const shouldAnimateRafRef = React__namespace.default.useRef(null);
1479
+ const snapPointsResetTimeoutRef = React__namespace.default.useRef(null);
1480
+ const justReleasedTimeoutRef = React__namespace.default.useRef(null);
1481
+ const touchEndHandlerRef = React__namespace.default.useRef(null);
1428
1482
  const [isOpen = false, setIsOpen] = useControllableState({
1429
1483
  defaultProp: defaultOpen,
1430
1484
  prop: openProp,
@@ -1433,13 +1487,20 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1433
1487
  if (!o && !nested) {
1434
1488
  restorePositionSetting();
1435
1489
  }
1436
- setTimeout(()=>{
1490
+ if (animationEndTimeoutRef.current !== null) {
1491
+ window.clearTimeout(animationEndTimeoutRef.current);
1492
+ }
1493
+ animationEndTimeoutRef.current = window.setTimeout(()=>{
1437
1494
  onAnimationEnd == null ? void 0 : onAnimationEnd(o);
1438
1495
  }, TRANSITIONS.DURATION * 1000);
1439
1496
  if (o && !modal) {
1440
1497
  if (typeof window !== 'undefined') {
1441
- window.requestAnimationFrame(()=>{
1498
+ if (nonModalPointerEventsRafRef.current !== null) {
1499
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1500
+ }
1501
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1442
1502
  document.body.style.pointerEvents = 'auto';
1503
+ nonModalPointerEventsRafRef.current = null;
1443
1504
  });
1444
1505
  }
1445
1506
  }
@@ -1517,19 +1578,23 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1517
1578
  noBodyStyles,
1518
1579
  autoFocus
1519
1580
  });
1520
- React__namespace.default.useEffect(()=>{
1581
+ useSafeLayoutEffect(()=>{
1521
1582
  var _drawerRef_current;
1522
1583
  if (!isOpen || !modal || autoFocus || typeof document === 'undefined') {
1523
1584
  return;
1524
1585
  }
1525
1586
  const activeElement = document.activeElement;
1526
- if (!(activeElement instanceof HTMLElement)) {
1587
+ if (!activeElement || activeElement === document.body) {
1527
1588
  return;
1528
1589
  }
1529
- if (((_drawerRef_current = drawerRef.current) == null ? void 0 : _drawerRef_current.contains(activeElement)) || activeElement.closest('[data-drawer]')) {
1590
+ const activeElementNode = activeElement;
1591
+ if (((_drawerRef_current = drawerRef.current) == null ? void 0 : _drawerRef_current.contains(activeElementNode)) || (activeElementNode.closest == null ? void 0 : activeElementNode.closest.call(activeElementNode, '[data-drawer]'))) {
1530
1592
  return;
1531
1593
  }
1532
- activeElement.blur();
1594
+ if (typeof activeElementNode.blur !== 'function') {
1595
+ return;
1596
+ }
1597
+ activeElementNode.blur();
1533
1598
  }, [
1534
1599
  autoFocus,
1535
1600
  isOpen,
@@ -1548,7 +1613,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1548
1613
  dragStartTime.current = new Date();
1549
1614
  // iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
1550
1615
  if (isIOS()) {
1551
- window.addEventListener('touchend', ()=>isAllowedToDrag.current = false, {
1616
+ if (touchEndHandlerRef.current) {
1617
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1618
+ }
1619
+ const handleTouchEnd = ()=>{
1620
+ isAllowedToDrag.current = false;
1621
+ touchEndHandlerRef.current = null;
1622
+ };
1623
+ touchEndHandlerRef.current = handleTouchEnd;
1624
+ window.addEventListener('touchend', handleTouchEnd, {
1552
1625
  once: true
1553
1626
  });
1554
1627
  }
@@ -1665,9 +1738,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1665
1738
  }
1666
1739
  }
1667
1740
  React__namespace.default.useEffect(()=>{
1668
- window.requestAnimationFrame(()=>{
1741
+ shouldAnimateRafRef.current = window.requestAnimationFrame(()=>{
1669
1742
  shouldAnimate.current = true;
1670
1743
  });
1744
+ return ()=>{
1745
+ if (shouldAnimateRafRef.current !== null) {
1746
+ window.cancelAnimationFrame(shouldAnimateRafRef.current);
1747
+ shouldAnimateRafRef.current = null;
1748
+ }
1749
+ };
1671
1750
  }, []);
1672
1751
  React__namespace.default.useEffect(()=>{
1673
1752
  var _window_visualViewport;
@@ -1717,10 +1796,14 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1717
1796
  if (!fromWithin) {
1718
1797
  setIsOpen(false);
1719
1798
  }
1720
- setTimeout(()=>{
1799
+ if (snapPointsResetTimeoutRef.current !== null) {
1800
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1801
+ }
1802
+ snapPointsResetTimeoutRef.current = window.setTimeout(()=>{
1721
1803
  if (snapPoints) {
1722
1804
  setActiveSnapPoint(snapPoints[0]);
1723
1805
  }
1806
+ snapPointsResetTimeoutRef.current = null;
1724
1807
  }, TRANSITIONS.DURATION * 1000); // seconds to ms
1725
1808
  }
1726
1809
  function resetDrawer() {
@@ -1794,8 +1877,12 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1794
1877
  if (releaseResult.shouldPreventFocus) {
1795
1878
  // `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
1879
  setJustReleased(true);
1797
- setTimeout(()=>{
1880
+ if (justReleasedTimeoutRef.current !== null) {
1881
+ window.clearTimeout(justReleasedTimeoutRef.current);
1882
+ }
1883
+ justReleasedTimeoutRef.current = window.setTimeout(()=>{
1798
1884
  setJustReleased(false);
1885
+ justReleasedTimeoutRef.current = null;
1799
1886
  }, 200);
1800
1887
  }
1801
1888
  if (releaseResult.action === 'close') {
@@ -1872,12 +1959,45 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1872
1959
  });
1873
1960
  }
1874
1961
  }
1962
+ React__namespace.default.useEffect(()=>{
1963
+ return ()=>{
1964
+ if (animationEndTimeoutRef.current !== null) {
1965
+ window.clearTimeout(animationEndTimeoutRef.current);
1966
+ }
1967
+ if (nonModalPointerEventsRafRef.current !== null) {
1968
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1969
+ }
1970
+ if (snapPointsResetTimeoutRef.current !== null) {
1971
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1972
+ }
1973
+ if (justReleasedTimeoutRef.current !== null) {
1974
+ window.clearTimeout(justReleasedTimeoutRef.current);
1975
+ }
1976
+ if (nestedOpenChangeTimer.current) {
1977
+ window.clearTimeout(nestedOpenChangeTimer.current);
1978
+ }
1979
+ if (touchEndHandlerRef.current) {
1980
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1981
+ touchEndHandlerRef.current = null;
1982
+ }
1983
+ };
1984
+ }, []);
1875
1985
  React__namespace.default.useEffect(()=>{
1876
1986
  if (!modal) {
1877
1987
  // Need to do this manually unfortunately
1878
- window.requestAnimationFrame(()=>{
1988
+ if (nonModalPointerEventsRafRef.current !== null) {
1989
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1990
+ }
1991
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1879
1992
  document.body.style.pointerEvents = 'auto';
1993
+ nonModalPointerEventsRafRef.current = null;
1880
1994
  });
1995
+ return ()=>{
1996
+ if (nonModalPointerEventsRafRef.current !== null) {
1997
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1998
+ nonModalPointerEventsRafRef.current = null;
1999
+ }
2000
+ };
1881
2001
  }
1882
2002
  }, [
1883
2003
  modal
@@ -1961,15 +2081,25 @@ const Content = /*#__PURE__*/ React__namespace.default.forwardRef(function({ onP
1961
2081
  const pointerStartRef = React__namespace.default.useRef(null);
1962
2082
  const lastKnownPointerEventRef = React__namespace.default.useRef(null);
1963
2083
  const wasBeyondThePointRef = React__namespace.default.useRef(false);
2084
+ const delayedSnapPointsRafRef = React__namespace.default.useRef(null);
1964
2085
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1965
2086
  useScaleBackground();
1966
2087
  React__namespace.default.useEffect(()=>{
1967
2088
  if (hasSnapPoints) {
1968
- window.requestAnimationFrame(()=>{
2089
+ delayedSnapPointsRafRef.current = window.requestAnimationFrame(()=>{
1969
2090
  setDelayedSnapPoints(true);
2091
+ delayedSnapPointsRafRef.current = null;
1970
2092
  });
1971
2093
  }
1972
- }, []);
2094
+ return ()=>{
2095
+ if (delayedSnapPointsRafRef.current !== null) {
2096
+ window.cancelAnimationFrame(delayedSnapPointsRafRef.current);
2097
+ delayedSnapPointsRafRef.current = null;
2098
+ }
2099
+ };
2100
+ }, [
2101
+ hasSnapPoints
2102
+ ]);
1973
2103
  function handleOnPointerUp(event) {
1974
2104
  pointerStartRef.current = null;
1975
2105
  wasBeyondThePointRef.current = false;
@@ -2070,6 +2200,7 @@ const DOUBLE_TAP_TIMEOUT = 120;
2070
2200
  const Handle = /*#__PURE__*/ React__namespace.default.forwardRef(function({ preventCycle = false, children, ...rest }, ref) {
2071
2201
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag } = useDrawerContext();
2072
2202
  const closeTimeoutIdRef = React__namespace.default.useRef(null);
2203
+ const cycleTimeoutIdRef = React__namespace.default.useRef(null);
2073
2204
  const shouldCancelInteractionRef = React__namespace.default.useRef(false);
2074
2205
  function handleStartCycle() {
2075
2206
  // Stop if this is the second click of a double click
@@ -2077,7 +2208,7 @@ const Handle = /*#__PURE__*/ React__namespace.default.forwardRef(function({ prev
2077
2208
  handleCancelInteraction();
2078
2209
  return;
2079
2210
  }
2080
- window.setTimeout(()=>{
2211
+ cycleTimeoutIdRef.current = window.setTimeout(()=>{
2081
2212
  handleCycleSnapPoints();
2082
2213
  }, DOUBLE_TAP_TIMEOUT);
2083
2214
  }
@@ -2110,9 +2241,19 @@ const Handle = /*#__PURE__*/ React__namespace.default.forwardRef(function({ prev
2110
2241
  function handleCancelInteraction() {
2111
2242
  if (closeTimeoutIdRef.current) {
2112
2243
  window.clearTimeout(closeTimeoutIdRef.current);
2244
+ closeTimeoutIdRef.current = null;
2245
+ }
2246
+ if (cycleTimeoutIdRef.current) {
2247
+ window.clearTimeout(cycleTimeoutIdRef.current);
2248
+ cycleTimeoutIdRef.current = null;
2113
2249
  }
2114
2250
  shouldCancelInteractionRef.current = false;
2115
2251
  }
2252
+ React__namespace.default.useEffect(()=>{
2253
+ return ()=>{
2254
+ handleCancelInteraction();
2255
+ };
2256
+ }, []);
2116
2257
  return /*#__PURE__*/ React__namespace.default.createElement("div", {
2117
2258
  onClick: handleStartCycle,
2118
2259
  onPointerCancel: handleCancelInteraction,