react-panel-layout 0.6.0 → 0.7.0

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.
Files changed (258) hide show
  1. package/dist/{FloatingPanelFrame-SgYLc6Ud.js → FloatingPanelFrame-3eU9AwPo.js} +2 -2
  2. package/dist/{FloatingPanelFrame-SgYLc6Ud.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
  3. package/dist/FloatingWindow-Bw2djgpz.js +1542 -0
  4. package/dist/FloatingWindow-Bw2djgpz.js.map +1 -0
  5. package/dist/FloatingWindow-Cvyokf0m.cjs +2 -0
  6. package/dist/FloatingWindow-Cvyokf0m.cjs.map +1 -0
  7. package/dist/GridLayout-B4aCqSyd.js +947 -0
  8. package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-B4aCqSyd.js.map} +1 -1
  9. package/dist/GridLayout-DNOClFzz.cjs +2 -0
  10. package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DNOClFzz.cjs.map} +1 -1
  11. package/dist/{HorizontalDivider-WF1k_qND.js → HorizontalDivider-DdxzfV0l.js} +3 -3
  12. package/dist/{HorizontalDivider-WF1k_qND.js.map → HorizontalDivider-DdxzfV0l.js.map} +1 -1
  13. package/dist/{HorizontalDivider-B5Z-KZLk.cjs → HorizontalDivider-_pgV4Mcv.cjs} +2 -2
  14. package/dist/{HorizontalDivider-B5Z-KZLk.cjs.map → HorizontalDivider-_pgV4Mcv.cjs.map} +1 -1
  15. package/dist/PanelSystem-B8Igvnb2.cjs +3 -0
  16. package/dist/PanelSystem-B8Igvnb2.cjs.map +1 -0
  17. package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-DDUSFjXD.js} +209 -248
  18. package/dist/PanelSystem-DDUSFjXD.js.map +1 -0
  19. package/dist/ResizeHandle-CBcAS918.cjs +2 -0
  20. package/dist/{ResizeHandle-CScipO5l.cjs.map → ResizeHandle-CBcAS918.cjs.map} +1 -1
  21. package/dist/{ResizeHandle-CdA_JYfN.js → ResizeHandle-CXjc1meV.js} +28 -29
  22. package/dist/{ResizeHandle-CdA_JYfN.js.map → ResizeHandle-CXjc1meV.js.map} +1 -1
  23. package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
  24. package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
  25. package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
  26. package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
  27. package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
  28. package/dist/components/window/Drawer.d.ts +4 -1
  29. package/dist/components/window/DrawerLayers.d.ts +1 -1
  30. package/dist/components/window/DrawerRevealContext.d.ts +61 -0
  31. package/dist/components/window/drawerRevealAnimationUtils.d.ts +212 -0
  32. package/dist/components/window/drawerStyles.d.ts +74 -0
  33. package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
  34. package/dist/components/window/useDrawerSwipeTransform.d.ts +29 -0
  35. package/dist/components/window/useDrawerTransform.d.ts +68 -0
  36. package/dist/components/window/useRevealDrawerTransform.d.ts +56 -0
  37. package/dist/config.cjs +1 -1
  38. package/dist/config.cjs.map +1 -1
  39. package/dist/config.js +9 -8
  40. package/dist/config.js.map +1 -1
  41. package/dist/constants/styles.d.ts +17 -0
  42. package/dist/dialog/index.d.ts +69 -0
  43. package/dist/floating.js +1 -1
  44. package/dist/grid.cjs +1 -1
  45. package/dist/grid.js +2 -2
  46. package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
  47. package/dist/hooks/gesture/types.d.ts +48 -5
  48. package/dist/hooks/gesture/utils.d.ts +19 -0
  49. package/dist/hooks/useAnimationFrame.d.ts +2 -0
  50. package/dist/hooks/useOperationContinuity.d.ts +64 -0
  51. package/dist/hooks/useResizeObserver.d.ts +33 -1
  52. package/dist/hooks/useSharedElementTransition.d.ts +112 -0
  53. package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.js +7 -7
  56. package/dist/modules/dialog/AlertDialog.d.ts +9 -0
  57. package/dist/modules/dialog/DialogContainer.d.ts +37 -0
  58. package/dist/modules/dialog/Modal.d.ts +26 -0
  59. package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
  60. package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
  61. package/dist/modules/dialog/types.d.ts +183 -0
  62. package/dist/modules/dialog/useDialog.d.ts +39 -0
  63. package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
  64. package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
  65. package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
  66. package/dist/modules/drawer/drawerStateMachine.d.ts +168 -0
  67. package/dist/modules/drawer/revealDrawerConstants.d.ts +33 -0
  68. package/dist/modules/drawer/revealDrawerStateMachine.d.ts +146 -0
  69. package/dist/modules/drawer/strategies/index.d.ts +8 -0
  70. package/dist/modules/drawer/strategies/overlayStrategy.d.ts +12 -0
  71. package/dist/modules/drawer/strategies/revealStrategy.d.ts +12 -0
  72. package/dist/modules/drawer/strategies/types.d.ts +116 -0
  73. package/dist/modules/drawer/types.d.ts +74 -0
  74. package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
  75. package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
  76. package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
  77. package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
  78. package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
  79. package/dist/panels.cjs +1 -1
  80. package/dist/panels.js +1 -1
  81. package/dist/pivot.cjs +1 -1
  82. package/dist/pivot.js +1 -1
  83. package/dist/resizer.cjs +1 -1
  84. package/dist/resizer.js +2 -2
  85. package/dist/stack.cjs +1 -1
  86. package/dist/stack.cjs.map +1 -1
  87. package/dist/stack.js +480 -780
  88. package/dist/stack.js.map +1 -1
  89. package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
  90. package/dist/sticky-header.cjs +1 -1
  91. package/dist/sticky-header.cjs.map +1 -1
  92. package/dist/sticky-header.js +59 -51
  93. package/dist/sticky-header.js.map +1 -1
  94. package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
  95. package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
  96. package/dist/styles-qf6ptVLD.cjs.map +1 -1
  97. package/dist/types.d.ts +30 -0
  98. package/dist/useAnimationFrame-BZ6D2lMq.cjs +2 -0
  99. package/dist/useAnimationFrame-BZ6D2lMq.cjs.map +1 -0
  100. package/dist/useAnimationFrame-Bg4e-H8O.js +394 -0
  101. package/dist/useAnimationFrame-Bg4e-H8O.js.map +1 -0
  102. package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
  103. package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
  104. package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
  105. package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
  106. package/dist/window/index.d.ts +2 -0
  107. package/dist/window.cjs +1 -1
  108. package/dist/window.cjs.map +1 -1
  109. package/dist/window.js +114 -103
  110. package/dist/window.js.map +1 -1
  111. package/package.json +6 -1
  112. package/src/components/gesture/SwipeSafeZone.tsx +70 -0
  113. package/src/components/grid/GridLayout.tsx +110 -38
  114. package/src/components/window/Drawer.tsx +353 -162
  115. package/src/components/window/DrawerLayers.tsx +54 -11
  116. package/src/components/window/DrawerRevealContext.spec.ts +20 -0
  117. package/src/components/window/DrawerRevealContext.tsx +99 -0
  118. package/src/components/window/drawerRevealAnimationUtils.spec.ts +375 -0
  119. package/src/components/window/drawerRevealAnimationUtils.ts +415 -0
  120. package/src/components/window/drawerStyles.spec.ts +302 -0
  121. package/src/components/window/drawerStyles.ts +252 -0
  122. package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
  123. package/src/components/window/drawerSwipeConfig.ts +112 -0
  124. package/src/components/window/useDrawerSwipeTransform.ts +67 -0
  125. package/src/components/window/useDrawerTransform.ts +505 -0
  126. package/src/components/window/useRevealDrawerTransform.spec.ts +1936 -0
  127. package/src/components/window/useRevealDrawerTransform.ts +105 -0
  128. package/src/constants/styles.ts +19 -0
  129. package/src/demo/components/FullscreenDemoPage.tsx +47 -0
  130. package/src/demo/fullscreenRoutes.tsx +32 -0
  131. package/src/demo/index.tsx +5 -0
  132. package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
  133. package/src/demo/pages/Dialog/card/index.tsx +22 -0
  134. package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
  135. package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
  136. package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +219 -0
  137. package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
  138. package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
  139. package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
  140. package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
  141. package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
  142. package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
  143. package/src/demo/pages/Dialog/modal/index.tsx +17 -0
  144. package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
  145. package/src/demo/pages/Drawer/components/DrawerBasics.module.css +6 -1
  146. package/src/demo/pages/Drawer/components/DrawerBasics.tsx +14 -4
  147. package/src/demo/pages/Drawer/components/DrawerReveal.module.css +157 -0
  148. package/src/demo/pages/Drawer/components/DrawerReveal.tsx +128 -0
  149. package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
  150. package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
  151. package/src/demo/pages/Drawer/reveal/index.tsx +17 -0
  152. package/src/demo/pages/Drawer/reveal-fullscreen/index.tsx +135 -0
  153. package/src/demo/pages/Drawer/reveal-fullscreen/styles.module.css +233 -0
  154. package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
  155. package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
  156. package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
  157. package/src/demo/pages/Stack/components/StackBasics.spec.tsx +156 -0
  158. package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
  159. package/src/demo/pages/Stack/components/StackTablet.spec.tsx +110 -0
  160. package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
  161. package/src/demo/routes.tsx +24 -1
  162. package/src/dialog/index.ts +85 -0
  163. package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
  164. package/src/hooks/gesture/testing/createGestureSimulator.ts +113 -37
  165. package/src/hooks/gesture/types.ts +83 -6
  166. package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
  167. package/src/hooks/gesture/useNativeGestureGuard.spec.ts +99 -31
  168. package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
  169. package/src/hooks/gesture/utils.ts +102 -0
  170. package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
  171. package/src/hooks/useAnimatedVisibility.ts +28 -2
  172. package/src/hooks/useAnimationFrame.ts +8 -0
  173. package/src/hooks/useOperationContinuity.spec.ts +394 -0
  174. package/src/hooks/useOperationContinuity.ts +135 -0
  175. package/src/hooks/useResizeObserver.spec.tsx +277 -0
  176. package/src/hooks/useResizeObserver.tsx +108 -39
  177. package/src/hooks/useScrollContainer.ts +4 -10
  178. package/src/hooks/useSharedElementTransition.ts +354 -0
  179. package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
  180. package/src/hooks/useSwipeContentTransform.ts +166 -28
  181. package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
  182. package/src/modules/dialog/AlertDialog.tsx +221 -0
  183. package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
  184. package/src/modules/dialog/DialogContainer.tsx +188 -0
  185. package/src/modules/dialog/Modal.spec.tsx +220 -0
  186. package/src/modules/dialog/Modal.tsx +182 -0
  187. package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
  188. package/src/modules/dialog/dialogAnimationUtils.spec.ts +252 -0
  189. package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
  190. package/src/modules/dialog/types.ts +186 -0
  191. package/src/modules/dialog/useDialog.spec.tsx +447 -0
  192. package/src/modules/dialog/useDialog.ts +214 -0
  193. package/src/modules/dialog/useDialogContainer.spec.ts +339 -0
  194. package/src/modules/dialog/useDialogContainer.ts +150 -0
  195. package/src/modules/dialog/useDialogSwipeInput.spec.ts +178 -0
  196. package/src/modules/dialog/useDialogSwipeInput.ts +350 -0
  197. package/src/modules/dialog/useDialogTransform.spec.ts +403 -0
  198. package/src/modules/dialog/useDialogTransform.ts +407 -0
  199. package/src/modules/drawer/drawerStateMachine.ts +500 -0
  200. package/src/modules/drawer/revealDrawerConstants.ts +38 -0
  201. package/src/modules/drawer/revealDrawerStateMachine.spec.ts +558 -0
  202. package/src/modules/drawer/revealDrawerStateMachine.ts +197 -0
  203. package/src/modules/drawer/strategies/index.ts +9 -0
  204. package/src/modules/drawer/strategies/overlayStrategy.ts +133 -0
  205. package/src/modules/drawer/strategies/revealStrategy.ts +111 -0
  206. package/src/modules/drawer/strategies/types.ts +160 -0
  207. package/src/modules/drawer/types.ts +102 -0
  208. package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
  209. package/src/modules/drawer/useDrawerSwipeInput.ts +402 -0
  210. package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
  211. package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
  212. package/src/modules/pivot/SwipePivotContent.spec.tsx +66 -25
  213. package/src/modules/pivot/SwipePivotContent.tsx +2 -2
  214. package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
  215. package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
  216. package/src/modules/pivot/scaleInputState.spec.ts +11 -2
  217. package/src/modules/pivot/usePivot.spec.ts +17 -3
  218. package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
  219. package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
  220. package/src/modules/stack/SwipeStackContent.tsx +43 -33
  221. package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
  222. package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
  223. package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
  224. package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
  225. package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
  226. package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
  227. package/src/modules/stack/useStackAnimationState.ts +18 -13
  228. package/src/modules/stack/useStackNavigation.spec.ts +198 -3
  229. package/src/modules/stack/useStackNavigation.tsx +113 -56
  230. package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
  231. package/src/modules/stack/useStackSwipeInput.ts +1 -1
  232. package/src/sticky-header/StickyArea.tsx +29 -57
  233. package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
  234. package/src/sticky-header/calculateStickyMetrics.ts +50 -0
  235. package/src/types.ts +33 -0
  236. package/src/window/index.ts +2 -0
  237. package/dist/FloatingWindow-BpdOpg_L.js +0 -400
  238. package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
  239. package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
  240. package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
  241. package/dist/GridLayout-B4VRsC0r.cjs +0 -2
  242. package/dist/GridLayout-BltqeCPK.js +0 -927
  243. package/dist/PanelSystem-Bs8bQwQF.cjs +0 -3
  244. package/dist/PanelSystem-Bs8bQwQF.cjs.map +0 -1
  245. package/dist/PanelSystem-Dr1TBhxM.js.map +0 -1
  246. package/dist/ResizeHandle-CScipO5l.cjs +0 -2
  247. package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
  248. package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
  249. package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
  250. package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
  251. package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
  252. package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
  253. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
  254. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
  255. package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
  256. package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
  257. package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
  258. package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @file Tests for useResizeObserver hook.
