react-panel-layout 0.5.2 → 0.6.1
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.
- package/dist/{FloatingPanelFrame-lLg-Lpg7.js → FloatingPanelFrame-3eU9AwPo.js} +11 -11
- package/dist/{FloatingPanelFrame-lLg-Lpg7.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
- package/dist/{FloatingPanelFrame-D9Cp2al1.cjs → FloatingPanelFrame-CEmXDvUA.cjs} +2 -2
- package/dist/{FloatingPanelFrame-D9Cp2al1.cjs.map → FloatingPanelFrame-CEmXDvUA.cjs.map} +1 -1
- package/dist/FloatingWindow-CUXnEtrb.js +827 -0
- package/dist/FloatingWindow-CUXnEtrb.js.map +1 -0
- package/dist/FloatingWindow-DMwyK0eK.cjs +2 -0
- package/dist/FloatingWindow-DMwyK0eK.cjs.map +1 -0
- package/dist/GridLayout-DKTg_N61.cjs +2 -0
- package/dist/GridLayout-DKTg_N61.cjs.map +1 -0
- package/dist/GridLayout-UWNxXw77.js +926 -0
- package/dist/GridLayout-UWNxXw77.js.map +1 -0
- package/dist/HorizontalDivider-DdxzfV0l.js +30 -0
- package/dist/HorizontalDivider-DdxzfV0l.js.map +1 -0
- package/dist/HorizontalDivider-_pgV4Mcv.cjs +2 -0
- package/dist/HorizontalDivider-_pgV4Mcv.cjs.map +1 -0
- package/dist/PanelSystem-BqUzNtf2.js +1946 -0
- package/dist/PanelSystem-BqUzNtf2.js.map +1 -0
- package/dist/PanelSystem-D603LKKv.cjs +3 -0
- package/dist/PanelSystem-D603LKKv.cjs.map +1 -0
- package/dist/ResizeHandle-CBcAS918.cjs +2 -0
- package/dist/ResizeHandle-CBcAS918.cjs.map +1 -0
- package/dist/ResizeHandle-CXjc1meV.js +119 -0
- package/dist/ResizeHandle-CXjc1meV.js.map +1 -0
- package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
- package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
- package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
- package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
- package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
- package/dist/components/window/Drawer.d.ts +3 -1
- package/dist/components/window/DrawerLayers.d.ts +1 -1
- package/dist/components/window/drawerStyles.d.ts +69 -0
- package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
- package/dist/components/window/useDrawerSwipeTransform.d.ts +23 -0
- package/dist/config.cjs +1 -1
- package/dist/config.cjs.map +1 -1
- package/dist/config.js +11 -9
- package/dist/config.js.map +1 -1
- package/dist/constants/styles.d.ts +35 -4
- package/dist/dialog/index.d.ts +69 -0
- package/dist/floating.cjs +1 -1
- package/dist/floating.js +1 -1
- package/dist/grid/index.d.ts +58 -0
- package/dist/grid.cjs +2 -0
- package/dist/grid.cjs.map +1 -0
- package/dist/grid.js +13 -0
- package/dist/grid.js.map +1 -0
- package/dist/hooks/gesture/presets.d.ts +33 -0
- package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +117 -0
- package/dist/hooks/gesture/thresholdValue.d.ts +44 -0
- package/dist/hooks/gesture/types.d.ts +297 -0
- package/dist/hooks/gesture/useDirectionalLock.d.ts +20 -0
- package/dist/hooks/gesture/useEdgeSwipeInput.d.ts +23 -0
- package/dist/hooks/gesture/useNativeGestureGuard.d.ts +23 -0
- package/dist/hooks/gesture/usePointerTracking.d.ts +22 -0
- package/dist/hooks/gesture/useScrollBoundary.d.ts +23 -0
- package/dist/hooks/gesture/useSwipeInput.d.ts +5 -0
- package/dist/hooks/gesture/utils.d.ts +59 -0
- package/dist/hooks/useAnimatedVisibility.d.ts +58 -0
- package/dist/hooks/useAnimationFrame.d.ts +86 -0
- package/dist/hooks/useOperationContinuity.d.ts +64 -0
- package/dist/hooks/useResizeObserver.d.ts +33 -1
- package/dist/hooks/useSharedElementTransition.d.ts +112 -0
- package/dist/hooks/useSnapAnimation.d.ts +54 -0
- package/dist/hooks/useSwipeContentTransform.d.ts +86 -0
- package/dist/index.cjs +1 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +25 -2006
- package/dist/index.js.map +1 -1
- package/dist/modules/dialog/AlertDialog.d.ts +9 -0
- package/dist/modules/dialog/DialogContainer.d.ts +37 -0
- package/dist/modules/dialog/Modal.d.ts +26 -0
- package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
- package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
- package/dist/modules/dialog/types.d.ts +183 -0
- package/dist/modules/dialog/useDialog.d.ts +39 -0
- package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
- package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
- package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
- package/dist/modules/drawer/types.d.ts +74 -0
- package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
- package/dist/modules/pivot/PivotContent.d.ts +1 -1
- package/dist/modules/pivot/SwipePivotContent.d.ts +39 -0
- package/dist/modules/pivot/SwipePivotContent.debug.tmp.d.ts +25 -0
- package/dist/modules/pivot/SwipePivotContent.test.d.ts +1 -0
- package/dist/modules/pivot/SwipePivotTabBar.d.ts +92 -0
- package/dist/modules/pivot/index.d.ts +3 -0
- package/dist/modules/pivot/scaleInputState.d.ts +37 -0
- package/dist/modules/pivot/types.d.ts +67 -2
- package/dist/modules/pivot/usePivotSwipeInput.d.ts +68 -0
- package/dist/modules/stack/StackContent.d.ts +15 -0
- package/dist/modules/stack/SwipeStackContent.d.ts +66 -0
- package/dist/modules/stack/SwipeStackOutlet.d.ts +80 -0
- package/dist/modules/stack/computeStackContentState.d.ts +99 -0
- package/dist/modules/stack/computeSwipeStackTransform.d.ts +76 -0
- package/dist/modules/stack/types.d.ts +194 -0
- package/dist/modules/stack/useStackAnimationState.d.ts +32 -0
- package/dist/modules/stack/useStackNavigation.d.ts +23 -0
- package/dist/modules/stack/useStackSwipeInput.d.ts +27 -0
- package/dist/panels/index.d.ts +67 -0
- package/dist/panels.cjs +2 -0
- package/dist/panels.cjs.map +1 -0
- package/dist/panels.js +28 -0
- package/dist/panels.js.map +1 -0
- package/dist/pivot/index.d.ts +3 -0
- package/dist/pivot.cjs +1 -1
- package/dist/pivot.cjs.map +1 -1
- package/dist/pivot.js +20 -2
- package/dist/pivot.js.map +1 -1
- package/dist/resizer/index.d.ts +57 -0
- package/dist/resizer.cjs +2 -0
- package/dist/resizer.cjs.map +1 -0
- package/dist/resizer.js +8 -0
- package/dist/resizer.js.map +1 -0
- package/dist/stack/index.d.ts +72 -0
- package/dist/stack.cjs +2 -0
- package/dist/stack.cjs.map +1 -0
- package/dist/stack.js +721 -0
- package/dist/stack.js.map +1 -0
- package/dist/sticky-header/StickyArea.d.ts +38 -0
- package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
- package/dist/sticky-header/index.d.ts +4 -4
- package/dist/sticky-header/types.d.ts +35 -22
- package/dist/sticky-header.cjs +1 -1
- package/dist/sticky-header.cjs.map +1 -1
- package/dist/sticky-header.js +73 -174
- package/dist/sticky-header.js.map +1 -1
- package/dist/styles-NkjuMOVS.js +57 -0
- package/dist/styles-NkjuMOVS.js.map +1 -0
- package/dist/styles-qf6ptVLD.cjs +2 -0
- package/dist/styles-qf6ptVLD.cjs.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/useContentCache-CO3LYNmz.js +24 -0
- package/dist/useContentCache-CO3LYNmz.js.map +1 -0
- package/dist/useContentCache-DqXtLrLs.cjs +2 -0
- package/dist/useContentCache-DqXtLrLs.cjs.map +1 -0
- package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
- package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
- package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
- package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
- package/dist/useFloatingState-C4kRaW_R.cjs +2 -0
- package/dist/useFloatingState-C4kRaW_R.cjs.map +1 -0
- package/dist/useFloatingState-tEfA_wbc.js +74 -0
- package/dist/useFloatingState-tEfA_wbc.js.map +1 -0
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs +2 -0
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +1 -0
- package/dist/useNativeGestureGuard-CGYo6O0r.js +347 -0
- package/dist/useNativeGestureGuard-CGYo6O0r.js.map +1 -0
- package/dist/window/index.d.ts +63 -0
- package/dist/window.cjs +2 -0
- package/dist/window.cjs.map +1 -0
- package/dist/window.js +160 -0
- package/dist/window.js.map +1 -0
- package/docs/design-tokens.md +405 -0
- package/package.json +34 -4
- package/src/PanelSystemContext.tsx +88 -0
- package/src/components/gesture/SwipeSafeZone.tsx +69 -0
- package/src/components/grid/GridLayerList.tsx +172 -0
- package/src/components/grid/GridLayerResizeHandles.tsx +145 -0
- package/src/components/grid/GridLayout.spec.tsx +743 -0
- package/src/components/grid/GridLayout.tsx +130 -0
- package/src/components/grid/GridTrackResizeHandle.tsx +87 -0
- package/src/components/paneling/FloatingPanelFrame.tsx +203 -0
- package/src/components/panels/DropSuggestOverlay.tsx +131 -0
- package/src/components/panels/PanelGroupView.tsx +112 -0
- package/src/components/pivot/PivotLayer.tsx +27 -0
- package/src/components/resizer/HorizontalDivider.tsx +52 -0
- package/src/components/resizer/ResizeHandle.tsx +118 -0
- package/src/components/tabs/TabBar.tsx +223 -0
- package/src/components/tabs/TabBarTab.tsx +133 -0
- package/src/components/tabs/TabDragOverlay.tsx +92 -0
- package/src/components/window/DialogOverlay.tsx +180 -0
- package/src/components/window/Drawer.tsx +369 -0
- package/src/components/window/DrawerLayers.tsx +68 -0
- package/src/components/window/FloatingWindow.tsx +95 -0
- package/src/components/window/PopupLayerPortal.tsx +218 -0
- package/src/components/window/drawerStyles.spec.ts +263 -0
- package/src/components/window/drawerStyles.ts +228 -0
- package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
- package/src/components/window/drawerSwipeConfig.ts +112 -0
- package/src/components/window/useDrawerSwipeTransform.spec.ts +234 -0
- package/src/components/window/useDrawerSwipeTransform.ts +129 -0
- package/src/config/PanelContentDeclaration.tsx +427 -0
- package/src/config/index.tsx +52 -0
- package/src/config/panelJsx.spec.tsx +54 -0
- package/src/config/panelJsxConfig.spec.tsx +54 -0
- package/src/config/panelJsxDrawer.spec.tsx +33 -0
- package/src/config/panelRouter.spec.ts +68 -0
- package/src/config/panelRouter.tsx +155 -0
- package/src/constants/styles.ts +280 -0
- package/src/demo/Layout.module.css +258 -0
- package/src/demo/Layout.tsx +176 -0
- package/src/demo/components/CodeBlock.module.css +54 -0
- package/src/demo/components/CodeBlock.tsx +34 -0
- package/src/demo/components/CodePreview.module.css +37 -0
- package/src/demo/components/CodePreview.tsx +31 -0
- package/src/demo/components/DataPreview.module.css +177 -0
- package/src/demo/components/DataPreview.tsx +115 -0
- package/src/demo/components/Story.module.css +68 -0
- package/src/demo/components/Story.tsx +54 -0
- package/src/demo/components/layout/CodePanel.module.css +183 -0
- package/src/demo/components/layout/CodePanel.tsx +149 -0
- package/src/demo/components/layout/DemoPage.module.css +60 -0
- package/src/demo/components/layout/DemoPage.tsx +56 -0
- package/src/demo/components/layout/SingleSamplePage.module.css +11 -0
- package/src/demo/components/layout/SingleSamplePage.tsx +35 -0
- package/src/demo/components/layout/SplitDemoLayout.module.css +107 -0
- package/src/demo/components/layout/SplitDemoLayout.tsx +218 -0
- package/src/demo/components/layout/index.ts +11 -0
- package/src/demo/components/tab-styles/ChromeTabBar.module.css +75 -0
- package/src/demo/components/tab-styles/ChromeTabBar.tsx +111 -0
- package/src/demo/components/tab-styles/GitHubTabBar.module.css +81 -0
- package/src/demo/components/tab-styles/GitHubTabBar.tsx +109 -0
- package/src/demo/components/tab-styles/VSCodeTabBar.module.css +78 -0
- package/src/demo/components/tab-styles/VSCodeTabBar.tsx +109 -0
- package/src/demo/components/tab-styles/index.ts +6 -0
- package/src/demo/components/ui/DemoButton.module.css +63 -0
- package/src/demo/components/ui/DemoButton.tsx +32 -0
- package/src/demo/components/ui/DemoCard.module.css +15 -0
- package/src/demo/components/ui/DemoCard.tsx +30 -0
- package/src/demo/components/ui/DemoContainer.module.css +17 -0
- package/src/demo/components/ui/DemoContainer.tsx +30 -0
- package/src/demo/components/ui/DemoPanel.module.css +23 -0
- package/src/demo/components/ui/DemoPanel.tsx +33 -0
- package/src/demo/components/ui/PanelText.module.css +18 -0
- package/src/demo/components/ui/PanelText.tsx +29 -0
- package/src/demo/components/ui/PanelTitle.module.css +18 -0
- package/src/demo/components/ui/PanelTitle.tsx +31 -0
- package/src/demo/contexts/TabbarDemoConfig.tsx +218 -0
- package/src/demo/demo.css +172 -0
- package/src/demo/hooks/useMedia.ts +41 -0
- package/src/demo/hooks/useShikiHighlight.ts +55 -0
- package/src/demo/index.tsx +293 -0
- package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
- package/src/demo/pages/Dialog/card/index.tsx +22 -0
- package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +204 -0
- package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
- package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
- package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
- package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
- package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
- package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
- package/src/demo/pages/Dialog/modal/index.tsx +17 -0
- package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
- package/src/demo/pages/Drawer/animations/index.tsx +22 -0
- package/src/demo/pages/Drawer/basics/index.tsx +17 -0
- package/src/demo/pages/Drawer/components/DrawerAnimations.module.css +125 -0
- package/src/demo/pages/Drawer/components/DrawerAnimations.tsx +118 -0
- package/src/demo/pages/Drawer/components/DrawerBasics.module.css +55 -0
- package/src/demo/pages/Drawer/components/DrawerBasics.tsx +76 -0
- package/src/demo/pages/Drawer/components/DrawerMenuLayout.module.css +332 -0
- package/src/demo/pages/Drawer/components/DrawerMenuLayout.tsx +199 -0
- package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
- package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
- package/src/demo/pages/Drawer/menu/index.tsx +17 -0
- package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
- package/src/demo/pages/FloatingPanelFrame/ResizableFloatingPanelsPreview.module.css +163 -0
- package/src/demo/pages/FloatingPanelFrame/ResizableFloatingPanelsPreview.tsx +234 -0
- package/src/demo/pages/FloatingPanelFrame/basic/index.tsx +17 -0
- package/src/demo/pages/FloatingPanelFrame/complex/index.tsx +26 -0
- package/src/demo/pages/FloatingPanelFrame/components/BasicPanel.module.css +16 -0
- package/src/demo/pages/FloatingPanelFrame/components/BasicPanel.tsx +24 -0
- package/src/demo/pages/FloatingPanelFrame/components/ComplexPanel.module.css +54 -0
- package/src/demo/pages/FloatingPanelFrame/components/ComplexPanel.tsx +67 -0
- package/src/demo/pages/FloatingPanelFrame/components/PanelWithControls.module.css +21 -0
- package/src/demo/pages/FloatingPanelFrame/components/PanelWithControls.tsx +41 -0
- package/src/demo/pages/FloatingPanelFrame/components/PanelWithMeta.module.css +5 -0
- package/src/demo/pages/FloatingPanelFrame/components/PanelWithMeta.tsx +43 -0
- package/src/demo/pages/FloatingPanelFrame/components/ScrollablePanel.module.css +11 -0
- package/src/demo/pages/FloatingPanelFrame/components/ScrollablePanel.tsx +42 -0
- package/src/demo/pages/FloatingPanelFrame/index.tsx +80 -0
- package/src/demo/pages/FloatingPanelFrame/scrollable/index.tsx +30 -0
- package/src/demo/pages/FloatingPanelFrame/with-controls/index.tsx +30 -0
- package/src/demo/pages/FloatingPanelFrame/with-meta/index.tsx +17 -0
- package/src/demo/pages/HorizontalDivider/components/PanelsWithRichContent.module.css +112 -0
- package/src/demo/pages/HorizontalDivider/components/PanelsWithRichContent.tsx +56 -0
- package/src/demo/pages/HorizontalDivider/components/SimpleResizablePanels.module.css +46 -0
- package/src/demo/pages/HorizontalDivider/components/SimpleResizablePanels.tsx +29 -0
- package/src/demo/pages/HorizontalDivider/components/ThreePanelLayout.module.css +54 -0
- package/src/demo/pages/HorizontalDivider/components/ThreePanelLayout.tsx +30 -0
- package/src/demo/pages/HorizontalDivider/index.module.css +14 -0
- package/src/demo/pages/HorizontalDivider/index.tsx +64 -0
- package/src/demo/pages/HorizontalDivider/panels-with-rich-content/index.tsx +21 -0
- package/src/demo/pages/HorizontalDivider/simple-resizable-panels/index.tsx +21 -0
- package/src/demo/pages/HorizontalDivider/three-panel-layout/index.tsx +21 -0
- package/src/demo/pages/PanelLayout/PanelLayoutDemo.module.css +174 -0
- package/src/demo/pages/PanelLayout/PanelLayoutDemo.tsx +248 -0
- package/src/demo/pages/PanelLayout/components/DashboardLayout.module.css +115 -0
- package/src/demo/pages/PanelLayout/components/DashboardLayout.tsx +124 -0
- package/src/demo/pages/PanelLayout/components/DraggableOverlays.module.css +101 -0
- package/src/demo/pages/PanelLayout/components/DraggableOverlays.tsx +122 -0
- package/src/demo/pages/PanelLayout/components/IDELayout.module.css +104 -0
- package/src/demo/pages/PanelLayout/components/IDELayout.tsx +143 -0
- package/src/demo/pages/PanelLayout/components/SimpleGrid.module.css +19 -0
- package/src/demo/pages/PanelLayout/components/SimpleGrid.tsx +62 -0
- package/src/demo/pages/PanelLayout/dashboard/index.tsx +22 -0
- package/src/demo/pages/PanelLayout/draggable-overlays/index.tsx +22 -0
- package/src/demo/pages/PanelLayout/ide-layout/index.tsx +22 -0
- package/src/demo/pages/PanelLayout/index.tsx +94 -0
- package/src/demo/pages/PanelLayout/simple-grid/index.tsx +22 -0
- package/src/demo/pages/PanelSystem/PanelSystemPreview.module.css +20 -0
- package/src/demo/pages/PanelSystem/PanelSystemPreview.tsx +101 -0
- package/src/demo/pages/PanelSystem/preview/index.tsx +18 -0
- package/src/demo/pages/PanelSystem/tabbar/index.tsx +129 -0
- package/src/demo/pages/Pivot/basics/index.tsx +17 -0
- package/src/demo/pages/Pivot/components/Pivot.module.css +278 -0
- package/src/demo/pages/Pivot/components/PivotBasics.tsx +103 -0
- package/src/demo/pages/Pivot/components/PivotSidebar.tsx +168 -0
- package/src/demo/pages/Pivot/components/PivotTabs.tsx +129 -0
- package/src/demo/pages/Pivot/components/PivotTransitions.tsx +120 -0
- package/src/demo/pages/Pivot/components/SwipePivot.module.css +114 -0
- package/src/demo/pages/Pivot/components/SwipePivot.tsx +193 -0
- package/src/demo/pages/Pivot/components/SwipeTabsPivot.module.css +203 -0
- package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +320 -0
- package/src/demo/pages/Pivot/sidebar/index.tsx +17 -0
- package/src/demo/pages/Pivot/swipe/index.tsx +16 -0
- package/src/demo/pages/Pivot/swipe-debug/index.tsx +287 -0
- package/src/demo/pages/Pivot/swipe-tabs/index.tsx +15 -0
- package/src/demo/pages/Pivot/tabs/index.tsx +17 -0
- package/src/demo/pages/Pivot/transitions/index.tsx +17 -0
- package/src/demo/pages/ResizeHandle/both-directions/index.tsx +17 -0
- package/src/demo/pages/ResizeHandle/components/BothDirectionsDemo.module.css +72 -0
- package/src/demo/pages/ResizeHandle/components/BothDirectionsDemo.tsx +41 -0
- package/src/demo/pages/ResizeHandle/components/HorizontalResizeDemo.module.css +61 -0
- package/src/demo/pages/ResizeHandle/components/HorizontalResizeDemo.tsx +33 -0
- package/src/demo/pages/ResizeHandle/components/NestedPanelsDemo.module.css +83 -0
- package/src/demo/pages/ResizeHandle/components/NestedPanelsDemo.tsx +53 -0
- package/src/demo/pages/ResizeHandle/components/VerticalResizeDemo.module.css +68 -0
- package/src/demo/pages/ResizeHandle/components/VerticalResizeDemo.tsx +33 -0
- package/src/demo/pages/ResizeHandle/horizontal/index.tsx +17 -0
- package/src/demo/pages/ResizeHandle/index.module.css +11 -0
- package/src/demo/pages/ResizeHandle/index.tsx +71 -0
- package/src/demo/pages/ResizeHandle/nested-panels/index.tsx +17 -0
- package/src/demo/pages/ResizeHandle/vertical/index.tsx +17 -0
- package/src/demo/pages/ResponsiveLayout/adaptive-workspace/index.tsx +22 -0
- package/src/demo/pages/ResponsiveLayout/components/ResponsiveWorkspace.module.css +423 -0
- package/src/demo/pages/ResponsiveLayout/components/ResponsiveWorkspace.tsx +398 -0
- package/src/demo/pages/Stack/basics/index.tsx +22 -0
- package/src/demo/pages/Stack/components/Stack.module.css +234 -0
- package/src/demo/pages/Stack/components/StackBasics.spec.tsx +152 -0
- package/src/demo/pages/Stack/components/StackBasics.tsx +301 -0
- package/src/demo/pages/Stack/components/StackTablet.module.css +299 -0
- package/src/demo/pages/Stack/components/StackTablet.spec.tsx +120 -0
- package/src/demo/pages/Stack/components/StackTablet.tsx +422 -0
- package/src/demo/pages/Stack/tablet/index.tsx +22 -0
- package/src/demo/pages/StickyHeader/basics/index.tsx +17 -0
- package/src/demo/pages/StickyHeader/components/StickyHeader.module.css +219 -0
- package/src/demo/pages/StickyHeader/components/StickyHeaderBasics.tsx +103 -0
- package/src/demo/routes.tsx +214 -0
- package/src/demo/styles/animations.css +68 -0
- package/src/demo/styles/stack-themes.css +35 -0
- package/src/demo/utils/createPanelView.tsx +58 -0
- package/src/dialog/index.ts +85 -0
- package/src/floating/index.ts +24 -0
- package/src/grid/index.ts +75 -0
- package/src/hooks/ContentCacheContext.tsx +87 -0
- package/src/hooks/gesture/presets.spec.ts +86 -0
- package/src/hooks/gesture/presets.ts +95 -0
- package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +241 -0
- package/src/hooks/gesture/testing/createGestureSimulator.ts +385 -0
- package/src/hooks/gesture/thresholdValue.spec.ts +103 -0
- package/src/hooks/gesture/thresholdValue.ts +77 -0
- package/src/hooks/gesture/types.ts +367 -0
- package/src/hooks/gesture/useDirectionalLock.spec.ts +271 -0
- package/src/hooks/gesture/useDirectionalLock.ts +115 -0
- package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +462 -0
- package/src/hooks/gesture/useEdgeSwipeInput.ts +131 -0
- package/src/hooks/gesture/useNativeGestureGuard.spec.ts +473 -0
- package/src/hooks/gesture/useNativeGestureGuard.ts +135 -0
- package/src/hooks/gesture/usePointerTracking.spec.ts +364 -0
- package/src/hooks/gesture/usePointerTracking.ts +134 -0
- package/src/hooks/gesture/useScrollBoundary.spec.ts +249 -0
- package/src/hooks/gesture/useScrollBoundary.ts +113 -0
- package/src/hooks/gesture/useSwipeInput.spec.ts +592 -0
- package/src/hooks/gesture/useSwipeInput.ts +310 -0
- package/src/hooks/gesture/utils.spec.ts +152 -0
- package/src/hooks/gesture/utils.ts +178 -0
- package/src/hooks/useAnimatedVisibility.spec.ts +277 -0
- package/src/hooks/useAnimatedVisibility.ts +172 -0
- package/src/hooks/useAnimationFrame.ts +208 -0
- package/src/hooks/useCSSMatrix.spec.ts +214 -0
- package/src/hooks/useCSSMatrix.ts +262 -0
- package/src/hooks/useClonedElementPreview.ts +28 -0
- package/src/hooks/useContainerScroll.ts +78 -0
- package/src/hooks/useContentCache.spec.tsx +232 -0
- package/src/hooks/useContentCache.tsx +127 -0
- package/src/hooks/useDocumentPointerEvents.ts +137 -0
- package/src/hooks/useDocumentScroll.ts +41 -0
- package/src/hooks/useEffectEvent.ts +40 -0
- package/src/hooks/useElementComponentWrapper.tsx +63 -0
- package/src/hooks/useIntersectionObserver.tsx +125 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +29 -0
- package/src/hooks/useOperationContinuity.spec.ts +387 -0
- package/src/hooks/useOperationContinuity.ts +135 -0
- package/src/hooks/useResizeObserver.spec.tsx +277 -0
- package/src/hooks/useResizeObserver.tsx +150 -0
- package/src/hooks/useScrollContainer.ts +73 -0
- package/src/hooks/useSharedElementTransition.ts +333 -0
- package/src/hooks/useSnapAnimation.ts +128 -0
- package/src/hooks/useSwipeContentTransform.spec.ts +133 -0
- package/src/hooks/useSwipeContentTransform.ts +373 -0
- package/src/hooks/useTransitionState.ts +95 -0
- package/src/index.tsx +88 -0
- package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
- package/src/modules/dialog/AlertDialog.tsx +221 -0
- package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
- package/src/modules/dialog/DialogContainer.tsx +188 -0
- package/src/modules/dialog/Modal.spec.tsx +220 -0
- package/src/modules/dialog/Modal.tsx +182 -0
- package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
- package/src/modules/dialog/dialogAnimationUtils.spec.ts +253 -0
- package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
- package/src/modules/dialog/types.ts +186 -0
- package/src/modules/dialog/useDialog.spec.tsx +447 -0
- package/src/modules/dialog/useDialog.ts +214 -0
- package/src/modules/dialog/useDialogContainer.spec.ts +331 -0
- package/src/modules/dialog/useDialogContainer.ts +150 -0
- package/src/modules/dialog/useDialogSwipeInput.spec.ts +157 -0
- package/src/modules/dialog/useDialogSwipeInput.ts +319 -0
- package/src/modules/dialog/useDialogTransform.spec.ts +370 -0
- package/src/modules/dialog/useDialogTransform.ts +407 -0
- package/src/modules/drawer/types.ts +102 -0
- package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
- package/src/modules/drawer/useDrawerSwipeInput.ts +399 -0
- package/src/modules/grid/GridLayoutContext.tsx +57 -0
- package/src/modules/grid/LayerInstanceContext.tsx +56 -0
- package/src/modules/grid/resizeHandles.ts +157 -0
- package/src/modules/grid/trackUtils.ts +146 -0
- package/src/modules/grid/useGridPlacements.ts +143 -0
- package/src/modules/grid/useGridTracks.ts +156 -0
- package/src/modules/grid/useLayerDragHandle.ts +16 -0
- package/src/modules/grid/useLayerInteractions.tsx +850 -0
- package/src/modules/keybindings/KeybindingsProvider.tsx +111 -0
- package/src/modules/panels/dom/DomRegistry.tsx +94 -0
- package/src/modules/panels/index.ts +45 -0
- package/src/modules/panels/interactions/InteractionsContext.test.tsx +330 -0
- package/src/modules/panels/interactions/InteractionsContext.tsx +394 -0
- package/src/modules/panels/interactions/dnd.ts +28 -0
- package/src/modules/panels/keybindings/KeybindingsInstaller.tsx +15 -0
- package/src/modules/panels/layout/adapter.ts +124 -0
- package/src/modules/panels/rendering/ContentRegistry.spec.tsx +311 -0
- package/src/modules/panels/rendering/ContentRegistry.tsx +205 -0
- package/src/modules/panels/rendering/GroupContainer.tsx +65 -0
- package/src/modules/panels/rendering/RenderBridge.tsx +115 -0
- package/src/modules/panels/rendering/RenderContext.tsx +31 -0
- package/src/modules/panels/state/PanelSplitHandles.tsx +147 -0
- package/src/modules/panels/state/PanelSystemContext.splitLimits.spec.tsx +50 -0
- package/src/modules/panels/state/PanelSystemContext.tsx +289 -0
- package/src/modules/panels/state/StateContext.tsx +12 -0
- package/src/modules/panels/state/cleanup.ts +37 -0
- package/src/modules/panels/state/commands.ts +53 -0
- package/src/modules/panels/state/focus/Context.tsx +25 -0
- package/src/modules/panels/state/focus/logic.ts +57 -0
- package/src/modules/panels/state/groups/Context.tsx +25 -0
- package/src/modules/panels/state/groups/logic.ts +105 -0
- package/src/modules/panels/state/splitLimits.spec.ts +46 -0
- package/src/modules/panels/state/splitLimits.ts +90 -0
- package/src/modules/panels/state/state.spec.ts +49 -0
- package/src/modules/panels/state/tree/Context.tsx +24 -0
- package/src/modules/panels/state/tree/logic.spec.ts +34 -0
- package/src/modules/panels/state/tree/logic.ts +138 -0
- package/src/modules/panels/state/types.ts +142 -0
- package/src/modules/panels/system/PanelSystem.empty-tabbar.spec.tsx +53 -0
- package/src/modules/panels/system/PanelSystem.tab-click-activates.spec.tsx +44 -0
- package/src/modules/panels/system/PanelSystem.tab-reorder.spec.tsx +64 -0
- package/src/modules/panels/system/PanelSystem.tabs-no-dup.spec.tsx +57 -0
- package/src/modules/panels/system/PanelSystem.tsx +206 -0
- package/src/modules/pivot/PivotContent.spec.tsx +179 -0
- package/src/modules/pivot/PivotContent.tsx +77 -0
- package/src/modules/pivot/SwipePivotContent.debug.tmp.tsx +237 -0
- package/src/modules/pivot/SwipePivotContent.position.spec.tsx +171 -0
- package/src/modules/pivot/SwipePivotContent.spec.tsx +494 -0
- package/src/modules/pivot/SwipePivotContent.test.tsx +502 -0
- package/src/modules/pivot/SwipePivotContent.tsx +197 -0
- package/src/modules/pivot/SwipePivotTabBar.spec.tsx +882 -0
- package/src/modules/pivot/SwipePivotTabBar.tsx +583 -0
- package/src/modules/pivot/index.ts +8 -0
- package/src/modules/pivot/scaleInputState.spec.ts +219 -0
- package/src/modules/pivot/scaleInputState.ts +66 -0
- package/src/modules/pivot/types.ts +139 -0
- package/src/modules/pivot/usePivot.spec.ts +635 -0
- package/src/modules/pivot/usePivot.spec.tsx +186 -0
- package/src/modules/pivot/usePivot.tsx +345 -0
- package/src/modules/pivot/usePivotSwipeInput.spec.ts +708 -0
- package/src/modules/pivot/usePivotSwipeInput.ts +136 -0
- package/src/modules/resizer/useResizeDrag.ts +94 -0
- package/src/modules/stack/StackContent.spec.tsx +264 -0
- package/src/modules/stack/StackContent.tsx +111 -0
- package/src/modules/stack/SwipeStackContent.spec.tsx +1564 -0
- package/src/modules/stack/SwipeStackContent.tsx +366 -0
- package/src/modules/stack/SwipeStackOutlet.spec.tsx +250 -0
- package/src/modules/stack/SwipeStackOutlet.tsx +221 -0
- package/src/modules/stack/computeStackContentState.spec.ts +281 -0
- package/src/modules/stack/computeStackContentState.ts +304 -0
- package/src/modules/stack/computeSwipeStackTransform.spec.ts +186 -0
- package/src/modules/stack/computeSwipeStackTransform.ts +145 -0
- package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
- package/src/modules/stack/types.ts +226 -0
- package/src/modules/stack/useStackAnimationState.spec.ts +188 -0
- package/src/modules/stack/useStackAnimationState.ts +143 -0
- package/src/modules/stack/useStackNavigation.spec.ts +672 -0
- package/src/modules/stack/useStackNavigation.tsx +393 -0
- package/src/modules/stack/useStackSwipeInput.spec.ts +309 -0
- package/src/modules/stack/useStackSwipeInput.ts +139 -0
- package/src/modules/window/useDrawerState.ts +81 -0
- package/src/modules/window/useFloatingState.spec.ts +252 -0
- package/src/modules/window/useFloatingState.ts +141 -0
- package/src/panels/index.ts +119 -0
- package/src/pivot/index.ts +19 -0
- package/src/resizer/index.ts +68 -0
- package/src/stack/index.ts +91 -0
- package/src/sticky-header/StickyArea.tsx +193 -0
- package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
- package/src/sticky-header/calculateStickyMetrics.ts +50 -0
- package/src/sticky-header/index.ts +18 -0
- package/src/sticky-header/types.ts +68 -0
- package/src/types.ts +341 -0
- package/src/utils/CSSMatrix.ts +321 -0
- package/src/utils/css.ts +65 -0
- package/src/utils/dialogUtils.ts +43 -0
- package/src/utils/math.ts +18 -0
- package/src/utils/polyfills/createDialogPolyfill.ts +18 -0
- package/src/utils/typedActions.ts +103 -0
- package/src/vite-env.d.ts +6 -0
- package/src/window/index.ts +69 -0
- package/dist/GridLayout-BQQ63eA1.cjs +0 -2
- package/dist/GridLayout-BQQ63eA1.cjs.map +0 -1
- package/dist/GridLayout-CJTKq7Mp.js +0 -1465
- package/dist/GridLayout-CJTKq7Mp.js.map +0 -1
- package/dist/sticky-header/StickyHeader.d.ts +0 -53
- package/dist/styles-CA2_zLZt.js +0 -52
- package/dist/styles-CA2_zLZt.js.map +0 -1
- package/dist/styles-PsqGOEJP.cjs +0 -2
- package/dist/styles-PsqGOEJP.cjs.map +0 -1
- package/dist/usePivot-7ctin_P_.cjs +0 -2
- package/dist/usePivot-7ctin_P_.cjs.map +0 -1
- package/dist/usePivot-CgQxB8rc.js +0 -124
- package/dist/usePivot-CgQxB8rc.js.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared useIntersectionObserver hook with cached observer instances.
|
|
3
|
+
*/
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
const createIdGenerator = () => {
|
|
7
|
+
const map = new Map<object, number>();
|
|
8
|
+
return (ref: object | null | undefined) => {
|
|
9
|
+
if (!ref) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const existing = map.get(ref);
|
|
13
|
+
if (existing !== undefined) {
|
|
14
|
+
return existing;
|
|
15
|
+
}
|
|
16
|
+
const nextId = map.size;
|
|
17
|
+
map.set(ref, nextId);
|
|
18
|
+
return nextId;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getId = createIdGenerator();
|
|
23
|
+
type Unobserve = () => void;
|
|
24
|
+
type Callback = (entry: IntersectionObserverEntry) => void;
|
|
25
|
+
type SharedObserver = {
|
|
26
|
+
observe: (target: Element, callback: Callback) => Unobserve;
|
|
27
|
+
};
|
|
28
|
+
const observerCache = new Map<string, SharedObserver>();
|
|
29
|
+
const getSharedObserver = (options: IntersectionObserverInit) => {
|
|
30
|
+
const observerKey = `ovs-threshold:${options.threshold}-rootMargin:${options.rootMargin}-root:${getId(options.root)}`;
|
|
31
|
+
|
|
32
|
+
if (observerCache.has(observerKey)) {
|
|
33
|
+
return observerCache.get(observerKey)!;
|
|
34
|
+
}
|
|
35
|
+
const observer = new (class {
|
|
36
|
+
#callbackMap = new Map<Element, Callback>();
|
|
37
|
+
#intersectionObserver = new IntersectionObserver((entries) => {
|
|
38
|
+
entries.forEach((entry) => {
|
|
39
|
+
const callback = this.#callbackMap.get(entry.target);
|
|
40
|
+
if (callback) {
|
|
41
|
+
callback(entry);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}, options);
|
|
45
|
+
observe(target: Element, callback: Callback) {
|
|
46
|
+
this.#callbackMap.set(target, callback);
|
|
47
|
+
this.#intersectionObserver.observe(target);
|
|
48
|
+
return () => {
|
|
49
|
+
this.#callbackMap.delete(target);
|
|
50
|
+
this.#intersectionObserver.unobserve(target);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
observerCache.set(observerKey, observer);
|
|
55
|
+
|
|
56
|
+
return observer;
|
|
57
|
+
};
|
|
58
|
+
const voidClientRect = Object.freeze({
|
|
59
|
+
x: 0,
|
|
60
|
+
y: 0,
|
|
61
|
+
width: 0,
|
|
62
|
+
height: 0,
|
|
63
|
+
top: 0,
|
|
64
|
+
right: 0,
|
|
65
|
+
bottom: 0,
|
|
66
|
+
left: 0,
|
|
67
|
+
}) as DOMRectReadOnly;
|
|
68
|
+
/**
|
|
69
|
+
* Observe intersection changes for a given element reference using shared observers.
|
|
70
|
+
*
|
|
71
|
+
* @param ref - Ref holding the element to observe.
|
|
72
|
+
* @param options - Intersection observer configuration.
|
|
73
|
+
* @returns Latest intersection entry snapshot with sensible defaults.
|
|
74
|
+
*/
|
|
75
|
+
export function useIntersectionObserver<T extends HTMLElement>(
|
|
76
|
+
ref: React.RefObject<T | null>,
|
|
77
|
+
{ threshold = 0, rootMargin = "0px", root = null }: IntersectionObserverInit,
|
|
78
|
+
): {
|
|
79
|
+
readonly boundingClientRect: DOMRectReadOnly;
|
|
80
|
+
readonly intersectionRatio: number;
|
|
81
|
+
readonly intersectionRect: DOMRectReadOnly;
|
|
82
|
+
readonly isIntersecting: boolean;
|
|
83
|
+
readonly rootBounds: DOMRectReadOnly | null;
|
|
84
|
+
readonly target: Element | null;
|
|
85
|
+
readonly time: DOMHighResTimeStamp;
|
|
86
|
+
} {
|
|
87
|
+
const [intersection, setIntersection] = React.useState<IntersectionObserverEntry | null>(null);
|
|
88
|
+
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
const target = ref.current;
|
|
91
|
+
if (!target) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const observer = getSharedObserver({
|
|
96
|
+
threshold,
|
|
97
|
+
rootMargin,
|
|
98
|
+
root,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return observer.observe(target, (entry) => {
|
|
102
|
+
setIntersection({
|
|
103
|
+
isIntersecting: entry.isIntersecting,
|
|
104
|
+
boundingClientRect: entry.boundingClientRect,
|
|
105
|
+
intersectionRatio: entry.intersectionRatio,
|
|
106
|
+
intersectionRect: entry.intersectionRect,
|
|
107
|
+
rootBounds: entry.rootBounds,
|
|
108
|
+
target: entry.target,
|
|
109
|
+
time: entry.time,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}, [ref, threshold, rootMargin, root]);
|
|
113
|
+
|
|
114
|
+
return React.useMemo(() => {
|
|
115
|
+
return {
|
|
116
|
+
isIntersecting: intersection?.isIntersecting ?? false,
|
|
117
|
+
boundingClientRect: intersection?.boundingClientRect ?? voidClientRect,
|
|
118
|
+
intersectionRatio: intersection?.intersectionRatio ?? 0,
|
|
119
|
+
intersectionRect: intersection?.intersectionRect ?? voidClientRect,
|
|
120
|
+
rootBounds: intersection?.rootBounds ?? null,
|
|
121
|
+
target: intersection?.target ?? ref.current,
|
|
122
|
+
time: intersection?.time ?? 0,
|
|
123
|
+
};
|
|
124
|
+
}, [intersection, ref]);
|
|
125
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useIsomorphicLayoutEffect - SSR-safe version of useLayoutEffect
|
|
3
|
+
*
|
|
4
|
+
* Uses useLayoutEffect on the client and useEffect on the server to avoid warnings
|
|
5
|
+
* during server-side rendering (e.g., with Next.js)
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if we're running in a browser environment
|
|
11
|
+
*/
|
|
12
|
+
const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SSR-safe version of useLayoutEffect
|
|
16
|
+
*
|
|
17
|
+
* - Client: Uses useLayoutEffect for synchronous layout updates
|
|
18
|
+
* - Server: Uses useEffect to avoid SSR warnings
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* useIsomorphicLayoutEffect(() => {
|
|
23
|
+
* // This runs synchronously after DOM mutations on the client
|
|
24
|
+
* // but safely falls back to useEffect on the server
|
|
25
|
+
* measureElement();
|
|
26
|
+
* }, [deps]);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for useOperationContinuity hook.
|
|
3
|
+
*/
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { renderHook } from "@testing-library/react";
|
|
6
|
+
import { useOperationContinuity } from "./useOperationContinuity.js";
|
|
7
|
+
|
|
8
|
+
const StrictModeWrapper = ({ children }: { children: React.ReactNode }): React.ReactNode => {
|
|
9
|
+
return React.createElement(React.StrictMode, null, children);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("useOperationContinuity", () => {
|
|
13
|
+
describe("value continuity", () => {
|
|
14
|
+
it("returns current value when not retaining", () => {
|
|
15
|
+
const { result } = renderHook(() => useOperationContinuity("active", false));
|
|
16
|
+
expect(result.current.value).toBe("active");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns current value when retaining but value unchanged", () => {
|
|
20
|
+
const { result } = renderHook(() => useOperationContinuity("active", true));
|
|
21
|
+
expect(result.current.value).toBe("active");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("retains previous value when shouldRetainPrevious is true", () => {
|
|
25
|
+
const { result, rerender } = renderHook(
|
|
26
|
+
({ value, shouldRetainPrevious }) => useOperationContinuity(value, shouldRetainPrevious),
|
|
27
|
+
{ initialProps: { value: "behind", shouldRetainPrevious: true } },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(result.current.value).toBe("behind");
|
|
31
|
+
|
|
32
|
+
// Value changes but we're still retaining
|
|
33
|
+
rerender({ value: "active", shouldRetainPrevious: true });
|
|
34
|
+
expect(result.current.value).toBe("behind");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("accepts new value when shouldRetainPrevious becomes false", () => {
|
|
38
|
+
const { result, rerender } = renderHook(
|
|
39
|
+
({ value, shouldRetainPrevious }) => useOperationContinuity(value, shouldRetainPrevious),
|
|
40
|
+
{ initialProps: { value: "behind", shouldRetainPrevious: true } },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Value changes while retaining
|
|
44
|
+
rerender({ value: "active", shouldRetainPrevious: true });
|
|
45
|
+
expect(result.current.value).toBe("behind");
|
|
46
|
+
|
|
47
|
+
// Stop retaining - should accept new value
|
|
48
|
+
rerender({ value: "active", shouldRetainPrevious: false });
|
|
49
|
+
expect(result.current.value).toBe("active");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("updates stored value when not retaining", () => {
|
|
53
|
+
const { result, rerender } = renderHook(
|
|
54
|
+
({ value, shouldRetainPrevious }) => useOperationContinuity(value, shouldRetainPrevious),
|
|
55
|
+
{ initialProps: { value: "behind", shouldRetainPrevious: false } },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
rerender({ value: "active", shouldRetainPrevious: false });
|
|
59
|
+
expect(result.current.value).toBe("active");
|
|
60
|
+
|
|
61
|
+
// Start retaining - should keep "active"
|
|
62
|
+
rerender({ value: "hidden", shouldRetainPrevious: true });
|
|
63
|
+
expect(result.current.value).toBe("active");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("changedDuringOperation tracking", () => {
|
|
68
|
+
it("returns false when value never changed", () => {
|
|
69
|
+
const { result, rerender } = renderHook(
|
|
70
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
71
|
+
{ initialProps: { value: "active", retain: true } },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
75
|
+
|
|
76
|
+
// End retention without value change
|
|
77
|
+
rerender({ value: "active", retain: false });
|
|
78
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns true when value changed during retention", () => {
|
|
82
|
+
const { result, rerender } = renderHook(
|
|
83
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
84
|
+
{ initialProps: { value: "behind", retain: true } },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
88
|
+
|
|
89
|
+
// Value changes while retaining
|
|
90
|
+
rerender({ value: "active", retain: true });
|
|
91
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
92
|
+
|
|
93
|
+
// End retention - should still be true (for this render)
|
|
94
|
+
rerender({ value: "active", retain: false });
|
|
95
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("resets changedDuringOperation after operation ends", () => {
|
|
99
|
+
const { result, rerender } = renderHook(
|
|
100
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
101
|
+
{ initialProps: { value: "behind", retain: true } },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Value changes while retaining
|
|
105
|
+
rerender({ value: "active", retain: true });
|
|
106
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
107
|
+
|
|
108
|
+
// End retention
|
|
109
|
+
rerender({ value: "active", retain: false });
|
|
110
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
111
|
+
|
|
112
|
+
// Next render - should be reset
|
|
113
|
+
rerender({ value: "active", retain: false });
|
|
114
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("tracks changes across multiple operations", () => {
|
|
118
|
+
const { result, rerender } = renderHook(
|
|
119
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
120
|
+
{ initialProps: { value: "behind", retain: true } },
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// First operation: value changes
|
|
124
|
+
rerender({ value: "active", retain: true });
|
|
125
|
+
rerender({ value: "active", retain: false });
|
|
126
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
127
|
+
|
|
128
|
+
// Reset
|
|
129
|
+
rerender({ value: "active", retain: false });
|
|
130
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
131
|
+
|
|
132
|
+
// Second operation: no value change
|
|
133
|
+
rerender({ value: "active", retain: true });
|
|
134
|
+
rerender({ value: "active", retain: false });
|
|
135
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
136
|
+
|
|
137
|
+
// Third operation: value changes again
|
|
138
|
+
rerender({ value: "active", retain: true });
|
|
139
|
+
rerender({ value: "hidden", retain: true });
|
|
140
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
141
|
+
rerender({ value: "hidden", retain: false });
|
|
142
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("simultaneous value and retention change", () => {
|
|
147
|
+
/**
|
|
148
|
+
* CRITICAL: This tests the over-swipe bug scenario.
|
|
149
|
+
*
|
|
150
|
+
* In the real app, when user releases after over-swipe:
|
|
151
|
+
* - displacement becomes 0 (shouldRetainPrevious becomes false)
|
|
152
|
+
* - role changes from "active" to "hidden"
|
|
153
|
+
* Both happen in the same render!
|
|
154
|
+
*
|
|
155
|
+
* The hook should detect that the value changed even though
|
|
156
|
+
* the change happened at the exact moment retention ended.
|
|
157
|
+
*/
|
|
158
|
+
it("detects value change when it happens simultaneously with retention ending", () => {
|
|
159
|
+
const { result, rerender } = renderHook(
|
|
160
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
161
|
+
{ initialProps: { role: "active" as const, displacement: 500 } },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// During swipe: role="active", retaining
|
|
165
|
+
expect(result.current.value).toBe("active");
|
|
166
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
167
|
+
|
|
168
|
+
// Simulate release: BOTH displacement becomes 0 AND role changes to "hidden"
|
|
169
|
+
// This is what happens in the real app during over-swipe
|
|
170
|
+
rerender({ role: "hidden" as const, displacement: 0 });
|
|
171
|
+
|
|
172
|
+
// value should now be "hidden" (retention ended)
|
|
173
|
+
expect(result.current.value).toBe("hidden");
|
|
174
|
+
// CRITICAL: changedDuringOperation should be TRUE because the value
|
|
175
|
+
// changed from "active" to "hidden" at the moment retention ended
|
|
176
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("does not report change when value stays the same at retention end", () => {
|
|
180
|
+
const { result, rerender } = renderHook(
|
|
181
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
182
|
+
{ initialProps: { role: "active" as const, displacement: 500 } },
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(result.current.value).toBe("active");
|
|
186
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
187
|
+
|
|
188
|
+
// Release but role stays "active" (e.g., partial swipe that didn't trigger navigation)
|
|
189
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
190
|
+
|
|
191
|
+
expect(result.current.value).toBe("active");
|
|
192
|
+
// No change occurred
|
|
193
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("does NOT report change during button navigation (no operation)", () => {
|
|
197
|
+
// This is the button navigation case: value changes but there was never
|
|
198
|
+
// any retention (no swipe operation). We should NOT report changedDuringOperation
|
|
199
|
+
// because this is normal navigation, not an operation-related change.
|
|
200
|
+
const { result, rerender } = renderHook(
|
|
201
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
202
|
+
{ initialProps: { role: "active" as const, displacement: 0 } },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(result.current.value).toBe("active");
|
|
206
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
207
|
+
|
|
208
|
+
// Button navigation: role changes but there's no operation (displacement is always 0)
|
|
209
|
+
rerender({ role: "behind" as const, displacement: 0 });
|
|
210
|
+
|
|
211
|
+
expect(result.current.value).toBe("behind");
|
|
212
|
+
// CRITICAL: changedDuringOperation should be FALSE because there was no operation
|
|
213
|
+
// This allows the animation to happen normally for button navigation
|
|
214
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("role transition scenarios", () => {
|
|
219
|
+
it("maintains role continuity during swipe (behind -> active)", () => {
|
|
220
|
+
// Simulates: behind panel becomes active during swipe
|
|
221
|
+
// displacement > 0, so we should retain the previous role
|
|
222
|
+
const { result, rerender } = renderHook(
|
|
223
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
224
|
+
{ initialProps: { role: "behind" as const, displacement: 100 } },
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(result.current.value).toBe("behind");
|
|
228
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
229
|
+
|
|
230
|
+
// Navigation changes role to "active" but displacement is still positive
|
|
231
|
+
rerender({ role: "active" as const, displacement: 100 });
|
|
232
|
+
expect(result.current.value).toBe("behind");
|
|
233
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
234
|
+
|
|
235
|
+
// Swipe ends (displacement becomes 0)
|
|
236
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
237
|
+
expect(result.current.value).toBe("active");
|
|
238
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("maintains role continuity during swipe (active -> hidden)", () => {
|
|
242
|
+
// Simulates: over-swipe where active panel becomes hidden
|
|
243
|
+
const { result, rerender } = renderHook(
|
|
244
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
245
|
+
{ initialProps: { role: "active" as const, displacement: 400 } },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(result.current.value).toBe("active");
|
|
249
|
+
|
|
250
|
+
// Over-swipe triggers navigation change
|
|
251
|
+
rerender({ role: "hidden" as const, displacement: 500 });
|
|
252
|
+
expect(result.current.value).toBe("active");
|
|
253
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
254
|
+
|
|
255
|
+
// Swipe ends
|
|
256
|
+
rerender({ role: "hidden" as const, displacement: 0 });
|
|
257
|
+
expect(result.current.value).toBe("hidden");
|
|
258
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("provides changedDuringOperation for animation decision", () => {
|
|
262
|
+
// This test demonstrates the intended use case:
|
|
263
|
+
// Use changedDuringOperation to decide whether to animate on operation end
|
|
264
|
+
const { result, rerender } = renderHook(
|
|
265
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
266
|
+
{ initialProps: { role: "behind" as const, displacement: 100 } },
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Simulate role change during swipe
|
|
270
|
+
rerender({ role: "active" as const, displacement: 100 });
|
|
271
|
+
|
|
272
|
+
// When swipe ends, changedDuringOperation tells us to skip animation
|
|
273
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
274
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
275
|
+
// Consumer would use this to skip target change animation
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("works with different value types", () => {
|
|
280
|
+
it("works with numbers", () => {
|
|
281
|
+
const { result, rerender } = renderHook(
|
|
282
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
283
|
+
{ initialProps: { value: 0, retain: true } },
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
rerender({ value: 1, retain: true });
|
|
287
|
+
expect(result.current.value).toBe(0);
|
|
288
|
+
|
|
289
|
+
rerender({ value: 1, retain: false });
|
|
290
|
+
expect(result.current.value).toBe(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("works with objects (by reference)", () => {
|
|
294
|
+
const obj1 = { id: 1 };
|
|
295
|
+
const obj2 = { id: 2 };
|
|
296
|
+
|
|
297
|
+
const { result, rerender } = renderHook(
|
|
298
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
299
|
+
{ initialProps: { value: obj1, retain: true } },
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
rerender({ value: obj2, retain: true });
|
|
303
|
+
expect(result.current.value).toBe(obj1);
|
|
304
|
+
|
|
305
|
+
rerender({ value: obj2, retain: false });
|
|
306
|
+
expect(result.current.value).toBe(obj2);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("React StrictMode compatibility", () => {
|
|
311
|
+
/**
|
|
312
|
+
* CRITICAL: These tests verify the hook works correctly in StrictMode.
|
|
313
|
+
*
|
|
314
|
+
* In StrictMode, React calls the render function twice. Hooks that mutate
|
|
315
|
+
* refs during render will see the mutated value on the second call, which
|
|
316
|
+
* can cause bugs.
|
|
317
|
+
*
|
|
318
|
+
* This hook uses useLayoutEffect for ref mutations to avoid this issue.
|
|
319
|
+
*/
|
|
320
|
+
it("operationJustEnded is correct in StrictMode", () => {
|
|
321
|
+
const { result, rerender } = renderHook(
|
|
322
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
323
|
+
{
|
|
324
|
+
initialProps: { value: "active", retain: true },
|
|
325
|
+
wrapper: StrictModeWrapper,
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// During retention
|
|
330
|
+
expect(result.current.operationJustEnded).toBe(false);
|
|
331
|
+
|
|
332
|
+
// End retention - operationJustEnded should be true
|
|
333
|
+
rerender({ value: "active", retain: false });
|
|
334
|
+
expect(result.current.operationJustEnded).toBe(true);
|
|
335
|
+
|
|
336
|
+
// Next render - should be false again
|
|
337
|
+
rerender({ value: "active", retain: false });
|
|
338
|
+
expect(result.current.operationJustEnded).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("over-swipe scenario works in StrictMode", () => {
|
|
342
|
+
// This is the exact scenario that was broken before the fix:
|
|
343
|
+
// User swipes beyond 100%, releases, and we need operationJustEnded=true
|
|
344
|
+
// to prevent the visual jump.
|
|
345
|
+
const { result, rerender } = renderHook(
|
|
346
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
347
|
+
{
|
|
348
|
+
initialProps: { role: "active" as const, displacement: 500 },
|
|
349
|
+
wrapper: StrictModeWrapper,
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// During over-swipe
|
|
354
|
+
expect(result.current.value).toBe("active");
|
|
355
|
+
expect(result.current.operationJustEnded).toBe(false);
|
|
356
|
+
|
|
357
|
+
// Release (displacement becomes 0)
|
|
358
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
359
|
+
|
|
360
|
+
// CRITICAL: operationJustEnded must be true even in StrictMode
|
|
361
|
+
// This is what was broken before the fix
|
|
362
|
+
expect(result.current.operationJustEnded).toBe(true);
|
|
363
|
+
expect(result.current.value).toBe("active");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("changedDuringOperation is tracked correctly in StrictMode", () => {
|
|
367
|
+
const { result, rerender } = renderHook(
|
|
368
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
369
|
+
{
|
|
370
|
+
initialProps: { value: "behind", retain: true },
|
|
371
|
+
wrapper: StrictModeWrapper,
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
376
|
+
|
|
377
|
+
// Value changes during retention
|
|
378
|
+
rerender({ value: "active", retain: true });
|
|
379
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
380
|
+
|
|
381
|
+
// End retention
|
|
382
|
+
rerender({ value: "active", retain: false });
|
|
383
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
384
|
+
expect(result.current.operationJustEnded).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Hook for maintaining value continuity during continuous operations.
|
|
3
|
+
*
|
|
4
|
+
* During operations like swipe gestures, external state (navigation depth, panel roles)
|
|
5
|
+
* may change before the gesture ends. This hook provides a pattern to:
|
|
6
|
+
* - Retain the previous value during the operation for visual continuity
|
|
7
|
+
* - Accept the new value when the operation ends
|
|
8
|
+
* - Track whether the value changed during the operation
|
|
9
|
+
*
|
|
10
|
+
* This is a core primitive for the "operation continuity" pattern used throughout
|
|
11
|
+
* the swipe gesture system.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result from useOperationContinuity hook.
|
|
17
|
+
*/
|
|
18
|
+
export type UseOperationContinuityResult<T> = {
|
|
19
|
+
/** The effective value (retained during operation, current after) */
|
|
20
|
+
value: T;
|
|
21
|
+
/**
|
|
22
|
+
* True if the value changed during the operation.
|
|
23
|
+
*
|
|
24
|
+
* This is useful for determining how to handle the transition when the
|
|
25
|
+
* operation ends. For example, if the role changed during a swipe,
|
|
26
|
+
* the target position change at operation end should snap rather than animate.
|
|
27
|
+
*
|
|
28
|
+
* This flag is true on the render where shouldRetainPrevious becomes false
|
|
29
|
+
* (operation end), allowing consumers to handle the transition appropriately.
|
|
30
|
+
* It resets to false on subsequent renders.
|
|
31
|
+
*/
|
|
32
|
+
changedDuringOperation: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* True on the render where the operation just ended.
|
|
35
|
+
*
|
|
36
|
+
* This is true when shouldRetainPrevious transitions from true to false,
|
|
37
|
+
* regardless of whether the value changed. Use this to detect the moment
|
|
38
|
+
* when an operation completes and delay any immediate animations.
|
|
39
|
+
*
|
|
40
|
+
* In the over-swipe case, this helps prevent unwanted snap-back animation
|
|
41
|
+
* in the intermediate render before navigation changes.
|
|
42
|
+
*/
|
|
43
|
+
operationJustEnded: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hook for maintaining value continuity during continuous operations.
|
|
48
|
+
*
|
|
49
|
+
* When an operation is in progress, this hook retains the previous value
|
|
50
|
+
* to prevent sudden visual changes from state updates. Once the operation
|
|
51
|
+
* ends (shouldRetainPrevious becomes false), the new value is accepted.
|
|
52
|
+
*
|
|
53
|
+
* Additionally, this hook tracks whether the value changed during the operation,
|
|
54
|
+
* which is useful for determining animation behavior at operation end.
|
|
55
|
+
*
|
|
56
|
+
* IMPORTANT: This hook is designed to be idempotent during render to work
|
|
57
|
+
* correctly with React StrictMode, which calls the render function twice.
|
|
58
|
+
* All ref mutations happen in useLayoutEffect, not during render.
|
|
59
|
+
*
|
|
60
|
+
* @param value - The current value from external state
|
|
61
|
+
* @param shouldRetainPrevious - Whether to retain the previous value (true during operation)
|
|
62
|
+
* @returns Object with effective value and whether it changed during operation
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* // Maintain role continuity during swipe
|
|
67
|
+
* const { value: effectiveRole, changedDuringOperation } = useOperationContinuity(
|
|
68
|
+
* role,
|
|
69
|
+
* displacement > 0,
|
|
70
|
+
* );
|
|
71
|
+
*
|
|
72
|
+
* // Use changedDuringOperation to skip animation on operation end
|
|
73
|
+
* useSwipeContentTransform({
|
|
74
|
+
* // ...
|
|
75
|
+
* skipTargetChangeAnimation: changedDuringOperation,
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function useOperationContinuity<T>(
|
|
80
|
+
value: T,
|
|
81
|
+
shouldRetainPrevious: boolean,
|
|
82
|
+
): UseOperationContinuityResult<T> {
|
|
83
|
+
// Store previous shouldRetainPrevious to detect transitions
|
|
84
|
+
const prevShouldRetainRef = React.useRef(shouldRetainPrevious);
|
|
85
|
+
// Store retained value (the value at the start of retention)
|
|
86
|
+
const retainedValueRef = React.useRef(value);
|
|
87
|
+
// Track if value changed during retention
|
|
88
|
+
const changedDuringRetentionRef = React.useRef(false);
|
|
89
|
+
|
|
90
|
+
// Derive operationJustEnded from transition: true → false
|
|
91
|
+
// This is idempotent - safe for StrictMode double-render
|
|
92
|
+
const wasRetaining = prevShouldRetainRef.current;
|
|
93
|
+
const operationJustEnded = wasRetaining && !shouldRetainPrevious;
|
|
94
|
+
|
|
95
|
+
// Check if value diverged from retained value
|
|
96
|
+
// This includes both current-render divergence and previously-tracked divergence
|
|
97
|
+
const valueDiverged = value !== retainedValueRef.current;
|
|
98
|
+
const currentlyDiverged = shouldRetainPrevious && valueDiverged;
|
|
99
|
+
|
|
100
|
+
// Derive changedDuringOperation
|
|
101
|
+
// True if:
|
|
102
|
+
// 1. Value diverged during retention (tracked from previous renders via ref)
|
|
103
|
+
// 2. Value diverges right now during retention (immediate comparison)
|
|
104
|
+
// 3. Value diverged at the moment retention ends
|
|
105
|
+
const changedDuringRetention = changedDuringRetentionRef.current || currentlyDiverged;
|
|
106
|
+
const changedAtExit = operationJustEnded && valueDiverged;
|
|
107
|
+
const changedDuringOperation = changedDuringRetention || changedAtExit;
|
|
108
|
+
|
|
109
|
+
// Determine effective value
|
|
110
|
+
// During retention: use retained value
|
|
111
|
+
// After retention ends: use current value
|
|
112
|
+
const effectiveValue = shouldRetainPrevious ? retainedValueRef.current : value;
|
|
113
|
+
|
|
114
|
+
// Update refs in useLayoutEffect to ensure idempotency during render.
|
|
115
|
+
// This runs once per commit, not per render in StrictMode.
|
|
116
|
+
React.useLayoutEffect(() => {
|
|
117
|
+
if (!shouldRetainPrevious) {
|
|
118
|
+
// Retention ended or never started - reset state
|
|
119
|
+
changedDuringRetentionRef.current = false;
|
|
120
|
+
retainedValueRef.current = value;
|
|
121
|
+
} else {
|
|
122
|
+
// During retention - track if value diverged
|
|
123
|
+
if (currentlyDiverged) {
|
|
124
|
+
changedDuringRetentionRef.current = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
prevShouldRetainRef.current = shouldRetainPrevious;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
value: effectiveValue,
|
|
132
|
+
changedDuringOperation,
|
|
133
|
+
operationJustEnded,
|
|
134
|
+
};
|
|
135
|
+
}
|