@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.
@@ -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() {
@@ -1403,8 +1450,15 @@ function getDragPermission({ targetTagName, hasNoDragAttribute, direction, timeS
1403
1450
  };
1404
1451
  }
1405
1452
 
1453
+ const useSafeLayoutEffect = typeof window === 'undefined' ? React__default.useEffect : React__default.useLayoutEffect;
1406
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 }) {
1407
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);
1408
1462
  const [isOpen = false, setIsOpen] = useControllableState({
1409
1463
  defaultProp: defaultOpen,
1410
1464
  prop: openProp,
@@ -1413,13 +1467,20 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1413
1467
  if (!o && !nested) {
1414
1468
  restorePositionSetting();
1415
1469
  }
1416
- setTimeout(()=>{
1470
+ if (animationEndTimeoutRef.current !== null) {
1471
+ window.clearTimeout(animationEndTimeoutRef.current);
1472
+ }
1473
+ animationEndTimeoutRef.current = window.setTimeout(()=>{
1417
1474
  onAnimationEnd == null ? void 0 : onAnimationEnd(o);
1418
1475
  }, TRANSITIONS.DURATION * 1000);
1419
1476
  if (o && !modal) {
1420
1477
  if (typeof window !== 'undefined') {
1421
- window.requestAnimationFrame(()=>{
1478
+ if (nonModalPointerEventsRafRef.current !== null) {
1479
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1480
+ }
1481
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1422
1482
  document.body.style.pointerEvents = 'auto';
1483
+ nonModalPointerEventsRafRef.current = null;
1423
1484
  });
1424
1485
  }
1425
1486
  }
@@ -1497,19 +1558,23 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1497
1558
  noBodyStyles,
1498
1559
  autoFocus
1499
1560
  });
1500
- React__default.useEffect(()=>{
1561
+ useSafeLayoutEffect(()=>{
1501
1562
  var _drawerRef_current;
1502
1563
  if (!isOpen || !modal || autoFocus || typeof document === 'undefined') {
1503
1564
  return;
1504
1565
  }
1505
1566
  const activeElement = document.activeElement;
1506
- if (!(activeElement instanceof HTMLElement)) {
1567
+ if (!activeElement || activeElement === document.body) {
1507
1568
  return;
1508
1569
  }
1509
- if (((_drawerRef_current = drawerRef.current) == null ? void 0 : _drawerRef_current.contains(activeElement)) || activeElement.closest('[data-drawer]')) {
1570
+ const activeElementNode = activeElement;
1571
+ if (((_drawerRef_current = drawerRef.current) == null ? void 0 : _drawerRef_current.contains(activeElementNode)) || (activeElementNode.closest == null ? void 0 : activeElementNode.closest.call(activeElementNode, '[data-drawer]'))) {
1510
1572
  return;
1511
1573
  }
1512
- activeElement.blur();
1574
+ if (typeof activeElementNode.blur !== 'function') {
1575
+ return;
1576
+ }
1577
+ activeElementNode.blur();
1513
1578
  }, [
1514
1579
  autoFocus,
1515
1580
  isOpen,
@@ -1528,7 +1593,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1528
1593
  dragStartTime.current = new Date();
1529
1594
  // iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
1530
1595
  if (isIOS()) {
1531
- window.addEventListener('touchend', ()=>isAllowedToDrag.current = false, {
1596
+ if (touchEndHandlerRef.current) {
1597
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1598
+ }
1599
+ const handleTouchEnd = ()=>{
1600
+ isAllowedToDrag.current = false;
1601
+ touchEndHandlerRef.current = null;
1602
+ };
1603
+ touchEndHandlerRef.current = handleTouchEnd;
1604
+ window.addEventListener('touchend', handleTouchEnd, {
1532
1605
  once: true
1533
1606
  });
1534
1607
  }
@@ -1645,9 +1718,15 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1645
1718
  }
1646
1719
  }
1647
1720
  React__default.useEffect(()=>{
1648
- window.requestAnimationFrame(()=>{
1721
+ shouldAnimateRafRef.current = window.requestAnimationFrame(()=>{
1649
1722
  shouldAnimate.current = true;
1650
1723
  });
1724
+ return ()=>{
1725
+ if (shouldAnimateRafRef.current !== null) {
1726
+ window.cancelAnimationFrame(shouldAnimateRafRef.current);
1727
+ shouldAnimateRafRef.current = null;
1728
+ }
1729
+ };
1651
1730
  }, []);
1652
1731
  React__default.useEffect(()=>{
1653
1732
  var _window_visualViewport;
@@ -1697,10 +1776,14 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1697
1776
  if (!fromWithin) {
1698
1777
  setIsOpen(false);
1699
1778
  }
1700
- setTimeout(()=>{
1779
+ if (snapPointsResetTimeoutRef.current !== null) {
1780
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1781
+ }
1782
+ snapPointsResetTimeoutRef.current = window.setTimeout(()=>{
1701
1783
  if (snapPoints) {
1702
1784
  setActiveSnapPoint(snapPoints[0]);
1703
1785
  }
1786
+ snapPointsResetTimeoutRef.current = null;
1704
1787
  }, TRANSITIONS.DURATION * 1000); // seconds to ms
1705
1788
  }
1706
1789
  function resetDrawer() {
@@ -1774,8 +1857,12 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1774
1857
  if (releaseResult.shouldPreventFocus) {
1775
1858
  // `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
1859
  setJustReleased(true);
1777
- setTimeout(()=>{
1860
+ if (justReleasedTimeoutRef.current !== null) {
1861
+ window.clearTimeout(justReleasedTimeoutRef.current);
1862
+ }
1863
+ justReleasedTimeoutRef.current = window.setTimeout(()=>{
1778
1864
  setJustReleased(false);
1865
+ justReleasedTimeoutRef.current = null;
1779
1866
  }, 200);
1780
1867
  }
1781
1868
  if (releaseResult.action === 'close') {
@@ -1852,12 +1939,45 @@ function Root({ open: openProp, onOpenChange, children, onDrag: onDragProp, onRe
1852
1939
  });
1853
1940
  }
1854
1941
  }
1942
+ React__default.useEffect(()=>{
1943
+ return ()=>{
1944
+ if (animationEndTimeoutRef.current !== null) {
1945
+ window.clearTimeout(animationEndTimeoutRef.current);
1946
+ }
1947
+ if (nonModalPointerEventsRafRef.current !== null) {
1948
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1949
+ }
1950
+ if (snapPointsResetTimeoutRef.current !== null) {
1951
+ window.clearTimeout(snapPointsResetTimeoutRef.current);
1952
+ }
1953
+ if (justReleasedTimeoutRef.current !== null) {
1954
+ window.clearTimeout(justReleasedTimeoutRef.current);
1955
+ }
1956
+ if (nestedOpenChangeTimer.current) {
1957
+ window.clearTimeout(nestedOpenChangeTimer.current);
1958
+ }
1959
+ if (touchEndHandlerRef.current) {
1960
+ window.removeEventListener('touchend', touchEndHandlerRef.current);
1961
+ touchEndHandlerRef.current = null;
1962
+ }
1963
+ };
1964
+ }, []);
1855
1965
  React__default.useEffect(()=>{
1856
1966
  if (!modal) {
1857
1967
  // Need to do this manually unfortunately
1858
- window.requestAnimationFrame(()=>{
1968
+ if (nonModalPointerEventsRafRef.current !== null) {
1969
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1970
+ }
1971
+ nonModalPointerEventsRafRef.current = window.requestAnimationFrame(()=>{
1859
1972
  document.body.style.pointerEvents = 'auto';
1973
+ nonModalPointerEventsRafRef.current = null;
1860
1974
  });
1975
+ return ()=>{
1976
+ if (nonModalPointerEventsRafRef.current !== null) {
1977
+ window.cancelAnimationFrame(nonModalPointerEventsRafRef.current);
1978
+ nonModalPointerEventsRafRef.current = null;
1979
+ }
1980
+ };
1861
1981
  }
1862
1982
  }, [
1863
1983
  modal
@@ -1941,15 +2061,25 @@ const Content = /*#__PURE__*/ React__default.forwardRef(function({ onPointerDown
1941
2061
  const pointerStartRef = React__default.useRef(null);
1942
2062
  const lastKnownPointerEventRef = React__default.useRef(null);
1943
2063
  const wasBeyondThePointRef = React__default.useRef(false);
2064
+ const delayedSnapPointsRafRef = React__default.useRef(null);
1944
2065
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1945
2066
  useScaleBackground();
1946
2067
  React__default.useEffect(()=>{
1947
2068
  if (hasSnapPoints) {
1948
- window.requestAnimationFrame(()=>{
2069
+ delayedSnapPointsRafRef.current = window.requestAnimationFrame(()=>{
1949
2070
  setDelayedSnapPoints(true);
2071
+ delayedSnapPointsRafRef.current = null;
1950
2072
  });
1951
2073
  }
1952
- }, []);
2074
+ return ()=>{
2075
+ if (delayedSnapPointsRafRef.current !== null) {
2076
+ window.cancelAnimationFrame(delayedSnapPointsRafRef.current);
2077
+ delayedSnapPointsRafRef.current = null;
2078
+ }
2079
+ };
2080
+ }, [
2081
+ hasSnapPoints
2082
+ ]);
1953
2083
  function handleOnPointerUp(event) {
1954
2084
  pointerStartRef.current = null;
1955
2085
  wasBeyondThePointRef.current = false;
@@ -2050,6 +2180,7 @@ const DOUBLE_TAP_TIMEOUT = 120;
2050
2180
  const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle = false, children, ...rest }, ref) {
2051
2181
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag } = useDrawerContext();
2052
2182
  const closeTimeoutIdRef = React__default.useRef(null);
2183
+ const cycleTimeoutIdRef = React__default.useRef(null);
2053
2184
  const shouldCancelInteractionRef = React__default.useRef(false);
2054
2185
  function handleStartCycle() {
2055
2186
  // Stop if this is the second click of a double click
@@ -2057,7 +2188,7 @@ const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle =
2057
2188
  handleCancelInteraction();
2058
2189
  return;
2059
2190
  }
2060
- window.setTimeout(()=>{
2191
+ cycleTimeoutIdRef.current = window.setTimeout(()=>{
2061
2192
  handleCycleSnapPoints();
2062
2193
  }, DOUBLE_TAP_TIMEOUT);
2063
2194
  }
@@ -2090,9 +2221,19 @@ const Handle = /*#__PURE__*/ React__default.forwardRef(function({ preventCycle =
2090
2221
  function handleCancelInteraction() {
2091
2222
  if (closeTimeoutIdRef.current) {
2092
2223
  window.clearTimeout(closeTimeoutIdRef.current);
2224
+ closeTimeoutIdRef.current = null;
2225
+ }
2226
+ if (cycleTimeoutIdRef.current) {
2227
+ window.clearTimeout(cycleTimeoutIdRef.current);
2228
+ cycleTimeoutIdRef.current = null;
2093
2229
  }
2094
2230
  shouldCancelInteractionRef.current = false;
2095
2231
  }
2232
+ React__default.useEffect(()=>{
2233
+ return ()=>{
2234
+ handleCancelInteraction();
2235
+ };
2236
+ }, []);
2096
2237
  return /*#__PURE__*/ React__default.createElement("div", {
2097
2238
  onClick: handleStartCycle,
2098
2239
  onPointerCancel: handleCancelInteraction,