@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
 
15
15
  const DEFAULT_OPTIONS = {
16
16
  direction: 'bottom',
@@ -342,30 +342,38 @@ const nonTextInputTypes = new Set([
342
342
  'submit',
343
343
  'reset'
344
344
  ]);
345
- // The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position
346
- let preventScrollCount = 0;
347
- let restore;
345
+ const activePreventScrollLocks = new Set();
346
+ let activePreventScrollRestore = null;
347
+ function acquirePreventScrollLock(lockId) {
348
+ activePreventScrollLocks.add(lockId);
349
+ if (activePreventScrollLocks.size === 1 && isIOS()) {
350
+ activePreventScrollRestore = preventScrollMobileSafari();
351
+ }
352
+ }
353
+ function releasePreventScrollLock(lockId) {
354
+ activePreventScrollLocks.delete(lockId);
355
+ if (activePreventScrollLocks.size === 0) {
356
+ activePreventScrollRestore == null ? void 0 : activePreventScrollRestore();
357
+ activePreventScrollRestore = null;
358
+ }
359
+ }
348
360
  /**
349
361
  * Prevents scrolling on the document body on mount, and
350
362
  * restores it on unmount. Also ensures that content does not
351
363
  * shift due to the scrollbars disappearing.
352
364
  */ function usePreventScroll(options = {}) {
353
365
  let { isDisabled } = options;
366
+ const lockIdRef = useRef();
367
+ if (!lockIdRef.current) {
368
+ lockIdRef.current = Symbol('drawer-prevent-scroll-lock');
369
+ }
354
370
  useIsomorphicLayoutEffect(()=>{
355
371
  if (isDisabled) {
356
372
  return;
357
373
  }
358
- preventScrollCount++;
359
- if (preventScrollCount === 1) {
360
- if (isIOS()) {
361
- restore = preventScrollMobileSafari();
362
- }
363
- }
374
+ acquirePreventScrollLock(lockIdRef.current);
364
375
  return ()=>{
365
- preventScrollCount--;
366
- if (preventScrollCount === 0) {
367
- restore == null ? void 0 : restore();
368
- }
376
+ releasePreventScrollLock(lockIdRef.current);
369
377
  };
370
378
  }, [
371
379
  isDisabled
@@ -1006,12 +1014,23 @@ function useScaleBackground() {
1006
1014
  const { direction, isOpen, shouldScaleBackground, setBackgroundColorOnScale, noBodyStyles } = useDrawerContext();
1007
1015
  const timeoutIdRef = React__default.useRef(null);
1008
1016
  const initialBackgroundColor = useMemo(()=>document.body.style.backgroundColor, []);
1017
+ React__default.useEffect(()=>{
1018
+ return ()=>{
1019
+ if (timeoutIdRef.current !== null) {
1020
+ window.clearTimeout(timeoutIdRef.current);
1021
+ timeoutIdRef.current = null;
1022
+ }
1023
+ };
1024
+ }, []);
1009
1025
  function getScale() {
1010
1026
  return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
1011
1027
  }
1012
1028
  React__default.useEffect(()=>{
1013
1029
  if (isOpen && shouldScaleBackground) {
1014
- if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current);
1030
+ if (timeoutIdRef.current !== null) {
1031
+ clearTimeout(timeoutIdRef.current);
1032
+ timeoutIdRef.current = null;
1033
+ }
1015
1034
  const wrapper = document.querySelector('[data-drawer-wrapper]');
1016
1035
  if (!wrapper) return;
1017
1036
  chain$1(setBackgroundColorOnScale && !noBodyStyles ? assignStyle(document.body, {
@@ -1039,6 +1058,7 @@ function useScaleBackground() {
1039
1058
  } else {
1040
1059
  document.body.style.removeProperty('background');
1041
1060
  }
1061
+ timeoutIdRef.current = null;
1042
1062
  }, TRANSITIONS.DURATION * 1000);
1043
1063
  };
1044
1064
  }
@@ -1050,6 +1070,14 @@ function useScaleBackground() {
1050
1070
  }
1051
1071
 
1052
1072
  let previousBodyPosition = null;
1073
+ const activeBodyPositionLocks = new Set();
1074
+ let bodyPositionTimeoutId = null;
1075
+ function clearBodyPositionTimeout() {
1076
+ if (bodyPositionTimeoutId !== null) {
1077
+ window.clearTimeout(bodyPositionTimeoutId);
1078
+ bodyPositionTimeoutId = null;
1079
+ }
1080
+ }
1053
1081
  /**
1054
1082
  * This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
1055
1083
  * 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.
@@ -1057,9 +1085,18 @@ let previousBodyPosition = null;
1057
1085
  */ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
1058
1086
  const [activeUrl, setActiveUrl] = React__default.useState(()=>typeof window !== 'undefined' ? window.location.href : '');
1059
1087
  const scrollPos = React__default.useRef(0);
1088
+ const lockIdRef = React__default.useRef();
1089
+ if (!lockIdRef.current) {
1090
+ lockIdRef.current = Symbol('drawer-body-position-lock');
1091
+ }
1060
1092
  const setPositionFixed = React__default.useCallback(()=>{
1061
1093
  // All browsers on iOS will return true here.
1062
1094
  if (!isSafari()) return;
1095
+ const lockId = lockIdRef.current;
1096
+ if (activeBodyPositionLocks.has(lockId)) {
1097
+ return;
1098
+ }
1099
+ activeBodyPositionLocks.add(lockId);
1063
1100
  // If previousBodyPosition is already set, don't set it again.
1064
1101
  if (previousBodyPosition === null && isOpen && !noBodyStyles) {
1065
1102
  previousBodyPosition = {
@@ -1078,7 +1115,8 @@ let previousBodyPosition = null;
1078
1115
  right: '0px',
1079
1116
  height: 'auto'
1080
1117
  });
1081
- window.setTimeout(()=>window.requestAnimationFrame(()=>{
1118
+ clearBodyPositionTimeout();
1119
+ bodyPositionTimeoutId = window.setTimeout(()=>window.requestAnimationFrame(()=>{
1082
1120
  // Attempt to check if the bottom bar appeared due to the position change
1083
1121
  const bottomBarHeight = innerHeight - window.innerHeight;
1084
1122
  if (bottomBarHeight && scrollPos.current >= innerHeight) {
@@ -1088,12 +1126,19 @@ let previousBodyPosition = null;
1088
1126
  }), 300);
1089
1127
  }
1090
1128
  }, [
1091
- isOpen
1129
+ isOpen,
1130
+ noBodyStyles
1092
1131
  ]);
1093
1132
  const restorePositionSetting = React__default.useCallback(()=>{
1094
1133
  // All browsers on iOS will return true here.
1095
1134
  if (!isSafari()) return;
1135
+ const lockId = lockIdRef.current;
1136
+ activeBodyPositionLocks.delete(lockId);
1137
+ if (activeBodyPositionLocks.size > 0) {
1138
+ return;
1139
+ }
1096
1140
  if (previousBodyPosition !== null && !noBodyStyles) {
1141
+ clearBodyPositionTimeout();
1097
1142
  // Convert the position from "px" to Int
1098
1143
  const y = -parseInt(document.body.style.top, 10);
1099
1144
  const x = -parseInt(document.body.style.left, 10);
@@ -1109,7 +1154,9 @@ let previousBodyPosition = null;
1109
1154
  previousBodyPosition = null;
1110
1155
  }
1111
1156
  }, [
1112
- activeUrl
1157
+ activeUrl,
1158
+ noBodyStyles,
1159
+ setActiveUrl
1113
1160
  ]);
1114
1161
  React__default.useEffect(()=>{
1115
1162
  function onScroll() {
@@ -1405,6 +1452,12 @@ function getDragPermission({ targetTagName, hasNoDragAttribute, direction, timeS
1405
1452
 
1406
1453
  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 }) {
1407
1454
  var _drawerRef_current, _drawerRef_current1;
1455
+ const animationEndTimeoutRef = React__default.useRef(null);
1456
+ const nonModalPointerEventsRafRef = React__default.useRef(null);
1457
+ const shouldAnimateRafRef = React__default.useRef(null);
1458
+ const snapPointsResetTimeoutRef = React__default.useRef(null);
1459
+ const justReleasedTimeoutRef = React__default.useRef(null);
1460
+ const touchEndHandlerRef = React__default.useRef(null);
1408
1461
  const [isOpen = false, setIsOpen] = useControllableState({
1409
1462
  defaultProp: defaultOpen,
1410
1463
  prop: openProp,
@@ -1413,13 +1466,20 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1413
1466
  if (!o && !nested) {
1414
1467
  restorePositionSetting();
1415
1468
  }
1416
- setTimeout(()=>{
1469
+ if (animationEndTimeoutRef.current !== null) {
1470
+ window.clearTimeout(animationEndTimeoutRef.current);
1471
+ }
1472
+ animationEndTimeoutRef.current = window.setTimeout(()=>{
1417
1473
  onAnimationEnd == null ? void 0 : onAnimationEnd(o);
1418
1474
  }, TRANSITIONS.DURATION * 1000);
1419
1475
  if (o && !modal) {
1420
1476
  if (typeof window !== 'undefined') {
1421
- window.requestAnimationFrame(()=>{
1477
+ if (nonModalPointerEventsRafRef.current !== null) {
1478
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1479
+ }
1480
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1422
1481
  document.body.style.pointerEvents = 'auto';
1482
+ nonModalPointerEventsRafRef.current = null;
1423
1483
  });
1424
1484
  }
1425
1485
  }
@@ -1528,7 +1588,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1528
1588
  dragStartTime.current = new Date();
1529
1589
  // iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
1530
1590
  if (isIOS()) {
1531
- window.addEventListener('touchend', ()=>isAllowedToDrag.current = false, {
1591
+ if (touchEndHandlerRef.current) {
1592
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1593
+ }
1594
+ const handleTouchEnd = ()=>{
1595
+ isAllowedToDrag.current = false;
1596
+ touchEndHandlerRef.current = null;
1597
+ };
1598
+ touchEndHandlerRef.current = handleTouchEnd;
1599
+ window.addEventListener('touchend', handleTouchEnd, {
1532
1600
  once: true
1533
1601
  });
1534
1602
  }
@@ -1645,9 +1713,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1645
1713
  }
1646
1714
  }
1647
1715
  React__default.useEffect(()=>{
1648
- window.requestAnimationFrame(()=>{
1716
+ shouldAnimateRafRef.current = window.requestAnimationFrame(()=>{
1649
1717
  shouldAnimate.current = true;
1650
1718
  });
1719
+ return ()=>{
1720
+ if (shouldAnimateRafRef.current !== null) {
1721
+ window.cancelAnimationFrame(shouldAnimateRafRef.current);
1722
+ shouldAnimateRafRef.current = null;
1723
+ }
1724
+ };
1651
1725
  }, []);
1652
1726
  React__default.useEffect(()=>{
1653
1727
  var _window_visualViewport;
@@ -1697,10 +1771,14 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1697
1771
  if (!fromWithin) {
1698
1772
  setIsOpen(false);
1699
1773
  }
1700
- setTimeout(()=>{
1774
+ if (snapPointsResetTimeoutRef.current !== null) {
1775
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1776
+ }
1777
+ snapPointsResetTimeoutRef.current = window.setTimeout(()=>{
1701
1778
  if (snapPoints) {
1702
1779
  setActiveSnapPoint(snapPoints[0]);
1703
1780
  }
1781
+ snapPointsResetTimeoutRef.current = null;
1704
1782
  }, TRANSITIONS.DURATION * 1000); // seconds to ms
1705
1783
  }
1706
1784
  function resetDrawer() {
@@ -1774,8 +1852,12 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1774
1852
  if (releaseResult.shouldPreventFocus) {
1775
1853
  // `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.
1776
1854
  setJustReleased(true);
1777
- setTimeout(()=>{
1855
+ if (justReleasedTimeoutRef.current !== null) {
1856
+ window.clearTimeout(justReleasedTimeoutRef.current);
1857
+ }
1858
+ justReleasedTimeoutRef.current = window.setTimeout(()=>{
1778
1859
  setJustReleased(false);
1860
+ justReleasedTimeoutRef.current = null;
1779
1861
  }, 200);
1780
1862
  }
1781
1863
  if (releaseResult.action === 'close') {
@@ -1852,12 +1934,45 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1852
1934
  });
1853
1935
  }
1854
1936
  }
1937
+ React__default.useEffect(()=>{
1938
+ return ()=>{
1939
+ if (animationEndTimeoutRef.current !== null) {
1940
+ window.clearTimeout(animationEndTimeoutRef.current);
1941
+ }
1942
+ if (nonModalPointerEventsRafRef.current !== null) {
1943
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1944
+ }
1945
+ if (snapPointsResetTimeoutRef.current !== null) {
1946
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1947
+ }
1948
+ if (justReleasedTimeoutRef.current !== null) {
1949
+ window.clearTimeout(justReleasedTimeoutRef.current);
1950
+ }
1951
+ if (nestedOpenChangeTimer.current) {
1952
+ window.clearTimeout(nestedOpenChangeTimer.current);
1953
+ }
1954
+ if (touchEndHandlerRef.current) {
1955
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1956
+ touchEndHandlerRef.current = null;
1957
+ }
1958
+ };
1959
+ }, []);
1855
1960
  React__default.useEffect(()=>{
1856
1961
  if (!modal) {
1857
1962
  // Need to do this manually unfortunately
1858
- window.requestAnimationFrame(()=>{
1963
+ if (nonModalPointerEventsRafRef.current !== null) {
1964
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1965
+ }
1966
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1859
1967
  document.body.style.pointerEvents = 'auto';
1968
+ nonModalPointerEventsRafRef.current = null;
1860
1969
  });
1970
+ return ()=>{
1971
+ if (nonModalPointerEventsRafRef.current !== null) {
1972
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1973
+ nonModalPointerEventsRafRef.current = null;
1974
+ }
1975
+ };
1861
1976
  }
1862
1977
  }, [
1863
1978
  modal
@@ -1941,15 +2056,25 @@ const Content = /*#__PURE__*/ React__default.forwardRef(function({ onPointerDown
1941
2056
  const pointerStartRef = React__default.useRef(null);
1942
2057
  const lastKnownPointerEventRef = React__default.useRef(null);
1943
2058
  const wasBeyondThePointRef = React__default.useRef(false);
2059
+ const delayedSnapPointsRafRef = React__default.useRef(null);
1944
2060
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1945
2061
  useScaleBackground();
1946
2062
  React__default.useEffect(()=>{
1947
2063
  if (hasSnapPoints) {
1948
- window.requestAnimationFrame(()=>{
2064
+ delayedSnapPointsRafRef.current = window.requestAnimationFrame(()=>{
1949
2065
  setDelayedSnapPoints(true);
2066
+ delayedSnapPointsRafRef.current = null;
1950
2067
  });
1951
2068
  }
1952
- }, []);
2069
+ return ()=>{
2070
+ if (delayedSnapPointsRafRef.current !== null) {
2071
+ window.cancelAnimationFrame(delayedSnapPointsRafRef.current);
2072
+ delayedSnapPointsRafRef.current = null;
2073
+ }
2074
+ };
2075
+ }, [
2076
+ hasSnapPoints
2077
+ ]);
1953
2078
  function handleOnPointerUp(event) {
1954
2079
  pointerStartRef.current = null;
1955
2080
  wasBeyondThePointRef.current = false;
@@ -2050,6 +2175,7 @@ const DOUBLE_TAP_TIMEOUT = 120;
2050
2175
  const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle = false, children, ...rest }, ref) {
2051
2176
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag } = useDrawerContext();
2052
2177
  const closeTimeoutIdRef = React__default.useRef(null);
2178
+ const cycleTimeoutIdRef = React__default.useRef(null);
2053
2179
  const shouldCancelInteractionRef = React__default.useRef(false);
2054
2180
  function handleStartCycle() {
2055
2181
  // Stop if this is the second click of a double click
@@ -2057,7 +2183,7 @@ const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle =
2057
2183
  handleCancelInteraction();
2058
2184
  return;
2059
2185
  }
2060
- window.setTimeout(()=>{
2186
+ cycleTimeoutIdRef.current = window.setTimeout(()=>{
2061
2187
  handleCycleSnapPoints();
2062
2188
  }, DOUBLE_TAP_TIMEOUT);
2063
2189
  }
@@ -2090,9 +2216,19 @@ const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle =
2090
2216
  function handleCancelInteraction() {
2091
2217
  if (closeTimeoutIdRef.current) {
2092
2218
  window.clearTimeout(closeTimeoutIdRef.current);
2219
+ closeTimeoutIdRef.current = null;
2220
+ }
2221
+ if (cycleTimeoutIdRef.current) {
2222
+ window.clearTimeout(cycleTimeoutIdRef.current);
2223
+ cycleTimeoutIdRef.current = null;
2093
2224
  }
2094
2225
  shouldCancelInteractionRef.current = false;
2095
2226
  }
2227
+ React__default.useEffect(()=>{
2228
+ return ()=>{
2229
+ handleCancelInteraction();
2230
+ };
2231
+ }, []);
2096
2232
  return /*#__PURE__*/ React__default.createElement("div", {
2097
2233
  onClick: handleStartCycle,
2098
2234
  onPointerCancel: handleCancelInteraction,