3
+ *
4
+ * These tests define the contract that useResizeObserver must fulfill.
5
+ * The key requirement is that consuming components can rely on containerSize
6
+ * being available when their animation logic runs.
7
+ */
8
+ /* eslint-disable no-restricted-syntax -- Dynamic imports needed for isolated module testing */
9
+ import { render, act } from "@testing-library/react";
10
+ import * as React from "react";
11
+
12
+ // We'll import the hook after defining what it should do
13
+ // import { useResizeObserver } from "./useResizeObserver.js";
14
+
15
+ /**
16
+ * Mock ResizeObserver
17
+ */
18
+ class MockResizeObserver implements ResizeObserver {
19
+ private static instances: MockResizeObserver[] = [];
20
+ private callback: ResizeObserverCallback;
21
+ private observed = new Set<Element>();
22
+
23
+ static getInstances(): MockResizeObserver[] {
24
+ return this.instances;
25
+ }
26
+
27
+ static clearInstances(): void {
28
+ this.instances = [];
29
+ }
30
+
31
+ constructor(callback: ResizeObserverCallback) {
32
+ this.callback = callback;
33
+ MockResizeObserver.instances.push(this);
34
+ }
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Required for interface compatibility
37
+ observe(target: Element, options?: ResizeObserverOptions): void {
38
+ this.observed.add(target);
39
+ // Real ResizeObserver fires callback asynchronously after observe
40
+ // We simulate immediate callback for testing
41
+ const entry = this.createEntry(target, 400, 300);
42
+ this.callback([entry], this);
43
+ }
44
+
45
+ unobserve(target: Element): void {
46
+ this.observed.delete(target);
47
+ }
48
+
49
+ disconnect(): void {
50
+ this.observed.clear();
51
+ }
52
+
53
+ triggerResize(target: Element, width: number, height: number): void {
54
+ if (this.observed.has(target)) {
55
+ const entry = this.createEntry(target, width, height);
56
+ this.callback([entry], this);
57
+ }
58
+ }
59
+
60
+ private createEntry(target: Element, width: number, height: number): ResizeObserverEntry {
61
+ return {
62
+ target,
63
+ contentRect: new DOMRect(0, 0, width, height),
64
+ borderBoxSize: [{ inlineSize: width, blockSize: height }],
65
+ contentBoxSize: [{ inlineSize: width, blockSize: height }],
66
+ devicePixelContentBoxSize: [],
67
+ };
68
+ }
69
+ }
70
+
71
+ const originalResizeObserver = globalThis.ResizeObserver;
72
+
73
+ describe("useResizeObserver contract", () => {
74
+ beforeEach(() => {
75
+ MockResizeObserver.clearInstances();
76
+ globalThis.ResizeObserver = MockResizeObserver;
77
+
78
+ // Mock getBoundingClientRect
79
+ Element.prototype.getBoundingClientRect = function () {
80
+ return new DOMRect(0, 0, 400, 300);
81
+ };
82
+ });
83
+
84
+ afterEach(() => {
85
+ globalThis.ResizeObserver = originalResizeObserver;
86
+ });
87
+
88
+ describe("timing requirements", () => {
89
+ /**
90
+ * This is the critical test case.
91
+ *
92
+ * In the real use case (StackTablet -> SwipeStackContent):
93
+ * 1. StackTablet renders, calls useResizeObserver
94
+ * 2. StackTablet passes containerSize to SwipeStackContent
95
+ * 3. SwipeStackContent uses containerSize in its first useLayoutEffect
96
+ *
97
+ * The question: Can containerSize be > 0 when SwipeStackContent's
98
+ * useLayoutEffect runs for the first time?
99
+ *
100
+ * Answer: This depends on React's effect execution order.
101
+ * - Effects run in order of hook registration
102
+ * - Parent's effects run before children's effects? NO.
103
+ * - Actually, React runs effects bottom-up (children first, then parents)
104
+ *
105
+ * So when SwipeStackContent's useLayoutEffect runs:
106
+ * - It's the FIRST effect to run (child before parent)
107
+ * - useResizeObserver's useLayoutEffect hasn't run yet
108
+ * - Therefore containerSize is still 0
109
+ *
110
+ * This means we MUST handle containerSize=0 in SwipeStackContent.
111
+ * The question is: how to abstract this properly?
112
+ */
113
+ it("documents React effect execution order", () => {
114
+ const executionOrder: string[] = [];
115
+
116
+ const Child: React.FC<{ size: number }> = ({ size }) => {
117
+ React.useLayoutEffect(() => {
118
+ executionOrder.push(`child-layout-effect: size=${size}`);
119
+ }, []);
120
+
121
+ return <div>Child: {size}</div>;
122
+ };
123
+
124
+ const Parent: React.FC = () => {
125
+ const ref = React.useRef<HTMLDivElement>(null);
126
+ const [size, setSize] = React.useState(0);
127
+
128
+ React.useLayoutEffect(() => {
129
+ executionOrder.push("parent-layout-effect: measuring");
130
+ if (ref.current) {
131
+ setSize(ref.current.getBoundingClientRect().width);
132
+ }
133
+ }, []);
134
+
135
+ executionOrder.push(`parent-render: size=${size}`);
136
+
137
+ return (
138
+ <div ref={ref}>
139
+ <Child size={size} />
140
+ </div>
141
+ );
142
+ };
143
+
144
+ render(<Parent />);
145
+
146
+ // This documents the actual execution order
147
+ // Parent renders first with size=0
148
+ // Child renders with size=0
149
+ // Child's useLayoutEffect runs (sees size=0)
150
+ // Parent's useLayoutEffect runs (sets size=400)
151
+ // Re-render with size=400
152
+ expect(executionOrder).toContain("child-layout-effect: size=0");
153
+ });
154
+
155
+ /**
156
+ * Given the above, the proper abstraction is:
157
+ *
158
+ * useResizeObserver should provide a way for consumers to know
159
+ * when the size is "ready" (first valid measurement complete).
160
+ *
161
+ * Consumers that depend on size for animation should:
162
+ * - Not start animation until size is ready
163
+ * - OR use a hook that handles this internally
164
+ */
165
+ it("should indicate when size is ready", async () => {
166
+ // Import dynamically to test the implementation
167
+ const { useResizeObserver } = await import("./useResizeObserver.js");
168
+
169
+ const results: Array<{ width: number; isReady: boolean }> = [];
170
+
171
+ const TestComponent: React.FC = () => {
172
+ const ref = React.useRef<HTMLDivElement>(null);
173
+ const { rect } = useResizeObserver(ref, { box: "border-box" });
174
+
175
+ const width = rect?.width ?? 0;
176
+ const isReady = rect !== null;
177
+
178
+ React.useEffect(() => {
179
+ results.push({ width, isReady });
180
+ }, [width, isReady]);
181
+
182
+ return <div ref={ref} style={{ width: 400, height: 300 }} />;
183
+ };
184
+
185
+ render(<TestComponent />);
186
+
187
+ // After effects run, size should be available
188
+ await act(async () => {});
189
+
190
+ const lastResult = results[results.length - 1];
191
+ expect(lastResult.isReady).toBe(true);
192
+ expect(lastResult.width).toBe(400);
193
+ });
194
+ });
195
+
196
+ describe("memory efficiency", () => {
197
+ it("shares ResizeObserver instances for same box option", async () => {
198
+ const { useResizeObserver, clearObserverCache } = await import("./useResizeObserver.js");
199
+ clearObserverCache();
200
+ MockResizeObserver.clearInstances();
201
+
202
+ const TestComponent: React.FC = () => {
203
+ const ref1 = React.useRef<HTMLDivElement>(null);
204
+ const ref2 = React.useRef<HTMLDivElement>(null);
205
+ const ref3 = React.useRef<HTMLDivElement>(null);
206
+
207
+ useResizeObserver(ref1, { box: "border-box" });
208
+ useResizeObserver(ref2, { box: "border-box" });
209
+ useResizeObserver(ref3, { box: "border-box" });
210
+
211
+ return (
212
+ <>
213
+ <div ref={ref1} />
214
+ <div ref={ref2} />
215
+ <div ref={ref3} />
216
+ </>
217
+ );
218
+ };
219
+
220
+ render(<TestComponent />);
221
+
222
+ expect(MockResizeObserver.getInstances().length).toBe(1);
223
+ });
224
+ });
225
+
226
+ describe("resize updates", () => {
227
+ it("updates when element size changes", async () => {
228
+ const { useResizeObserver, clearObserverCache } = await import("./useResizeObserver.js");
229
+ clearObserverCache();
230
+ MockResizeObserver.clearInstances();
231
+
232
+ const widths: number[] = [];
233
+
234
+ const TestComponent: React.FC = () => {
235
+ const ref = React.useRef<HTMLDivElement>(null);
236
+ const { rect } = useResizeObserver(ref, { box: "border-box" });
237
+
238
+ React.useEffect(() => {
239
+ if (rect) {
240
+ widths.push(rect.width);
241
+ }
242
+ }, [rect]);
243
+
244
+ return <div ref={ref} data-testid="target" />;
245
+ };
246
+
247
+ const { getByTestId } = render(<TestComponent />);
248
+
249
+ await act(async () => {});
250
+
251
+ const element = getByTestId("target");
252
+ const observer = MockResizeObserver.getInstances()[0];
253
+
254
+ act(() => {
255
+ observer.triggerResize(element, 800, 600);
256
+ });
257
+
258
+ expect(widths).toContain(400); // Initial
259
+ expect(widths).toContain(800); // After resize
260
+ });
261
+ });
262
+ });
263
+
264
+ /**
265
+ * Based on the tests above, the conclusion is:
266
+ *
267
+ * 1. React's effect execution order means child effects run before parent effects
268
+ * 2. Therefore, on first render, child components will see containerSize=0
269
+ * 3. This is a fundamental React constraint, not a bug in useResizeObserver
270
+ *
271
+ * The proper abstraction for SwipeStackContent is:
272
+ * - Check if containerSize > 0 before consuming isFirstMount
273
+ * - This is NOT a workaround, it's the correct pattern for this React constraint
274
+ *
275
+ * Alternatively, create a higher-level hook like useAnimatedStack that
276
+ * encapsulates both the size observation and the "ready" state.
277
+ */
@@ -1,80 +1,149 @@
1
1
  /**
2
2
  * @file Shared useResizeObserver hook with cached observer instances.
3
+ *
4
+ * Provides element size observation with shared observers for memory efficiency.
5
+ * Size becomes available after the first useLayoutEffect cycle completes.
6
+ *
7
+ * Note: Due to React's effect execution order (children before parents),
8
+ * child components may see containerSize=0 on their first effect run.
9
+ * This is a React constraint, not a bug. Consumers should check for
10
+ * valid size before using it for calculations like animation positions.
3
11
  */
4
12
  import * as React from "react";
13
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
5
14
 
6
- type Unobserve = () => void;
7
- type Callback = (entry: ResizeObserverEntry, observer: ResizeObserver) => void;
15
+ /**
16
+ * Shared ResizeObserver that can observe multiple elements.
17
+ */
8
18
  type SharedObserver = {
9
- observe: (target: Element, callback: Callback) => Unobserve;
19
+ observe: (target: Element, callback: (entry: ResizeObserverEntry) => void) => () => void;
10
20
  };
21
+
22
+ /** Cache of shared observers per box option */
11
23
  const observerCache = new Map<string, SharedObserver>();
12
- const getSharedObserver = (options: ResizeObserverOptions) => {
13
- const { box = "content-box" } = options;
24
+
25
+ /**
26
+ * Get or create a shared ResizeObserver for the given box option.
27
+ */
28
+ const getSharedObserver = (box: ResizeObserverBoxOptions): SharedObserver => {
14
29
  const observerKey = `resize-box:${box}`;
15
30
  const cached = observerCache.get(observerKey);
16
31
  if (cached) {
17
32
  return cached;
18
33
  }
19
- const observer = new (class {
20
- #callbackMap = new Map<Element, Callback>();
21
- #resizeObserver = new ResizeObserver((entries, observer) => {
22
- entries.forEach((entry) => {
23
- const callback = this.#callbackMap.get(entry.target);
24
- if (callback) {
25
- callback(entry, observer);
26
- }
27
- });
28
- });
29
- observe(target: Element, callback: Callback) {
30
- this.#callbackMap.set(target, callback);
31
- this.#resizeObserver.observe(target, options);
34
+
35
+ const callbacks = new Map<Element, (entry: ResizeObserverEntry) => void>();
36
+
37
+ const resizeObserver = new ResizeObserver((entries) => {
38
+ for (const entry of entries) {
39
+ const callback = callbacks.get(entry.target);
40
+ if (callback) {
41
+ callback(entry);
42
+ }
43
+ }
44
+ });
45
+
46
+ const sharedObserver: SharedObserver = {
47
+ observe(target, callback) {
48
+ callbacks.set(target, callback);
49
+ resizeObserver.observe(target, { box });
50
+
32
51
  return () => {
33
- this.#callbackMap.delete(target);
34
- this.#resizeObserver.unobserve(target);
52
+ callbacks.delete(target);
53
+ resizeObserver.unobserve(target);
35
54
  };
36
- }
37
- })();
38
- observerCache.set(observerKey, observer);
55
+ },
56
+ };
57
+
58
+ observerCache.set(observerKey, sharedObserver);
59
+ return sharedObserver;
60
+ };
61
+
62
+ /**
63
+ * Create a ResizeObserverEntry from getBoundingClientRect.
64
+ */
65
+ const measureElement = (target: Element): ResizeObserverEntry => {
66
+ const rect = target.getBoundingClientRect();
67
+ return {
68
+ target,
69
+ contentRect: rect,
70
+ borderBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
71
+ contentBoxSize: [{ inlineSize: rect.width, blockSize: rect.height }],
72
+ devicePixelContentBoxSize: [],
73
+ };
74
+ };
39
75
 
40
- return observer;
76
+ /**
77
+ * Extract DOMRect from ResizeObserverEntry.
78
+ */
79
+ const entryToRect = (entry: ResizeObserverEntry): DOMRect => {
80
+ if (entry.borderBoxSize?.length > 0) {
81
+ const size = entry.borderBoxSize[0];
82
+ return new DOMRect(0, 0, size.inlineSize, size.blockSize);
83
+ }
84
+ return entry.contentRect;
41
85
  };
86
+
87
+ /**
88
+ * Clear observer cache. Exported for testing purposes.
89
+ */
90
+ export function clearObserverCache(): void {
91
+ observerCache.clear();
92
+ }
93
+
42
94
  /**
43
95
  * Observe size changes for a given element reference using shared resize observers.
44
96
  *
45
97
  * @param ref - Ref holding the element whose size to monitor.
46
98
  * @param options - Resize observer configuration.
47
99
  * @returns Latest resize entry and a derived DOMRect snapshot.
100
+ *
101
+ * @remarks
102
+ * The `rect` will be `null` on the first render. After the initial
103
+ * useLayoutEffect runs and triggers a re-render, `rect` will contain
104
+ * the measured size.
105
+ *
106
+ * Due to React's effect execution order, child components' effects run
107
+ * before parent effects. If you pass `rect.width` to a child as a prop,
108
+ * the child's first effect will see `0` (or whatever default you use).
109
+ * This is expected React behavior.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * const containerRef = useRef<HTMLDivElement>(null);
114
+ * const { rect } = useResizeObserver(containerRef, { box: "border-box" });
115
+ * const width = rect?.width ?? 0;
116
+ *
117
+ * // Check if size is ready before using for calculations
118
+ * const isReady = rect !== null;
119
+ * ```
48
120
  */
49
121
  export function useResizeObserver<T extends HTMLElement>(
50
122
  ref: React.RefObject<T | null>,
51
- { box }: ResizeObserverOptions,
123
+ { box = "content-box" }: ResizeObserverOptions,
52
124
  ) {
53
125
  const [entry, setEntry] = React.useState<ResizeObserverEntry | null>(null);
54
- const target = ref.current;
55
126
 
56
- React.useEffect(() => {
127
+ useIsomorphicLayoutEffect(() => {
128
+ const target = ref.current;
57
129
  if (!target) {
130
+ setEntry(null);
58
131
  return;
59
132
  }
60
133
 
61
- const observer = getSharedObserver({ box });
62
- return observer.observe(target, (nextEntry) => {
63
- setEntry(nextEntry);
64
- });
65
- }, [box, target]);
134
+ // Measure immediately
135
+ setEntry(measureElement(target));
136
+
137
+ // Set up ResizeObserver for subsequent updates
138
+ const observer = getSharedObserver(box);
139
+ return observer.observe(target, setEntry);
140
+ }, [ref, box]);
66
141
 
67
142
  const rect = React.useMemo(() => {
68
143
  if (!entry) {
69
144
  return null;
70
145
  }
71
-
72
- if (entry.borderBoxSize?.length > 0) {
73
- const size = entry.borderBoxSize[0];
74
- return new DOMRect(0, 0, size.inlineSize, size.blockSize);
75
- }
76
-
77
- return entry.contentRect;
146
+ return entryToRect(entry);
78
147
  }, [entry]);
79
148
 
80
149
  return { entry, rect };
@@ -5,6 +5,7 @@
5
5
  * Returns null if the document is the scroll container.
6
6
  */
7
7
  import * as React from "react";
8
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
8
9
 
9
10
  /**
10
11
  * Check if an element is a scroll container.
@@ -14,12 +15,7 @@ function isScrollContainer(element: Element): boolean {
14
15
  const overflowY = style.overflowY;
15
16
  const overflowX = style.overflowX;
16
17
 
17
- return (
18
- overflowY === "scroll" ||
19
- overflowY === "auto" ||
20
- overflowX === "scroll" ||
21
- overflowX === "auto"
22
- );
18
+ return overflowY === "scroll" || overflowY === "auto" || overflowX === "scroll" || overflowX === "auto";
23
19
  }
24
20
 
25
21
  /**
@@ -59,12 +55,10 @@ function findScrollContainer(element: Element | null): HTMLElement | null {
59
55
  * // scrollContainer is HTMLElement if in nested scroll, null if document scroll
60
56
  * ```
61
57
  */
62
- export function useScrollContainer<T extends HTMLElement>(
63
- ref: React.RefObject<T | null>,
64
- ): HTMLElement | null {
58
+ export function useScrollContainer<T extends HTMLElement>(ref: React.RefObject<T | null>): HTMLElement | null {
65
59
  const [container, setContainer] = React.useState<HTMLElement | null>(null);
66
60
 
67
- React.useEffect(() => {
61
+ useIsomorphicLayoutEffect(() => {
68
62
  const element = ref.current;
69
63
  if (!element) {
70
64
  setContainer(null);