@lodev09/react-native-true-sheet 4.0.0-beta.0 → 4.0.0-beta.10
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/LICENSE +1 -1
- package/README.md +62 -30
- package/RNTrueSheet.podspec +10 -2
- package/android/build.gradle +24 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +189 -47
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +401 -19
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +18 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +145 -21
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +14 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +28 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +84 -24
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +6 -4
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt +72 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekViewManager.kt +33 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +63 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +564 -226
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +1399 -598
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +161 -29
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +63 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +36 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +255 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +375 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +299 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +162 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +180 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +207 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +220 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +127 -0
- package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetStateEvents.kt +56 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +84 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +93 -67
- package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +42 -0
- package/android/src/main/jni/CMakeLists.txt +63 -0
- package/android/src/main/jni/TrueSheetSpec.h +20 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +16 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +68 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +33 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +39 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +63 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +36 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +30 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +45 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +23 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +52 -0
- package/ios/TrueSheetContainerView.h +88 -6
- package/ios/TrueSheetContainerView.mm +278 -21
- package/ios/TrueSheetContentView.h +54 -1
- package/ios/TrueSheetContentView.mm +371 -47
- package/ios/TrueSheetFooterView.h +33 -2
- package/ios/TrueSheetFooterView.mm +187 -35
- package/ios/{events/OnWillDismissEvent.h → TrueSheetHeaderView.h} +10 -7
- package/ios/TrueSheetHeaderView.mm +64 -0
- package/ios/TrueSheetModule.h +2 -7
- package/ios/TrueSheetModule.mm +98 -20
- package/ios/TrueSheetOverlayView.h +32 -0
- package/ios/TrueSheetOverlayView.mm +161 -0
- package/ios/TrueSheetPeekView.h +32 -0
- package/ios/TrueSheetPeekView.mm +99 -0
- package/ios/TrueSheetView.h +7 -15
- package/ios/TrueSheetView.mm +775 -215
- package/ios/TrueSheetViewController.h +94 -16
- package/ios/TrueSheetViewController.mm +1316 -331
- package/ios/core/RNScreensEventObserver.h +46 -0
- package/ios/core/RNScreensEventObserver.mm +280 -0
- package/ios/core/TrueSheetBlurView.h +27 -0
- package/ios/core/TrueSheetBlurView.mm +82 -0
- package/ios/core/TrueSheetDetentCalculator.h +107 -0
- package/ios/core/TrueSheetDetentCalculator.mm +197 -0
- package/ios/core/TrueSheetGrabberView.h +72 -0
- package/ios/core/TrueSheetGrabberView.mm +188 -0
- package/ios/core/TrueSheetKeyboardObserver.h +39 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +112 -0
- package/ios/core/TrueSheetOverlayContainerView.h +31 -0
- package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
- package/ios/events/TrueSheetDragEvents.h +39 -0
- package/ios/events/TrueSheetDragEvents.mm +62 -0
- package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -5
- package/ios/events/TrueSheetFocusEvents.mm +49 -0
- package/ios/events/TrueSheetLifecycleEvents.h +42 -0
- package/ios/events/TrueSheetLifecycleEvents.mm +79 -0
- package/ios/events/TrueSheetStateEvents.h +35 -0
- package/ios/events/TrueSheetStateEvents.mm +49 -0
- package/ios/tvos/TrueSheetStubs.mm +166 -0
- package/ios/{events/OnMountEvent.h → utils/BlurUtil.h} +5 -7
- package/ios/utils/BlurUtil.mm +69 -0
- package/ios/utils/GestureUtil.h +7 -0
- package/ios/utils/GestureUtil.mm +12 -0
- package/ios/utils/PlatformUtil.h +15 -0
- package/ios/utils/UIView+FirstResponder.h +15 -0
- package/ios/utils/UIView+FirstResponder.mm +26 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.h +26 -0
- package/ios/utils/UIView+ScrollEdgeInteraction.mm +84 -0
- package/ios/utils/WindowUtil.mm +17 -1
- package/lib/module/TrueSheet.js +349 -74
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +1270 -0
- package/lib/module/TrueSheet.web.js.map +1 -0
- package/lib/module/TrueSheetOverlay.js +30 -0
- package/lib/module/TrueSheetOverlay.js.map +1 -0
- package/lib/module/TrueSheetOverlay.web.js +35 -0
- package/lib/module/TrueSheetOverlay.web.js.map +1 -0
- package/lib/module/TrueSheetPeek.js +15 -0
- package/lib/module/TrueSheetPeek.js.map +1 -0
- package/lib/module/TrueSheetPeek.web.js +55 -0
- package/lib/module/TrueSheetPeek.web.js.map +1 -0
- package/lib/module/TrueSheetProvider.js +28 -0
- package/lib/module/TrueSheetProvider.js.map +1 -0
- package/lib/module/TrueSheetProvider.web.js +157 -0
- package/lib/module/TrueSheetProvider.web.js.map +1 -0
- package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/lib/module/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +110 -18
- package/lib/module/index.js +4 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/mocks/index.js +103 -0
- package/lib/module/mocks/index.js.map +1 -0
- package/lib/module/mocks/navigation.js +95 -0
- package/lib/module/mocks/navigation.js.map +1 -0
- package/lib/module/mocks/navigationExpoRouter.js +21 -0
- package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
- package/lib/module/mocks/reanimated.js +91 -0
- package/lib/module/mocks/reanimated.js.map +1 -0
- package/lib/module/navigation/TrueSheetNavigatorContent.js +73 -0
- package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
- package/lib/module/navigation/actions.js +50 -0
- package/lib/module/navigation/actions.js.map +1 -0
- package/lib/module/navigation/base-types.js +2 -0
- package/lib/module/navigation/base-types.js.map +1 -0
- package/lib/module/navigation/createTrueSheetRouter.js +157 -0
- package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
- package/lib/module/navigation/expo-router/base-types.js +2 -0
- package/lib/module/navigation/expo-router/base-types.js.map +1 -0
- package/lib/module/navigation/expo-router/index.js +50 -0
- package/lib/module/navigation/expo-router/index.js.map +1 -0
- package/lib/module/navigation/expo-router/types.js +4 -0
- package/lib/module/navigation/expo-router/types.js.map +1 -0
- package/lib/module/navigation/index.js +47 -0
- package/lib/module/navigation/index.js.map +1 -0
- package/lib/module/navigation/navigator.js +10 -0
- package/lib/module/navigation/navigator.js.map +1 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +56 -0
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -0
- package/lib/module/navigation/screen/TrueSheetScreen.js +49 -0
- package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -0
- package/lib/module/navigation/screen/index.js +5 -0
- package/lib/module/navigation/screen/index.js.map +1 -0
- package/lib/module/navigation/screen/types.js +4 -0
- package/lib/module/navigation/screen/types.js.map +1 -0
- package/lib/module/navigation/screen/useSheetScreenState.js +69 -0
- package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -0
- package/lib/module/navigation/types.js +4 -0
- package/lib/module/navigation/types.js.map +1 -0
- package/lib/module/navigation/useTrueSheetNavigation.js +24 -0
- package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.js +15 -9
- package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js +81 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -0
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
- package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
- package/lib/module/reanimated/index.js +2 -2
- package/lib/module/reanimated/index.js.map +1 -1
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +20 -0
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -0
- package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
- package/lib/module/web/constants.js +16 -0
- package/lib/module/web/constants.js.map +1 -0
- package/lib/module/web/dom.js +10 -0
- package/lib/module/web/dom.js.map +1 -0
- package/lib/module/web/vaul/README.md +9 -0
- package/lib/module/web/vaul/browser.js +30 -0
- package/lib/module/web/vaul/browser.js.map +1 -0
- package/lib/module/web/vaul/constants.js +19 -0
- package/lib/module/web/vaul/constants.js.map +1 -0
- package/lib/module/web/vaul/context.js +59 -0
- package/lib/module/web/vaul/context.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +99 -0
- package/lib/module/web/vaul/helpers.js.map +1 -0
- package/lib/module/web/vaul/index.js +1342 -0
- package/lib/module/web/vaul/index.js.map +1 -0
- package/lib/module/web/vaul/style.css +287 -0
- package/lib/module/web/vaul/style.css.d.js +2 -0
- package/lib/module/web/vaul/style.css.d.js.map +1 -0
- package/lib/module/web/vaul/types.js +2 -0
- package/lib/module/web/vaul/types.js.map +1 -0
- package/lib/module/web/vaul/use-composed-refs.js +34 -0
- package/lib/module/web/vaul/use-composed-refs.js.map +1 -0
- package/lib/module/web/vaul/use-controllable-state.js +54 -0
- package/lib/module/web/vaul/use-controllable-state.js.map +1 -0
- package/lib/module/web/vaul/use-position-fixed.js +123 -0
- package/lib/module/web/vaul/use-position-fixed.js.map +1 -0
- package/lib/module/web/vaul/use-prevent-scroll.js +258 -0
- package/lib/module/web/vaul/use-prevent-scroll.js.map +1 -0
- package/lib/module/web/vaul/use-scale-background.js +57 -0
- package/lib/module/web/vaul/use-scale-background.js.map +1 -0
- package/lib/module/web/vaul/use-snap-points.js +305 -0
- package/lib/module/web/vaul/use-snap-points.js.map +1 -0
- package/lib/typescript/src/TrueSheet.d.ts +57 -14
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +535 -55
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +5 -0
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
- package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.d.ts +9 -0
- package/lib/typescript/src/TrueSheetPeek.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts +28 -0
- package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts +18 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +27 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +71 -16
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +47 -0
- package/lib/typescript/src/mocks/index.d.ts.map +1 -0
- package/lib/typescript/src/mocks/navigation.d.ts +70 -0
- package/lib/typescript/src/mocks/navigation.d.ts.map +1 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
- package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
- package/lib/typescript/src/mocks/reanimated.d.ts +48 -0
- package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
- package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
- package/lib/typescript/src/navigation/actions.d.ts +71 -0
- package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
- package/lib/typescript/src/navigation/base-types.d.ts +9 -0
- package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +3 -0
- package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
- package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
- package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
- package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/index.d.ts +45 -0
- package/lib/typescript/src/navigation/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/navigator.d.ts +7 -0
- package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +3 -0
- package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +3 -0
- package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/index.d.ts +4 -0
- package/lib/typescript/src/navigation/screen/index.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/types.d.ts +14 -0
- package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +34 -0
- package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -0
- package/lib/typescript/src/navigation/types.d.ts +213 -0
- package/lib/typescript/src/navigation/types.d.ts.map +1 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +23 -0
- package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +41 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
- package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +15 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +25 -3
- package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +14 -0
- package/lib/typescript/src/web/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/dom.d.ts +7 -0
- package/lib/typescript/src/web/dom.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/browser.d.ts +8 -0
- package/lib/typescript/src/web/vaul/browser.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +14 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/context.d.ts +54 -0
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts +17 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/index.d.ts +234 -0
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/types.d.ts +7 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts +14 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts +9 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts +19 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +18 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts +2 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +38 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -0
- package/package.json +104 -37
- package/react-native.config.js +10 -2
- package/src/TrueSheet.tsx +424 -70
- package/src/TrueSheet.types.ts +602 -72
- package/src/TrueSheet.web.tsx +1437 -0
- package/src/TrueSheetOverlay.tsx +24 -0
- package/src/TrueSheetOverlay.web.tsx +33 -0
- package/src/TrueSheetPeek.tsx +11 -0
- package/src/TrueSheetPeek.web.tsx +64 -0
- package/src/TrueSheetProvider.tsx +33 -0
- package/src/TrueSheetProvider.web.tsx +220 -0
- package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
- package/src/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +110 -18
- package/src/index.ts +3 -2
- package/src/mocks/index.ts +96 -0
- package/src/mocks/navigation.ts +79 -0
- package/src/mocks/navigationExpoRouter.ts +41 -0
- package/src/mocks/reanimated.ts +100 -0
- package/src/navigation/TrueSheetNavigatorContent.tsx +106 -0
- package/src/navigation/actions.ts +78 -0
- package/src/navigation/base-types.ts +20 -0
- package/src/navigation/createTrueSheetRouter.ts +194 -0
- package/src/navigation/expo-router/base-types.ts +18 -0
- package/src/navigation/expo-router/index.ts +90 -0
- package/src/navigation/expo-router/types.ts +116 -0
- package/src/navigation/index.ts +80 -0
- package/src/navigation/navigator.ts +18 -0
- package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +61 -0
- package/src/navigation/screen/TrueSheetScreen.tsx +54 -0
- package/src/navigation/screen/index.ts +3 -0
- package/src/navigation/screen/types.ts +19 -0
- package/src/navigation/screen/useSheetScreenState.ts +102 -0
- package/src/navigation/types.ts +302 -0
- package/src/navigation/useTrueSheetNavigation.ts +26 -0
- package/src/reanimated/ReanimatedTrueSheet.tsx +12 -7
- package/src/reanimated/ReanimatedTrueSheet.web.tsx +75 -0
- package/src/reanimated/ReanimatedTrueSheetProvider.tsx +12 -8
- package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +31 -0
- package/src/specs/NativeTrueSheetModule.ts +28 -3
- package/src/web/constants.ts +13 -0
- package/src/web/dom.ts +9 -0
- package/src/web/vaul/README.md +9 -0
- package/src/web/vaul/browser.ts +38 -0
- package/src/web/vaul/constants.ts +24 -0
- package/src/web/vaul/context.ts +102 -0
- package/src/web/vaul/helpers.ts +131 -0
- package/src/web/vaul/index.tsx +1785 -0
- package/src/web/vaul/style.css +287 -0
- package/src/web/vaul/style.css.d.ts +1 -0
- package/src/web/vaul/types.ts +7 -0
- package/src/web/vaul/use-composed-refs.ts +35 -0
- package/src/web/vaul/use-controllable-state.ts +66 -0
- package/src/web/vaul/use-position-fixed.ts +147 -0
- package/src/web/vaul/use-prevent-scroll.ts +309 -0
- package/src/web/vaul/use-scale-background.ts +64 -0
- package/src/web/vaul/use-snap-points.ts +455 -0
- package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -26
- package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +0 -32
- package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +0 -27
- package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
- package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -26
- package/android/src/main/res/values/styles.xml +0 -8
- package/ios/events/OnDetentChangeEvent.h +0 -28
- package/ios/events/OnDetentChangeEvent.mm +0 -30
- package/ios/events/OnDidDismissEvent.h +0 -26
- package/ios/events/OnDidDismissEvent.mm +0 -25
- package/ios/events/OnDidPresentEvent.h +0 -28
- package/ios/events/OnDidPresentEvent.mm +0 -30
- package/ios/events/OnDragBeginEvent.h +0 -28
- package/ios/events/OnDragBeginEvent.mm +0 -30
- package/ios/events/OnDragChangeEvent.h +0 -28
- package/ios/events/OnDragChangeEvent.mm +0 -30
- package/ios/events/OnDragEndEvent.h +0 -28
- package/ios/events/OnDragEndEvent.mm +0 -30
- package/ios/events/OnMountEvent.mm +0 -25
- package/ios/events/OnPositionChangeEvent.mm +0 -32
- package/ios/events/OnSizeChangeEvent.h +0 -28
- package/ios/events/OnSizeChangeEvent.mm +0 -30
- package/ios/events/OnWillDismissEvent.mm +0 -25
- package/ios/events/OnWillPresentEvent.h +0 -28
- package/ios/events/OnWillPresentEvent.mm +0 -30
- package/ios/utils/LayoutUtil.h +0 -44
- package/ios/utils/LayoutUtil.mm +0 -50
- package/lib/module/TrueSheetGrabber.js +0 -51
- package/lib/module/TrueSheetGrabber.js.map +0 -1
- package/lib/typescript/src/TrueSheetGrabber.d.ts +0 -39
- package/lib/typescript/src/TrueSheetGrabber.d.ts.map +0 -1
- package/src/TrueSheetGrabber.tsx +0 -82
- package/src/__mocks__/index.js +0 -67
|
@@ -0,0 +1,1437 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import {
|
|
3
|
+
createElement,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import type { DimensionValue, LayoutChangeEvent } from 'react-native';
|
|
14
|
+
import { StyleSheet, useColorScheme, useWindowDimensions, View } from 'react-native';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
DetentChangeEvent,
|
|
18
|
+
DetentInfoEventPayload,
|
|
19
|
+
DidBlurEvent,
|
|
20
|
+
DidDismissEvent,
|
|
21
|
+
DidFocusEvent,
|
|
22
|
+
DidPresentEvent,
|
|
23
|
+
DragBeginEvent,
|
|
24
|
+
DragChangeEvent,
|
|
25
|
+
DragEndEvent,
|
|
26
|
+
MountEvent,
|
|
27
|
+
PositionChangeEvent,
|
|
28
|
+
SheetDetent,
|
|
29
|
+
TrueSheetMethods,
|
|
30
|
+
TrueSheetProps,
|
|
31
|
+
TrueSheetStaticMethods,
|
|
32
|
+
WillBlurEvent,
|
|
33
|
+
WillDismissEvent,
|
|
34
|
+
DismissAttemptEvent,
|
|
35
|
+
WillFocusEvent,
|
|
36
|
+
WillPresentEvent,
|
|
37
|
+
} from './TrueSheet.types';
|
|
38
|
+
import { measurePeekContentHeight, TrueSheetPeekContext } from './TrueSheetPeek.web';
|
|
39
|
+
import { usePortalContainer, useRegisterSheet, useSheetStack } from './TrueSheetProvider.web';
|
|
40
|
+
import {
|
|
41
|
+
COLOR_SURFACE_CONTAINER_LOW_DARK,
|
|
42
|
+
COLOR_SURFACE_CONTAINER_LOW_LIGHT,
|
|
43
|
+
DEFAULT_ANCHOR_OFFSET,
|
|
44
|
+
DEFAULT_CORNER_RADIUS,
|
|
45
|
+
DEFAULT_DETACHED_OFFSET,
|
|
46
|
+
DEFAULT_FORM_SHEET_HEIGHT_RATIO,
|
|
47
|
+
DEFAULT_FORM_SHEET_WIDTH,
|
|
48
|
+
DEFAULT_GRABBER_COLOR_DARK,
|
|
49
|
+
DEFAULT_GRABBER_COLOR_LIGHT,
|
|
50
|
+
DEFAULT_GRABBER_HEIGHT,
|
|
51
|
+
DEFAULT_GRABBER_TOP_MARGIN,
|
|
52
|
+
DEFAULT_GRABBER_WIDTH,
|
|
53
|
+
DEFAULT_MAX_WIDTH,
|
|
54
|
+
} from './web/constants';
|
|
55
|
+
import { getDOMElement } from './web/dom';
|
|
56
|
+
import { Drawer } from './web/vaul';
|
|
57
|
+
import { DEFAULT_PEEK_HEIGHT, DRAG_CLASS, TRANSITIONS } from './web/vaul/constants';
|
|
58
|
+
|
|
59
|
+
// Resolves the user's `scrollableRef` to its scrollable DOM element — RN-web
|
|
60
|
+
// ScrollView refs expose the scroll node directly (with helpers attached);
|
|
61
|
+
// list refs (e.g. FlatList) expose it via getScrollableNode(). The node's
|
|
62
|
+
// first child is the content container.
|
|
63
|
+
const getScrollableElement = (scrollable: unknown): HTMLElement | null => {
|
|
64
|
+
if (!scrollable) return null;
|
|
65
|
+
if (scrollable instanceof HTMLElement) return scrollable;
|
|
66
|
+
const node = (scrollable as { getScrollableNode?: () => unknown }).getScrollableNode?.();
|
|
67
|
+
return node instanceof HTMLElement ? node : null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props, ref) => {
|
|
71
|
+
const {
|
|
72
|
+
children,
|
|
73
|
+
name,
|
|
74
|
+
dismissible = true,
|
|
75
|
+
draggable = true,
|
|
76
|
+
cornerRadius,
|
|
77
|
+
style,
|
|
78
|
+
backgroundColor: backgroundColorProp,
|
|
79
|
+
maxContentHeight,
|
|
80
|
+
maxContentWidth,
|
|
81
|
+
anchor = 'center',
|
|
82
|
+
anchorOffset = DEFAULT_ANCHOR_OFFSET,
|
|
83
|
+
scrollableRef,
|
|
84
|
+
scrollableOptions,
|
|
85
|
+
grabber = true,
|
|
86
|
+
grabberOptions,
|
|
87
|
+
accessibilityOptions,
|
|
88
|
+
detents = [0.5, 1],
|
|
89
|
+
dimmed = true,
|
|
90
|
+
dimmedDetentIndex = 0,
|
|
91
|
+
initialDetentIndex = -1,
|
|
92
|
+
header,
|
|
93
|
+
headerStyle,
|
|
94
|
+
headerOptions,
|
|
95
|
+
footer,
|
|
96
|
+
footerStyle,
|
|
97
|
+
footerOptions,
|
|
98
|
+
presentation = 'page',
|
|
99
|
+
detached = false,
|
|
100
|
+
detachedOffset = DEFAULT_DETACHED_OFFSET,
|
|
101
|
+
elevation = 4,
|
|
102
|
+
insetAdjustment = 'automatic',
|
|
103
|
+
initialDetentAnimated = true,
|
|
104
|
+
onPositionChange,
|
|
105
|
+
onWillPresent,
|
|
106
|
+
onDidPresent,
|
|
107
|
+
onWillDismiss,
|
|
108
|
+
onDidDismiss,
|
|
109
|
+
onDismissAttempt,
|
|
110
|
+
onDetentChange,
|
|
111
|
+
onDragBegin,
|
|
112
|
+
onDragChange,
|
|
113
|
+
onDragEnd,
|
|
114
|
+
onMount,
|
|
115
|
+
onWillFocus,
|
|
116
|
+
onDidFocus,
|
|
117
|
+
onWillBlur,
|
|
118
|
+
onDidBlur,
|
|
119
|
+
} = props;
|
|
120
|
+
|
|
121
|
+
const validDetents = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
detents.filter(
|
|
124
|
+
(d): d is SheetDetent => typeof d === 'number' || d === 'auto' || d === 'peek'
|
|
125
|
+
),
|
|
126
|
+
[detents]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const snapPointsProps = useMemo<
|
|
130
|
+
{ snapPoints: SheetDetent[]; fadeFromIndex: number } | { snapPoints?: undefined }
|
|
131
|
+
>(() => {
|
|
132
|
+
if (validDetents.length === 0) return {};
|
|
133
|
+
return {
|
|
134
|
+
snapPoints: validDetents,
|
|
135
|
+
fadeFromIndex: Math.min(dimmedDetentIndex, validDetents.length - 1),
|
|
136
|
+
};
|
|
137
|
+
}, [validDetents, dimmedDetentIndex]);
|
|
138
|
+
|
|
139
|
+
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
140
|
+
const isLandscapeOrTablet = windowWidth >= 600 || windowWidth > windowHeight;
|
|
141
|
+
const isFormSheet = isLandscapeOrTablet && presentation === 'form';
|
|
142
|
+
|
|
143
|
+
// A form sheet floats (detached) only on tablet/landscape — mirrors iOS where
|
|
144
|
+
// a form sheet is a centered card on iPad but an edge-attached bottom sheet on
|
|
145
|
+
// a compact iPhone. On mobile portrait it stays edge-attached unless `detached`
|
|
146
|
+
// is explicitly set.
|
|
147
|
+
const effectiveDetached = isFormSheet || detached;
|
|
148
|
+
|
|
149
|
+
const colorScheme = useColorScheme();
|
|
150
|
+
const backgroundColor =
|
|
151
|
+
backgroundColorProp ??
|
|
152
|
+
(colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
|
|
153
|
+
|
|
154
|
+
const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
|
|
155
|
+
const [isOpen, setIsOpen] = useState(shouldAutoPresent);
|
|
156
|
+
const [activeSnapPoint, setActiveSnapPoint] = useState<SheetDetent | null>(
|
|
157
|
+
() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Keep activeSnapPoint valid if detents change (e.g., prop updates).
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (validDetents.length === 0) return;
|
|
163
|
+
setActiveSnapPoint((current) =>
|
|
164
|
+
current != null && validDetents.includes(current) ? current : validDetents[0]!
|
|
165
|
+
);
|
|
166
|
+
}, [validDetents]);
|
|
167
|
+
|
|
168
|
+
const validDetentsRef = useRef(validDetents);
|
|
169
|
+
validDetentsRef.current = validDetents;
|
|
170
|
+
|
|
171
|
+
const hasAutoDetent = validDetents.includes('auto');
|
|
172
|
+
|
|
173
|
+
const handleSetActiveSnapPoint = useCallback((snapPoint: number | string | null) => {
|
|
174
|
+
setActiveSnapPoint(
|
|
175
|
+
snapPoint == null
|
|
176
|
+
? null
|
|
177
|
+
: typeof snapPoint === 'number' || snapPoint === 'auto' || snapPoint === 'peek'
|
|
178
|
+
? (snapPoint as SheetDetent)
|
|
179
|
+
: null
|
|
180
|
+
);
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const handleOpenChange = useCallback(
|
|
184
|
+
(open: boolean) => {
|
|
185
|
+
if (!open && isOpen) {
|
|
186
|
+
setIsOpen(false);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[isOpen]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const portalContainer = usePortalContainer();
|
|
193
|
+
|
|
194
|
+
const handlePointerDownOutside = (e: Event) => {
|
|
195
|
+
const target = e.target;
|
|
196
|
+
if (!(target instanceof Node)) return;
|
|
197
|
+
// Pointer down that landed outside this sheet's portal container (e.g.,
|
|
198
|
+
// in another screen's tree when navigating) should not close the drawer.
|
|
199
|
+
if (portalContainer && !portalContainer.contains(target)) {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!(target instanceof Element)) return;
|
|
204
|
+
const wrapper = drawerContentRef.current?.closest('[data-vaul-detached-wrapper]');
|
|
205
|
+
if (!wrapper) return;
|
|
206
|
+
// The footer is rendered via vaul's `detachedSiblings` as a sibling of
|
|
207
|
+
// Drawer.Content inside [data-vaul-detached-wrapper], so Radix treats
|
|
208
|
+
// clicks on it as "outside" the content. Don't dismiss for clicks that
|
|
209
|
+
// landed inside the wrapper.
|
|
210
|
+
if (wrapper.contains(target)) {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Every sheet runs Radix non-modal and portals its overlay + wrapper as
|
|
215
|
+
// siblings into the shared container, so a press on a sheet stacked above
|
|
216
|
+
// this one (its content or its dim overlay) is also delivered here as
|
|
217
|
+
// "outside". Sheets present in document order, so anything after our
|
|
218
|
+
// wrapper belongs to a descendant. Radix may dispatch on the deferred
|
|
219
|
+
// click (touch), after a dismissing descendant has left the DOM — treat
|
|
220
|
+
// disconnected as above too.
|
|
221
|
+
/* eslint-disable no-bitwise */
|
|
222
|
+
const above = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_DISCONNECTED;
|
|
223
|
+
if (wrapper.compareDocumentPosition(target) & above) {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
}
|
|
226
|
+
/* eslint-enable no-bitwise */
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const dismissAboveRef = useRef<(animated?: boolean) => Promise<void>>(async () => {});
|
|
230
|
+
const dismissDescendantsRef = useRef<() => void>(() => {});
|
|
231
|
+
|
|
232
|
+
const methods = useMemo<TrueSheetMethods>(
|
|
233
|
+
() => ({
|
|
234
|
+
present: async (index = 0) => {
|
|
235
|
+
const detent = validDetentsRef.current[index];
|
|
236
|
+
if (detent === undefined) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
setActiveSnapPoint(detent);
|
|
242
|
+
setIsOpen(true);
|
|
243
|
+
},
|
|
244
|
+
dismiss: async () => {
|
|
245
|
+
setIsOpen(false);
|
|
246
|
+
},
|
|
247
|
+
resize: async (index) => {
|
|
248
|
+
const detent = validDetentsRef.current[index];
|
|
249
|
+
if (detent === undefined) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`TrueSheet: resize index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
setActiveSnapPoint(detent);
|
|
255
|
+
},
|
|
256
|
+
dismissStack: async (animated) => {
|
|
257
|
+
await dismissAboveRef.current(animated);
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
[]
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
useImperativeHandle(ref, () => methods, [methods]);
|
|
264
|
+
|
|
265
|
+
const methodsRef = useRef<TrueSheetMethods | null>(methods);
|
|
266
|
+
useRegisterSheet(name, methodsRef);
|
|
267
|
+
|
|
268
|
+
const drawerContentRef = useRef<HTMLDivElement | null>(null);
|
|
269
|
+
|
|
270
|
+
// Measured header/footer heights drive the 'peek' snap point — mirrors
|
|
271
|
+
// native, where the controller tracks headerHeight/footerHeight.
|
|
272
|
+
const [headerHeight, setHeaderHeight] = useState(0);
|
|
273
|
+
const [footerHeight, setFooterHeight] = useState(0);
|
|
274
|
+
|
|
275
|
+
const handleHeaderLayout = useCallback((e: LayoutChangeEvent) => {
|
|
276
|
+
setHeaderHeight(e.nativeEvent.layout.height);
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
const handleFooterLayout = useCallback((e: LayoutChangeEvent) => {
|
|
280
|
+
setFooterHeight(e.nativeEvent.layout.height);
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
// DOM refs for live 'peek' measurement in computeDetentGeometry — the
|
|
284
|
+
// state above (which drives vaul's `peekHeight` prop) comes from RN-web
|
|
285
|
+
// `onLayout`, which dispatches a frame after mount: too late for the first
|
|
286
|
+
// willPresent.
|
|
287
|
+
const headerElRef = useRef<View>(null);
|
|
288
|
+
const footerElRef = useRef<View>(null);
|
|
289
|
+
|
|
290
|
+
// Distance from the content top to the bottom of a `TrueSheetPeek` rendered
|
|
291
|
+
// within the content — measured by the peek against `contentRef`.
|
|
292
|
+
const [peekContentHeight, setPeekContentHeight] = useState(0);
|
|
293
|
+
const contentRef = useRef<View>(null);
|
|
294
|
+
const peekElRef = useRef<View>(null);
|
|
295
|
+
const peekContext = useMemo(() => ({ contentRef, peekRef: peekElRef, setPeekContentHeight }), []);
|
|
296
|
+
|
|
297
|
+
// The peek's own onLayout only fires when *it* resizes. Content added above
|
|
298
|
+
// it moves it without resizing it, so re-measure its offset whenever the
|
|
299
|
+
// content view's layout changes.
|
|
300
|
+
const handleContentLayout = useCallback(() => {
|
|
301
|
+
const peekEl = getDOMElement(peekElRef.current);
|
|
302
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
303
|
+
if (peekEl && contentEl) {
|
|
304
|
+
setPeekContentHeight(measurePeekContentHeight(peekEl, contentEl));
|
|
305
|
+
}
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
// An absolute (floating) header overlaps the content, so it contributes no
|
|
309
|
+
// height to the 'auto' detent measurement.
|
|
310
|
+
const absoluteHeader = headerOptions?.position === 'absolute';
|
|
311
|
+
const absoluteHeaderRef = useRef(absoluteHeader);
|
|
312
|
+
absoluteHeaderRef.current = absoluteHeader;
|
|
313
|
+
|
|
314
|
+
const resolvedHeaderStyle = absoluteHeader ? [absoluteHeaderStyle, headerStyle] : headerStyle;
|
|
315
|
+
|
|
316
|
+
// Same for an absolute (floating) footer — rendered via vaul's
|
|
317
|
+
// `detachedSiblings` instead of in the content flow.
|
|
318
|
+
const absoluteFooter = footerOptions?.position === 'absolute';
|
|
319
|
+
const absoluteFooterRef = useRef(absoluteFooter);
|
|
320
|
+
absoluteFooterRef.current = absoluteFooter;
|
|
321
|
+
|
|
322
|
+
// The footer owns the sheet's bottom edge, so it absorbs the bottom
|
|
323
|
+
// safe-area inset as padding (on top of its own) — mirrors native, where the
|
|
324
|
+
// footer shadow node pads the inset so its background fills it.
|
|
325
|
+
const footerOwnsInset = Boolean(footer) && insetAdjustment === 'automatic';
|
|
326
|
+
const resolvedFooterStyle = useMemo(() => {
|
|
327
|
+
if (!footerOwnsInset) return footerStyle;
|
|
328
|
+
|
|
329
|
+
const flat = StyleSheet.flatten(footerStyle);
|
|
330
|
+
const base = flat?.paddingBottom ?? flat?.paddingVertical ?? flat?.padding;
|
|
331
|
+
const baseCss =
|
|
332
|
+
typeof base === 'number' ? `${base}px` : typeof base === 'string' ? base : '0px';
|
|
333
|
+
return [
|
|
334
|
+
footerStyle,
|
|
335
|
+
{
|
|
336
|
+
// RN types don't model CSS calc(), but RN-web passes it through
|
|
337
|
+
paddingBottom: `calc(${baseCss} + env(safe-area-inset-bottom, 0px))` as DimensionValue,
|
|
338
|
+
},
|
|
339
|
+
];
|
|
340
|
+
}, [footerOwnsInset, footerStyle]);
|
|
341
|
+
|
|
342
|
+
// A relative footer is laid out below the content, so it's pushed off-screen
|
|
343
|
+
// at the peek detent and contributes no height.
|
|
344
|
+
const peekHeight =
|
|
345
|
+
(header ? headerHeight : 0) +
|
|
346
|
+
(footer && absoluteFooter ? footerHeight : 0) +
|
|
347
|
+
peekContentHeight || DEFAULT_PEEK_HEIGHT;
|
|
348
|
+
|
|
349
|
+
// Web mirror of native's scrollable handling for 'auto' detents: a plugged
|
|
350
|
+
// scrollable keeps the sized (bounded) layout so its viewport is capped to
|
|
351
|
+
// the visible sheet and can scroll, while the 'auto' height is measured with
|
|
352
|
+
// the viewport replaced by the scrollable's content size — mirrors native
|
|
353
|
+
// `naturalHeight`.
|
|
354
|
+
const [hasBoundedScrollable, setHasBoundedScrollable] = useState(false);
|
|
355
|
+
const [scrollableAutoHeight, setScrollableAutoHeight] = useState(0);
|
|
356
|
+
const detectedScrollerRef = useRef<HTMLElement | null>(null);
|
|
357
|
+
|
|
358
|
+
// Natural content height (header + content + footer, with a detected
|
|
359
|
+
// scrollable's viewport replaced by its content size) — the height the
|
|
360
|
+
// content wants regardless of the sheet's bounds.
|
|
361
|
+
const measureNaturalHeight = useCallback(() => {
|
|
362
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
363
|
+
if (!contentEl || !contentEl.isConnected) return 0;
|
|
364
|
+
const headerEl = absoluteHeaderRef.current ? null : getDOMElement(headerElRef.current);
|
|
365
|
+
const footerEl = absoluteFooterRef.current ? null : getDOMElement(footerElRef.current);
|
|
366
|
+
let height =
|
|
367
|
+
(headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + contentEl.offsetHeight;
|
|
368
|
+
const scroller = detectedScrollerRef.current;
|
|
369
|
+
const scrollContent = scroller?.firstElementChild;
|
|
370
|
+
if (scroller?.isConnected && scrollContent instanceof HTMLElement) {
|
|
371
|
+
height += scrollContent.offsetHeight - scroller.clientHeight;
|
|
372
|
+
}
|
|
373
|
+
return Math.max(0, height);
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
if (!isOpen || !hasAutoDetent) return undefined;
|
|
378
|
+
|
|
379
|
+
let canceled = false;
|
|
380
|
+
let rafId = 0;
|
|
381
|
+
let mutationObserver: MutationObserver | null = null;
|
|
382
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
383
|
+
|
|
384
|
+
const attach = () => {
|
|
385
|
+
if (canceled) return;
|
|
386
|
+
const drawerEl = drawerContentRef.current;
|
|
387
|
+
if (!drawerEl || !drawerEl.isConnected) {
|
|
388
|
+
// Radix Presence defers the portal mount; poll until the drawer is live.
|
|
389
|
+
rafId = window.requestAnimationFrame(attach);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const measure = () => setScrollableAutoHeight(measureNaturalHeight());
|
|
394
|
+
|
|
395
|
+
let observed: {
|
|
396
|
+
contentEl: HTMLElement | null;
|
|
397
|
+
headerEl: HTMLElement | null;
|
|
398
|
+
scroller: HTMLElement;
|
|
399
|
+
scrollContent: Element | null;
|
|
400
|
+
} | null = null;
|
|
401
|
+
|
|
402
|
+
// (Re)resolve the plugged scrollable and observe the nodes whose size
|
|
403
|
+
// feeds the natural height — content growth inside a bounded scroller is
|
|
404
|
+
// invisible to the sheet's own layout (mirrors native's contentSize
|
|
405
|
+
// observation). Resolves the content node live: the layout-branch swap
|
|
406
|
+
// (natural flow ↔ sized) remounts it.
|
|
407
|
+
const observe = () => {
|
|
408
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
409
|
+
const headerEl = getDOMElement(headerElRef.current);
|
|
410
|
+
|
|
411
|
+
// Skip mutations that don't change the observed topology — e.g. a
|
|
412
|
+
// virtualized list mounting rows inside the plugged scroller. Size
|
|
413
|
+
// changes are already covered by the ResizeObserver.
|
|
414
|
+
if (
|
|
415
|
+
observed &&
|
|
416
|
+
observed.contentEl === contentEl &&
|
|
417
|
+
contentEl?.isConnected &&
|
|
418
|
+
observed.headerEl === headerEl &&
|
|
419
|
+
observed.scroller.isConnected &&
|
|
420
|
+
observed.scrollContent === observed.scroller.firstElementChild
|
|
421
|
+
) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const resolved =
|
|
426
|
+
contentEl && contentEl.isConnected ? getScrollableElement(scrollableRef?.current) : null;
|
|
427
|
+
const scroller = resolved?.isConnected ? resolved : null;
|
|
428
|
+
detectedScrollerRef.current = scroller;
|
|
429
|
+
setHasBoundedScrollable(scroller != null);
|
|
430
|
+
observed = scroller
|
|
431
|
+
? { contentEl, headerEl, scroller, scrollContent: scroller.firstElementChild }
|
|
432
|
+
: null;
|
|
433
|
+
|
|
434
|
+
resizeObserver?.disconnect();
|
|
435
|
+
resizeObserver = new ResizeObserver(measure);
|
|
436
|
+
if (contentEl?.isConnected) resizeObserver.observe(contentEl);
|
|
437
|
+
if (headerEl) resizeObserver.observe(headerEl);
|
|
438
|
+
if (scroller) {
|
|
439
|
+
resizeObserver.observe(scroller);
|
|
440
|
+
if (scroller.firstElementChild) resizeObserver.observe(scroller.firstElementChild);
|
|
441
|
+
}
|
|
442
|
+
measure();
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
observe();
|
|
446
|
+
// Watch the whole drawer subtree — catches the scrollable mounting/
|
|
447
|
+
// unmounting AND the layout-branch swap, which remounts the content node
|
|
448
|
+
// (a content-scoped observer would go stale after the swap).
|
|
449
|
+
mutationObserver = new MutationObserver(observe);
|
|
450
|
+
mutationObserver.observe(drawerEl, { childList: true, subtree: true });
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
rafId = window.requestAnimationFrame(attach);
|
|
454
|
+
|
|
455
|
+
return () => {
|
|
456
|
+
canceled = true;
|
|
457
|
+
window.cancelAnimationFrame(rafId);
|
|
458
|
+
mutationObserver?.disconnect();
|
|
459
|
+
resizeObserver?.disconnect();
|
|
460
|
+
};
|
|
461
|
+
}, [isOpen, hasAutoDetent, measureNaturalHeight, scrollableRef]);
|
|
462
|
+
|
|
463
|
+
// Below the last detent a vertical touch pan moves the sheet, not the
|
|
464
|
+
// content — `[data-vaul-scroll-locked]` disables vertical touch panning on
|
|
465
|
+
// the scroll container and everything inside it (see vaul/style.css).
|
|
466
|
+
const isScrollLocked =
|
|
467
|
+
validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
|
|
468
|
+
|
|
469
|
+
// Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
|
|
470
|
+
// Track it here so the form sheet can size its card to fit content,
|
|
471
|
+
// clamped between a minimum ratio of the viewport and a maximum derived
|
|
472
|
+
// from `detachedOffset` (the breathing room left at top + bottom of the
|
|
473
|
+
// floating card).
|
|
474
|
+
const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
|
|
475
|
+
|
|
476
|
+
const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
|
|
477
|
+
if (maxContentHeight !== undefined) return maxContentHeight;
|
|
478
|
+
if (!isFormSheet) return undefined;
|
|
479
|
+
const min = windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO;
|
|
480
|
+
const max = Math.max(min, windowHeight - 2 * detachedOffset);
|
|
481
|
+
if (measuredContentHeight <= 0) return min;
|
|
482
|
+
return Math.max(min, Math.min(measuredContentHeight, max));
|
|
483
|
+
}, [maxContentHeight, isFormSheet, windowHeight, detachedOffset, measuredContentHeight]);
|
|
484
|
+
|
|
485
|
+
// Center the form sheet using the actual visible drawer height. Vaul
|
|
486
|
+
// auto-sizes to content (capped by `maxContentHeight`), so when content is
|
|
487
|
+
// shorter than `effectiveMaxContentHeight`'s min-clamped floor, using that
|
|
488
|
+
// for the offset would push a small sheet below the viewport center.
|
|
489
|
+
const effectiveDetachedOffset = useMemo(() => {
|
|
490
|
+
if (!isFormSheet) return detachedOffset;
|
|
491
|
+
const max = Math.max(0, windowHeight - 2 * detachedOffset);
|
|
492
|
+
const visibleHeight =
|
|
493
|
+
measuredContentHeight > 0
|
|
494
|
+
? Math.min(measuredContentHeight, max)
|
|
495
|
+
: (effectiveMaxContentHeight ?? 0);
|
|
496
|
+
return Math.max(0, (windowHeight - visibleHeight) / 2);
|
|
497
|
+
}, [isFormSheet, windowHeight, detachedOffset, measuredContentHeight, effectiveMaxContentHeight]);
|
|
498
|
+
|
|
499
|
+
// Present/dismiss events. The sheet settles via a CSS `transform` transition
|
|
500
|
+
// on either the drawer (snap-points on autopresent) or the wrapper (whole-
|
|
501
|
+
// card slide on reopen/dismiss). `Animation.finished` from the Web Animations
|
|
502
|
+
// API tracks whichever is actually running — reflects what the browser is
|
|
503
|
+
// doing, doesn't miss when no transition runs (same-value change), and
|
|
504
|
+
// handles interruptions correctly (a drag/resnap mid-present resolves only
|
|
505
|
+
// once all transform animations drain).
|
|
506
|
+
const onWillPresentRef = useRef(onWillPresent);
|
|
507
|
+
const onDidPresentRef = useRef(onDidPresent);
|
|
508
|
+
const onWillDismissRef = useRef(onWillDismiss);
|
|
509
|
+
const onDidDismissRef = useRef(onDidDismiss);
|
|
510
|
+
const onDetentChangeRef = useRef(onDetentChange);
|
|
511
|
+
const onDragBeginRef = useRef(onDragBegin);
|
|
512
|
+
const onDragChangeRef = useRef(onDragChange);
|
|
513
|
+
const onDragEndRef = useRef(onDragEnd);
|
|
514
|
+
const onPositionChangeRef = useRef(onPositionChange);
|
|
515
|
+
const activeSnapPointRef = useRef(activeSnapPoint);
|
|
516
|
+
useEffect(() => {
|
|
517
|
+
onWillPresentRef.current = onWillPresent;
|
|
518
|
+
onDidPresentRef.current = onDidPresent;
|
|
519
|
+
onWillDismissRef.current = onWillDismiss;
|
|
520
|
+
onDidDismissRef.current = onDidDismiss;
|
|
521
|
+
onDetentChangeRef.current = onDetentChange;
|
|
522
|
+
onDragBeginRef.current = onDragBegin;
|
|
523
|
+
onDragChangeRef.current = onDragChange;
|
|
524
|
+
onDragEndRef.current = onDragEnd;
|
|
525
|
+
onPositionChangeRef.current = onPositionChange;
|
|
526
|
+
activeSnapPointRef.current = activeSnapPoint;
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Detent geometry — target top-Y (`positions`) and height ratio (`values`)
|
|
530
|
+
// per detent. Mirrors vaul's snap-offset math exactly (same effective
|
|
531
|
+
// height, ceiling, and 'auto'/'peek' resolution) so computed targets match
|
|
532
|
+
// where the drawer actually settles. Numeric detent d → top-Y =
|
|
533
|
+
// (1 - d) * effectiveH. 'auto' and 'peek' are measured live from the DOM
|
|
534
|
+
// (see below). Inputs live in a render-synced ref so the compute callbacks
|
|
535
|
+
// stay referentially stable for the event effects.
|
|
536
|
+
const geometryInputsRef = useRef({
|
|
537
|
+
effectiveDetached,
|
|
538
|
+
effectiveDetachedOffset,
|
|
539
|
+
effectiveMaxContentHeight,
|
|
540
|
+
peekHeight,
|
|
541
|
+
peekContentHeight,
|
|
542
|
+
measuredContentHeight,
|
|
543
|
+
});
|
|
544
|
+
geometryInputsRef.current = {
|
|
545
|
+
effectiveDetached,
|
|
546
|
+
effectiveDetachedOffset,
|
|
547
|
+
effectiveMaxContentHeight,
|
|
548
|
+
peekHeight,
|
|
549
|
+
peekContentHeight,
|
|
550
|
+
measuredContentHeight,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const computeDetentGeometry = useCallback(() => {
|
|
554
|
+
const inputs = geometryInputsRef.current;
|
|
555
|
+
const windowH = window.innerHeight;
|
|
556
|
+
const effectiveH = inputs.effectiveDetached
|
|
557
|
+
? windowH - inputs.effectiveDetachedOffset
|
|
558
|
+
: windowH;
|
|
559
|
+
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
560
|
+
const ceiling =
|
|
561
|
+
inputs.effectiveMaxContentHeight !== undefined
|
|
562
|
+
? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
|
|
563
|
+
: effectiveH;
|
|
564
|
+
// 'auto' resolves to the content's natural height. Read it live — the
|
|
565
|
+
// `measuredContentHeight` state lags mount by a frame (vaul's initial
|
|
566
|
+
// ref-callback measure runs while the portal subtree is still detached,
|
|
567
|
+
// so it reads 0 until the ResizeObserver fires). Falls back to the state
|
|
568
|
+
// value, then to vaul's pre-measure fallback (effectiveHeight / 2).
|
|
569
|
+
const measuredHeight = measureNaturalHeight() || inputs.measuredContentHeight;
|
|
570
|
+
const autoHeight = Math.min(measuredHeight > 0 ? measuredHeight : effectiveH / 2, ceiling);
|
|
571
|
+
|
|
572
|
+
// 'peek' is measured live from the DOM (offsetHeight is layout-based, so
|
|
573
|
+
// in-flight transforms don't skew it): the state-driven `peekHeight` prop
|
|
574
|
+
// comes from RN-web onLayout, which dispatches a frame after mount — too
|
|
575
|
+
// late for the first willPresent on autopresent. Ref callbacks can't
|
|
576
|
+
// measure either: they fire while the portal subtree is still detached.
|
|
577
|
+
// Geometry only runs while the drawer is mounted, so the elements are
|
|
578
|
+
// measurable here; fall back to the state value when they're absent.
|
|
579
|
+
const headerEl = getDOMElement(headerElRef.current);
|
|
580
|
+
// A relative footer is pushed off-screen at the peek detent — excluded
|
|
581
|
+
const footerEl = absoluteFooterRef.current ? getDOMElement(footerElRef.current) : null;
|
|
582
|
+
const peekEl = getDOMElement(peekElRef.current);
|
|
583
|
+
const contentEl = getDOMElement(contentRef.current);
|
|
584
|
+
const livePeekContentHeight =
|
|
585
|
+
peekEl && contentEl ? measurePeekContentHeight(peekEl, contentEl) : inputs.peekContentHeight;
|
|
586
|
+
const livePeekHeight =
|
|
587
|
+
headerEl || footerEl || peekEl
|
|
588
|
+
? (headerEl?.offsetHeight ?? 0) + (footerEl?.offsetHeight ?? 0) + livePeekContentHeight ||
|
|
589
|
+
DEFAULT_PEEK_HEIGHT
|
|
590
|
+
: inputs.peekHeight;
|
|
591
|
+
|
|
592
|
+
const positions: number[] = [];
|
|
593
|
+
const values: number[] = [];
|
|
594
|
+
for (const d of validDetentsRef.current) {
|
|
595
|
+
const h =
|
|
596
|
+
typeof d === 'number'
|
|
597
|
+
? Math.min(d * effectiveH, ceiling)
|
|
598
|
+
: d === 'peek'
|
|
599
|
+
? Math.min(livePeekHeight, ceiling)
|
|
600
|
+
: autoHeight;
|
|
601
|
+
positions.push(effectiveH - h);
|
|
602
|
+
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
603
|
+
}
|
|
604
|
+
return { windowH, effectiveH, ceiling, positions, values };
|
|
605
|
+
}, [measureNaturalHeight]);
|
|
606
|
+
|
|
607
|
+
// Detent info for lifecycle events. Position/detent come from the active
|
|
608
|
+
// detent's target geometry — not the live DOM rect — so willPresent (drawer
|
|
609
|
+
// not mounted yet), detentChange (animation just started), and didPresent
|
|
610
|
+
// all emit the settled detent position, matching iOS/Android. Drag events
|
|
611
|
+
// pass `live: true` to report the in-flight rect position instead, also
|
|
612
|
+
// matching native.
|
|
613
|
+
const computeDetentInfo = useCallback(
|
|
614
|
+
(live = false): DetentInfoEventPayload => {
|
|
615
|
+
const snap = activeSnapPointRef.current;
|
|
616
|
+
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
617
|
+
const { windowH, positions, values } = computeDetentGeometry();
|
|
618
|
+
const target = index >= 0 ? positions[index] : undefined;
|
|
619
|
+
const position = live
|
|
620
|
+
? (drawerContentRef.current?.getBoundingClientRect().top ?? target ?? windowH)
|
|
621
|
+
: (target ?? windowH);
|
|
622
|
+
return { index, position, detent: index >= 0 ? (values[index] ?? 0) : 0 };
|
|
623
|
+
},
|
|
624
|
+
[computeDetentGeometry]
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// Mirror Android: interpolate fractional index and detent from the drawer's
|
|
628
|
+
// top-Y so continuous position updates (drag, animation) carry smooth values
|
|
629
|
+
// between detent boundaries.
|
|
630
|
+
const interpolateFromPosition = useCallback(
|
|
631
|
+
(position: number): { index: number; detent: number } => {
|
|
632
|
+
const { windowH, positions, values } = computeDetentGeometry();
|
|
633
|
+
const count = positions.length;
|
|
634
|
+
if (count === 0) return { index: -1, detent: 0 };
|
|
635
|
+
|
|
636
|
+
// Absorb subpixel drift from getBoundingClientRect so at-rest positions
|
|
637
|
+
// don't sneak into the below-first branch and emit near-zero negatives
|
|
638
|
+
// like `-1e-8` (which render as "-1" via JS scientific-notation toString).
|
|
639
|
+
const epsilon = 0.5;
|
|
640
|
+
const firstPos = positions[0]!;
|
|
641
|
+
const lastPos = positions[count - 1]!;
|
|
642
|
+
|
|
643
|
+
if (position > firstPos + epsilon) {
|
|
644
|
+
// Two ranges: index spans the full animation (windowH of wrapper
|
|
645
|
+
// travel) so it's smooth for driving dependent animations end-to-end;
|
|
646
|
+
// detent tracks the sheet's visible-height ratio (windowH - firstPos)
|
|
647
|
+
// so its 0–values[0] fade has fine resolution while the sheet is still
|
|
648
|
+
// in view. Both clamp to keep outputs in [-1, 0].
|
|
649
|
+
const indexRaw = (position - firstPos) / windowH;
|
|
650
|
+
const detentRaw = (position - firstPos) / Math.max(1, windowH - firstPos);
|
|
651
|
+
const indexProgress = Math.max(0, Math.min(1, indexRaw));
|
|
652
|
+
const detentProgress = Math.max(0, Math.min(1, detentRaw));
|
|
653
|
+
return {
|
|
654
|
+
index: -indexProgress,
|
|
655
|
+
detent: Math.max(0, values[0]! * (1 - detentProgress)),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (count === 1) return { index: 0, detent: values[0]! };
|
|
660
|
+
|
|
661
|
+
// Clamp into the segment range so subpixel drift at the boundaries
|
|
662
|
+
// resolves cleanly to the nearest segment edge (index 0 or count-1).
|
|
663
|
+
const clamped = Math.max(lastPos, Math.min(firstPos, position));
|
|
664
|
+
|
|
665
|
+
for (let i = 0; i < count - 1; i++) {
|
|
666
|
+
const pos = positions[i]!;
|
|
667
|
+
const nextPos = positions[i + 1]!;
|
|
668
|
+
if (clamped >= nextPos && clamped <= pos) {
|
|
669
|
+
const range = pos - nextPos;
|
|
670
|
+
const progress = range > 0 ? (pos - clamped) / range : 0;
|
|
671
|
+
const clampedProgress = Math.max(0, Math.min(1, progress));
|
|
672
|
+
return {
|
|
673
|
+
index: i + clampedProgress,
|
|
674
|
+
detent: values[i]! + clampedProgress * (values[i + 1]! - values[i]!),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return { index: count - 1, detent: values[count - 1]! };
|
|
680
|
+
},
|
|
681
|
+
[computeDetentGeometry]
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// Mirror native synchronous layout: while dragging (and through the post-
|
|
685
|
+
// release settle) the sized layout tracks the sheet's visible height in
|
|
686
|
+
// realtime, clamped to the smallest detent — dragging below it slides the
|
|
687
|
+
// sheet out without resizing. Once settled, the inline height is cleared so
|
|
688
|
+
// the CSS calc() owns sizing again (adapts to viewport resizes at rest).
|
|
689
|
+
const sizedLayoutRef = useRef<HTMLDivElement>(null);
|
|
690
|
+
const isDraggingRef = useRef(false);
|
|
691
|
+
const isSettlingAfterDragRef = useRef(false);
|
|
692
|
+
const updateSizedLayoutHeight = useCallback(
|
|
693
|
+
(position: number) => {
|
|
694
|
+
const node = sizedLayoutRef.current;
|
|
695
|
+
if (!node) return;
|
|
696
|
+
|
|
697
|
+
const { effectiveH, ceiling, positions } = computeDetentGeometry();
|
|
698
|
+
if (positions.length === 0) return;
|
|
699
|
+
|
|
700
|
+
if (isSettlingAfterDragRef.current) {
|
|
701
|
+
const snap = activeSnapPointRef.current;
|
|
702
|
+
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
703
|
+
const target = index >= 0 ? positions[index]! : null;
|
|
704
|
+
if (target != null && Math.abs(position - target) < 1) {
|
|
705
|
+
isSettlingAfterDragRef.current = false;
|
|
706
|
+
// Restore the calc — height is an inline style, so clearing it would
|
|
707
|
+
// leave the div unsized (React won't re-apply it without a render)
|
|
708
|
+
node.style.height = SIZED_LAYOUT_HEIGHT;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// positions[0] is the smallest detent's top-Y — its height is the clamp floor.
|
|
714
|
+
const minHeight = effectiveH - positions[0]!;
|
|
715
|
+
const height = Math.min(Math.max(effectiveH - position, minHeight), ceiling);
|
|
716
|
+
node.style.height = `${height}px`;
|
|
717
|
+
},
|
|
718
|
+
[computeDetentGeometry]
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
const handlePositionChange = useCallback(
|
|
722
|
+
(position: number) => {
|
|
723
|
+
if (isDraggingRef.current || isSettlingAfterDragRef.current) {
|
|
724
|
+
updateSizedLayoutHeight(position);
|
|
725
|
+
}
|
|
726
|
+
const { index, detent } = interpolateFromPosition(position);
|
|
727
|
+
onPositionChangeRef.current?.({
|
|
728
|
+
nativeEvent: { index, position, detent, realtime: true },
|
|
729
|
+
} as PositionChangeEvent);
|
|
730
|
+
},
|
|
731
|
+
[interpolateFromPosition, updateSizedLayoutHeight]
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
// Fire onMount once after first render. React-mount is the earliest point
|
|
735
|
+
// the component is ready for imperative calls, matching the native
|
|
736
|
+
// "ready for present" contract. Declared before the present/dismiss effect
|
|
737
|
+
// so it fires first during autopresent (onMount → onWillPresent).
|
|
738
|
+
const onMountRef = useRef(onMount);
|
|
739
|
+
useEffect(() => {
|
|
740
|
+
onMountRef.current = onMount;
|
|
741
|
+
});
|
|
742
|
+
useEffect(() => {
|
|
743
|
+
onMountRef.current?.({ nativeEvent: null } as MountEvent);
|
|
744
|
+
}, []);
|
|
745
|
+
|
|
746
|
+
// Start at `false` so a mount with `isOpen=true` (autopresent via
|
|
747
|
+
// `initialDetentIndex`) is detected as a false→true transition and fires
|
|
748
|
+
// `onWillPresent`.
|
|
749
|
+
const wasOpenRef = useRef(false);
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const wasOpen = wasOpenRef.current;
|
|
752
|
+
wasOpenRef.current = isOpen;
|
|
753
|
+
|
|
754
|
+
if (isOpen === wasOpen) return undefined;
|
|
755
|
+
|
|
756
|
+
const present = isOpen;
|
|
757
|
+
if (!present) {
|
|
758
|
+
// Mirror native: dismissing a sheet takes every sheet stacked above it
|
|
759
|
+
// (iOS dismisses from the presenter; Android runs dismissStack first).
|
|
760
|
+
dismissDescendantsRef.current();
|
|
761
|
+
// Pair willBlur with willDismiss on dismiss — mirrors native iOS
|
|
762
|
+
// emitWillDismissEvents (blur fires before dismiss). willPresent is
|
|
763
|
+
// deferred to `start()` below (needs the mounted drawer's geometry).
|
|
764
|
+
onWillBlurRef.current?.({ nativeEvent: null } as WillBlurEvent);
|
|
765
|
+
onWillDismissRef.current?.({ nativeEvent: null } as WillDismissEvent);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const fireDone = () => {
|
|
769
|
+
if (present) {
|
|
770
|
+
onDidPresentRef.current?.({ nativeEvent: computeDetentInfo() } as DidPresentEvent);
|
|
771
|
+
onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
|
|
772
|
+
} else {
|
|
773
|
+
onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
|
|
774
|
+
onDidDismissRef.current?.({ nativeEvent: null } as DidDismissEvent);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
let canceled = false;
|
|
779
|
+
let rafId = 0;
|
|
780
|
+
|
|
781
|
+
const start = () => {
|
|
782
|
+
if (canceled) return;
|
|
783
|
+
const drawer = drawerContentRef.current;
|
|
784
|
+
if (!drawer || !drawer.isConnected) {
|
|
785
|
+
// Drawer hasn't mounted yet (Radix Presence defers the portal mount
|
|
786
|
+
// past the first effect pass), or its subtree isn't attached to the
|
|
787
|
+
// document yet (autopresent commits the portal before the app tree
|
|
788
|
+
// attaches, so nothing is measurable). Poll until it's live.
|
|
789
|
+
rafId = window.requestAnimationFrame(start);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (present) {
|
|
793
|
+
// Emit will-events only once the drawer is mounted and attached —
|
|
794
|
+
// detent geometry ('auto' height, 'peek', form-sheet sizing) is
|
|
795
|
+
// measurable from the DOM here; a synchronous emission would use the
|
|
796
|
+
// pre-measure fallbacks on first present. Pairing willFocus with
|
|
797
|
+
// willPresent mirrors native iOS where viewWillAppear dispatches
|
|
798
|
+
// both; the descendant-stack focus effect handles subsequent
|
|
799
|
+
// transitions.
|
|
800
|
+
onWillPresentRef.current?.({ nativeEvent: computeDetentInfo() } as WillPresentEvent);
|
|
801
|
+
onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
|
|
802
|
+
}
|
|
803
|
+
const wrapper = drawer.closest<HTMLElement>('[data-vaul-detached-wrapper]') ?? null;
|
|
804
|
+
const targets = wrapper ? [drawer, wrapper] : [drawer];
|
|
805
|
+
|
|
806
|
+
const waitForSettle = (): void => {
|
|
807
|
+
if (canceled) return;
|
|
808
|
+
// Force style recalc so transitions queued by vaul's effects this
|
|
809
|
+
// commit are registered in `getAnimations()`. RAF callbacks run BEFORE
|
|
810
|
+
// the frame's style recalc, and ignoring this returns a stale empty
|
|
811
|
+
// list — we'd fire `did` immediately with nothing queued.
|
|
812
|
+
|
|
813
|
+
drawer.offsetHeight;
|
|
814
|
+
const pending = targets.flatMap((el) =>
|
|
815
|
+
el.getAnimations().filter((a) => a.playState !== 'finished')
|
|
816
|
+
);
|
|
817
|
+
if (pending.length === 0) {
|
|
818
|
+
fireDone();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// allSettled: resolve even when a transition is canceled (drag /
|
|
822
|
+
// resnap), then re-check — a replacement transition may have started.
|
|
823
|
+
Promise.allSettled(pending.map((a) => a.finished)).then(() => {
|
|
824
|
+
if (!canceled) waitForSettle();
|
|
825
|
+
});
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
waitForSettle();
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
rafId = window.requestAnimationFrame(start);
|
|
832
|
+
|
|
833
|
+
return () => {
|
|
834
|
+
canceled = true;
|
|
835
|
+
window.cancelAnimationFrame(rafId);
|
|
836
|
+
};
|
|
837
|
+
}, [isOpen, computeDetentInfo]);
|
|
838
|
+
|
|
839
|
+
// Fire onDetentChange only while open→open. Present/dismiss have their own
|
|
840
|
+
// events and carry detent info via onDidPresent, so we skip those edges.
|
|
841
|
+
const detentChangeStateRef = useRef({ isOpen, activeSnapPoint });
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
const prev = detentChangeStateRef.current;
|
|
844
|
+
detentChangeStateRef.current = { isOpen, activeSnapPoint };
|
|
845
|
+
if (!prev.isOpen || !isOpen) return;
|
|
846
|
+
if (prev.activeSnapPoint === activeSnapPoint) return;
|
|
847
|
+
onDetentChangeRef.current?.({ nativeEvent: computeDetentInfo() } as DetentChangeEvent);
|
|
848
|
+
}, [isOpen, activeSnapPoint, computeDetentInfo]);
|
|
849
|
+
|
|
850
|
+
// Vaul's `onDrag` fires once per pointermove while dragging; the first tick
|
|
851
|
+
// after an idle gap marks the drag boundary, so track it via a ref.
|
|
852
|
+
const handleDrag = useCallback(() => {
|
|
853
|
+
if (!isDraggingRef.current) {
|
|
854
|
+
isDraggingRef.current = true;
|
|
855
|
+
onDragBeginRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragBeginEvent);
|
|
856
|
+
}
|
|
857
|
+
onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
|
|
858
|
+
}, [computeDetentInfo]);
|
|
859
|
+
const handleRelease = useCallback(
|
|
860
|
+
(_event: unknown, open: boolean) => {
|
|
861
|
+
if (!isDraggingRef.current) return;
|
|
862
|
+
isDraggingRef.current = false;
|
|
863
|
+
// Track the sized layout through the settle animation only when the sheet
|
|
864
|
+
// stays open — a dismissal slides out at the min-clamped height.
|
|
865
|
+
isSettlingAfterDragRef.current = open;
|
|
866
|
+
onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
|
|
867
|
+
},
|
|
868
|
+
[computeDetentInfo]
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const { isNested, dismissAbove, descendants } = useSheetStack(
|
|
872
|
+
methodsRef,
|
|
873
|
+
drawerContentRef,
|
|
874
|
+
isOpen,
|
|
875
|
+
isFormSheet
|
|
876
|
+
);
|
|
877
|
+
dismissAboveRef.current = dismissAbove;
|
|
878
|
+
// Captured per render: by the time the dismiss effect runs, this sheet has
|
|
879
|
+
// already been popped from the stack, so `dismissAbove` can't find it.
|
|
880
|
+
dismissDescendantsRef.current = () => {
|
|
881
|
+
for (let i = descendants.length - 1; i >= 0; i--) {
|
|
882
|
+
descendants[i]!.ref.current?.dismiss();
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
887
|
+
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
888
|
+
// re-runs whenever the stack (and thus its descendants) changes.
|
|
889
|
+
useEffect(() => {
|
|
890
|
+
const parent = drawerContentRef.current;
|
|
891
|
+
if (!parent) return;
|
|
892
|
+
// Skip while dismissing: this sheet's stack pop changes `descendants`,
|
|
893
|
+
// which would re-fire the effect and write `wrapper.style.transition =
|
|
894
|
+
// 'clip-path …'`, clobbering vaul's just-written `'transform …'` for the
|
|
895
|
+
// dismiss animation. Vaul fully owns this sheet's transitions on the way
|
|
896
|
+
// out — nothing to align with anymore.
|
|
897
|
+
if (!isOpen) return;
|
|
898
|
+
const parentWrapper = parent.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
899
|
+
|
|
900
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
901
|
+
const wrapperTransition = `clip-path ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
902
|
+
const CLIP_NONE = 'inset(0px round 0px)';
|
|
903
|
+
|
|
904
|
+
// Animate clip-path on the wrapper. Dedupes via DOM read so repeat ticks
|
|
905
|
+
// (mutation observer) skip identical writes. Seeds CLIP_NONE before the
|
|
906
|
+
// first inset() so the browser interpolates between two inset() shapes —
|
|
907
|
+
// `none → inset()` interpolates inconsistently.
|
|
908
|
+
const setClip = (next: string) => {
|
|
909
|
+
if (!parentWrapper) return;
|
|
910
|
+
const current = parentWrapper.style.clipPath;
|
|
911
|
+
if (current === next) return;
|
|
912
|
+
if (next === CLIP_NONE && !current) return;
|
|
913
|
+
if (!current) {
|
|
914
|
+
parentWrapper.style.transition = '';
|
|
915
|
+
parentWrapper.style.clipPath = CLIP_NONE;
|
|
916
|
+
// eslint-disable-next-line no-void
|
|
917
|
+
void parentWrapper.offsetHeight;
|
|
918
|
+
}
|
|
919
|
+
parentWrapper.style.transition = wrapperTransition;
|
|
920
|
+
parentWrapper.style.clipPath = next;
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
if (descendants.length === 0) {
|
|
924
|
+
parent.style.transition = transition;
|
|
925
|
+
parent.style.transform = '';
|
|
926
|
+
setClip(CLIP_NONE);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Track the immediate child's *actual* top: its cascade transform when it
|
|
931
|
+
// was itself pushed behind a grandchild, else its own snap point. Mirrors
|
|
932
|
+
// Android's addTranslation propagating extra translation up the stack —
|
|
933
|
+
// a grandchild opening pushes this sheet only as far as our child moved.
|
|
934
|
+
const readTranslateY = (el: HTMLElement) => {
|
|
935
|
+
const match = el.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
|
|
936
|
+
return match ? Number.parseFloat(match[1]!) : null;
|
|
937
|
+
};
|
|
938
|
+
const computeTargetY = () => {
|
|
939
|
+
const parentSnap =
|
|
940
|
+
Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
941
|
+
const node = descendants[0]?.nodeRef.current;
|
|
942
|
+
if (!node) return parentSnap;
|
|
943
|
+
const childTop =
|
|
944
|
+
readTranslateY(node) ??
|
|
945
|
+
(Number.parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0);
|
|
946
|
+
return Math.max(parentSnap, childTop);
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// When a form-sheet parent has a form-sheet descendant, clip the parent to
|
|
950
|
+
// the child card's viewport box so it doesn't peek above/around. Only
|
|
951
|
+
// applies when this sheet is itself form — a page parent should remain
|
|
952
|
+
// visible behind/around a floating form child. Geometry comes from the
|
|
953
|
+
// child's inline styles (not getBoundingClientRect) to read the at-rest
|
|
954
|
+
// box, unskewed by vaul's slide-in.
|
|
955
|
+
const applyFormClip = () => {
|
|
956
|
+
const form = isFormSheet ? descendants.find((d) => d.isFormSheetRef.current) : undefined;
|
|
957
|
+
if (!form) {
|
|
958
|
+
setClip(CLIP_NONE);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const childDrawer = form.nodeRef.current;
|
|
962
|
+
const childWrapper = childDrawer?.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
963
|
+
if (!parentWrapper || !childDrawer || !childWrapper) return;
|
|
964
|
+
const snapY =
|
|
965
|
+
Number.parseFloat(childDrawer.style.getPropertyValue('--snap-point-height')) || 0;
|
|
966
|
+
const childBottomGap = Number.parseFloat(childWrapper.style.bottom) || 0;
|
|
967
|
+
const childMaxW = Number.parseFloat(childWrapper.style.maxWidth) || window.innerWidth;
|
|
968
|
+
const formLeft = (window.innerWidth - childMaxW) / 2;
|
|
969
|
+
const formRight = (window.innerWidth + childMaxW) / 2;
|
|
970
|
+
const formBottom = window.innerHeight - childBottomGap;
|
|
971
|
+
const rect = parentWrapper.getBoundingClientRect();
|
|
972
|
+
const top = Math.max(0, snapY - rect.top);
|
|
973
|
+
const left = Math.max(0, formLeft - rect.left);
|
|
974
|
+
const right = Math.max(0, rect.right - formRight);
|
|
975
|
+
const bottom = Math.max(0, rect.bottom - formBottom);
|
|
976
|
+
const radius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
977
|
+
setClip(`inset(${top}px ${right}px ${bottom}px ${left}px round ${radius}px)`);
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const apply = () => {
|
|
981
|
+
applyFormClip();
|
|
982
|
+
// Mirror iPad/Android (`isExpanded`): a parent at full height (detent 1)
|
|
983
|
+
// stays put and the child simply overlays it. Full height is the sheet's
|
|
984
|
+
// own ceiling — the viewport for a page sheet, the capped content-fit
|
|
985
|
+
// height for a form sheet — so compare against where detent 1 lands.
|
|
986
|
+
const parentSnap =
|
|
987
|
+
Number.parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
988
|
+
const { effectiveH, ceiling } = computeDetentGeometry();
|
|
989
|
+
if (parentSnap <= effectiveH - ceiling + 0.5) {
|
|
990
|
+
parent.style.transition = transition;
|
|
991
|
+
parent.style.transform = '';
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
// Mirror Android: stay put while the child is being dragged (its live
|
|
995
|
+
// transform would drag us along during drag-to-dismiss). The release
|
|
996
|
+
// snap rewrites the child's style and re-applies.
|
|
997
|
+
if (descendants[0]?.nodeRef.current?.classList.contains(DRAG_CLASS)) return;
|
|
998
|
+
const targetY = computeTargetY();
|
|
999
|
+
const currentY = readTranslateY(parent) ?? 0;
|
|
1000
|
+
if (Math.abs(currentY - targetY) < 0.5) return;
|
|
1001
|
+
parent.style.transition = transition;
|
|
1002
|
+
parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const raf = requestAnimationFrame(apply);
|
|
1006
|
+
// Vaul re-runs snapToPoint on window resize (e.g., mobile keyboard open)
|
|
1007
|
+
// which clobbers the cascade transform. Re-apply whenever the parent's
|
|
1008
|
+
// inline style changes.
|
|
1009
|
+
const observer = new MutationObserver(apply);
|
|
1010
|
+
observer.observe(parent, { attributes: true, attributeFilter: ['style'] });
|
|
1011
|
+
// The child's own cascade lands in a later frame — follow it when it moves.
|
|
1012
|
+
const childNode = descendants[0]?.nodeRef.current;
|
|
1013
|
+
if (childNode) observer.observe(childNode, { attributes: true, attributeFilter: ['style'] });
|
|
1014
|
+
|
|
1015
|
+
return () => {
|
|
1016
|
+
cancelAnimationFrame(raf);
|
|
1017
|
+
observer.disconnect();
|
|
1018
|
+
};
|
|
1019
|
+
}, [descendants, activeSnapPoint, cornerRadius, isFormSheet, isOpen, computeDetentGeometry]);
|
|
1020
|
+
|
|
1021
|
+
// Focus/blur events fire when a descendant sheet appears on top of this one
|
|
1022
|
+
// (blur) or when all descendants are dismissed (focus). will-events fire
|
|
1023
|
+
// synchronously at the transition boundary; did-events fire once the cascade
|
|
1024
|
+
// transform drains. Intermediate count changes (1↔2) don't re-fire — this
|
|
1025
|
+
// sheet stays blurred throughout.
|
|
1026
|
+
const onWillBlurRef = useRef(onWillBlur);
|
|
1027
|
+
const onDidBlurRef = useRef(onDidBlur);
|
|
1028
|
+
const onWillFocusRef = useRef(onWillFocus);
|
|
1029
|
+
const onDidFocusRef = useRef(onDidFocus);
|
|
1030
|
+
const onDismissAttemptRef = useRef(onDismissAttempt);
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
onWillBlurRef.current = onWillBlur;
|
|
1033
|
+
onDidBlurRef.current = onDidBlur;
|
|
1034
|
+
onWillFocusRef.current = onWillFocus;
|
|
1035
|
+
onDidFocusRef.current = onDidFocus;
|
|
1036
|
+
onDismissAttemptRef.current = onDismissAttempt;
|
|
1037
|
+
});
|
|
1038
|
+
const handleDismissAttempt = useCallback(() => {
|
|
1039
|
+
onDismissAttemptRef.current?.({ nativeEvent: null } as DismissAttemptEvent);
|
|
1040
|
+
}, []);
|
|
1041
|
+
|
|
1042
|
+
const prevDescendantCountRef = useRef(0);
|
|
1043
|
+
useEffect(() => {
|
|
1044
|
+
const prevCount = prevDescendantCountRef.current;
|
|
1045
|
+
const count = descendants.length;
|
|
1046
|
+
prevDescendantCountRef.current = count;
|
|
1047
|
+
if (!isOpen) return;
|
|
1048
|
+
const gained = count > 0 && prevCount === 0;
|
|
1049
|
+
const lost = count === 0 && prevCount > 0;
|
|
1050
|
+
if (!gained && !lost) return;
|
|
1051
|
+
|
|
1052
|
+
if (gained) {
|
|
1053
|
+
onWillBlurRef.current?.({ nativeEvent: null } as WillBlurEvent);
|
|
1054
|
+
} else {
|
|
1055
|
+
onWillFocusRef.current?.({ nativeEvent: null } as WillFocusEvent);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const drawer = drawerContentRef.current;
|
|
1059
|
+
if (!drawer) return;
|
|
1060
|
+
|
|
1061
|
+
let canceled = false;
|
|
1062
|
+
const fireDone = () => {
|
|
1063
|
+
if (canceled) return;
|
|
1064
|
+
if (gained) onDidBlurRef.current?.({ nativeEvent: null } as DidBlurEvent);
|
|
1065
|
+
else onDidFocusRef.current?.({ nativeEvent: null } as DidFocusEvent);
|
|
1066
|
+
};
|
|
1067
|
+
const rafId = window.requestAnimationFrame(() => {
|
|
1068
|
+
if (canceled) return;
|
|
1069
|
+
// Force style recalc so the cascade effect's queued transform registers.
|
|
1070
|
+
|
|
1071
|
+
drawer.offsetHeight;
|
|
1072
|
+
const pending = drawer.getAnimations().filter((a) => a.playState !== 'finished');
|
|
1073
|
+
if (pending.length === 0) {
|
|
1074
|
+
fireDone();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
Promise.allSettled(pending.map((a) => a.finished)).then(() => {
|
|
1078
|
+
if (!canceled) fireDone();
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
return () => {
|
|
1083
|
+
canceled = true;
|
|
1084
|
+
window.cancelAnimationFrame(rafId);
|
|
1085
|
+
};
|
|
1086
|
+
}, [isOpen, descendants.length]);
|
|
1087
|
+
|
|
1088
|
+
// Definite-height flex layout (per-detent sizing) unless content must be
|
|
1089
|
+
// measured in natural flow: 'auto' detents (without a plugged scrollable —
|
|
1090
|
+
// those measure via `measureNaturalHeight`) and form-sheet content-fit sizing.
|
|
1091
|
+
const useSizedLayout = (!hasAutoDetent || hasBoundedScrollable) && !isFormSheet;
|
|
1092
|
+
|
|
1093
|
+
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
1094
|
+
|
|
1095
|
+
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
1096
|
+
// Android's `elevation` semantics roughly — the sheet "lifts" off whatever is
|
|
1097
|
+
// behind it. Scales linearly so higher elevation reads as more separation.
|
|
1098
|
+
// Applied to the vaul wrapper (not the drawer) as `filter: drop-shadow`: the
|
|
1099
|
+
// wrapper clips the drawer (overflow: hidden + contain: paint), which would
|
|
1100
|
+
// cut off `box-shadow` on the drawer at the wrapper edges — visible in
|
|
1101
|
+
// detached mode (bottom blur clipped in the floating gap) and when the
|
|
1102
|
+
// wrapper is narrowed by maxWidth/anchor margins (lateral blur clipped at
|
|
1103
|
+
// wrapper edges). drop-shadow on the wrapper follows the post-clip silhouette
|
|
1104
|
+
// and isn't clipped by the wrapper itself.
|
|
1105
|
+
const dropShadow =
|
|
1106
|
+
elevation > 0
|
|
1107
|
+
? `drop-shadow(0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15))`
|
|
1108
|
+
: undefined;
|
|
1109
|
+
|
|
1110
|
+
const mergedContentStyle = useMemo<React.CSSProperties>(
|
|
1111
|
+
() => ({
|
|
1112
|
+
position: 'fixed',
|
|
1113
|
+
top: 0,
|
|
1114
|
+
left: 0,
|
|
1115
|
+
right: 0,
|
|
1116
|
+
bottom: 0,
|
|
1117
|
+
display: 'flex',
|
|
1118
|
+
flexDirection: 'column',
|
|
1119
|
+
borderTopLeftRadius: effectiveCornerRadius,
|
|
1120
|
+
borderTopRightRadius: effectiveCornerRadius,
|
|
1121
|
+
backgroundColor: backgroundColor as string,
|
|
1122
|
+
// Clip children to the rounded top so headers/content with their own
|
|
1123
|
+
// background don't bleed past the corners. `clip` (not `hidden`): a
|
|
1124
|
+
// hidden box is still a scroll container, so browser focus-reveal (e.g.
|
|
1125
|
+
// tapping a button near the sheet bottom) can scroll-offset the drawer —
|
|
1126
|
+
// shifting content and blocking vaul's shouldDrag walk (scrollTop > 0).
|
|
1127
|
+
overflow: 'clip',
|
|
1128
|
+
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
1129
|
+
// A relative footer owns the inset instead (see resolvedFooterStyle); an
|
|
1130
|
+
// absolute footer floats over the content, so the content keeps its lift.
|
|
1131
|
+
// A plugged scrollable keeps the lift too (contentInsetAdjustmentBehavior,
|
|
1132
|
+
// default on) — disable it to scroll edge-to-edge behind the indicator
|
|
1133
|
+
// with user-applied content padding.
|
|
1134
|
+
paddingBottom:
|
|
1135
|
+
insetAdjustment === 'automatic' &&
|
|
1136
|
+
!(footerOwnsInset && !absoluteFooter) &&
|
|
1137
|
+
(!scrollableRef || (scrollableOptions?.contentInsetAdjustmentBehavior ?? true))
|
|
1138
|
+
? 'env(safe-area-inset-bottom, 0px)'
|
|
1139
|
+
: 0,
|
|
1140
|
+
}),
|
|
1141
|
+
[
|
|
1142
|
+
backgroundColor,
|
|
1143
|
+
effectiveCornerRadius,
|
|
1144
|
+
insetAdjustment,
|
|
1145
|
+
footerOwnsInset,
|
|
1146
|
+
absoluteFooter,
|
|
1147
|
+
scrollableRef,
|
|
1148
|
+
scrollableOptions,
|
|
1149
|
+
]
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const defaultGrabberColor =
|
|
1153
|
+
colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
|
|
1154
|
+
|
|
1155
|
+
const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
|
|
1156
|
+
|
|
1157
|
+
// Footer is rendered inside the wrapper via `detachedSiblings`, so it
|
|
1158
|
+
// follows the wrapper on dismiss and drag-overshoot. Positioning is
|
|
1159
|
+
// relative to the wrapper (contain: paint creates the containing block).
|
|
1160
|
+
const footerFloatStyle = useMemo<React.CSSProperties>(
|
|
1161
|
+
() => ({
|
|
1162
|
+
position: 'fixed',
|
|
1163
|
+
left: 0,
|
|
1164
|
+
right: 0,
|
|
1165
|
+
bottom: 0,
|
|
1166
|
+
// Wrapper has `pointer-events: none` to let clicks fall through; the
|
|
1167
|
+
// footer must opt back in.
|
|
1168
|
+
pointerEvents: 'auto',
|
|
1169
|
+
}),
|
|
1170
|
+
[]
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
// Form-sheet style (presentation='form'): centered floating card with a
|
|
1174
|
+
// default width and a height fit to content. We reuse the existing detached
|
|
1175
|
+
// mechanic so drag/snap math stays correct — the wrapper is bottom-attached
|
|
1176
|
+
// with a computed offset (`effectiveDetachedOffset`, declared above) that
|
|
1177
|
+
// centers it vertically. `presentation` is absolute: when 'form',
|
|
1178
|
+
// `maxContentWidth` is ignored and the card uses DEFAULT_FORM_SHEET_WIDTH.
|
|
1179
|
+
|
|
1180
|
+
// The wrapper holds all horizontal sizing/anchoring so its rounded-bottom
|
|
1181
|
+
// clip (when detached) aligns with the drawer's horizontal bounds on
|
|
1182
|
+
// desktop — otherwise its corners sit at the far viewport edges.
|
|
1183
|
+
// - presentation='form' → DEFAULT_FORM_SHEET_WIDTH on tablet/landscape;
|
|
1184
|
+
// `maxContentWidth` is ignored ('form' is absolute).
|
|
1185
|
+
// - presentation='page' → `maxContentWidth` (any viewport) or
|
|
1186
|
+
// DEFAULT_MAX_WIDTH (tablet/landscape readability cap).
|
|
1187
|
+
// Detached without a width constraint applies anchorOffset on both edges so
|
|
1188
|
+
// the floating card breathes from the viewport sides.
|
|
1189
|
+
const wrapperStyle = useMemo<React.CSSProperties | undefined>(() => {
|
|
1190
|
+
// Mobile portrait ignores width sizing entirely (matches iOS/Android:
|
|
1191
|
+
// both apply `maxContentWidth` only when not on a portrait phone).
|
|
1192
|
+
// `detached` + `detachedOffset` are still respected via the wrapper.
|
|
1193
|
+
const maxWidth = isLandscapeOrTablet
|
|
1194
|
+
? presentation === 'form'
|
|
1195
|
+
? DEFAULT_FORM_SHEET_WIDTH
|
|
1196
|
+
: (maxContentWidth ?? DEFAULT_MAX_WIDTH)
|
|
1197
|
+
: undefined;
|
|
1198
|
+
|
|
1199
|
+
const needsMargins = maxWidth != null || effectiveDetached;
|
|
1200
|
+
if (!needsMargins && !dropShadow) return undefined;
|
|
1201
|
+
|
|
1202
|
+
const next: React.CSSProperties = {};
|
|
1203
|
+
if (dropShadow) next.filter = dropShadow;
|
|
1204
|
+
if (!needsMargins) return next;
|
|
1205
|
+
|
|
1206
|
+
let marginLeft: number | string;
|
|
1207
|
+
let marginRight: number | string;
|
|
1208
|
+
if (isFormSheet) {
|
|
1209
|
+
marginLeft = 'auto';
|
|
1210
|
+
marginRight = 'auto';
|
|
1211
|
+
} else if (maxWidth == null) {
|
|
1212
|
+
marginLeft = anchorOffset;
|
|
1213
|
+
marginRight = anchorOffset;
|
|
1214
|
+
} else {
|
|
1215
|
+
marginLeft = anchor === 'left' ? anchorOffset : 'auto';
|
|
1216
|
+
marginRight = anchor === 'right' ? anchorOffset : 'auto';
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (maxWidth != null) next.maxWidth = maxWidth;
|
|
1220
|
+
next.marginLeft = marginLeft;
|
|
1221
|
+
next.marginRight = marginRight;
|
|
1222
|
+
return next;
|
|
1223
|
+
}, [
|
|
1224
|
+
isLandscapeOrTablet,
|
|
1225
|
+
isFormSheet,
|
|
1226
|
+
maxContentWidth,
|
|
1227
|
+
presentation,
|
|
1228
|
+
anchor,
|
|
1229
|
+
anchorOffset,
|
|
1230
|
+
effectiveDetached,
|
|
1231
|
+
dropShadow,
|
|
1232
|
+
]);
|
|
1233
|
+
|
|
1234
|
+
// Absolute-position the grabber so it overlays the content top-edge
|
|
1235
|
+
// instead of consuming flow height — mirrors native iOS/Android, where
|
|
1236
|
+
// the grabber sits in the rounded corner zone above the content and
|
|
1237
|
+
// doesn't push the header down or inflate the 'auto' detent measurement.
|
|
1238
|
+
const handleStyle = useMemo<React.CSSProperties>(
|
|
1239
|
+
() => ({
|
|
1240
|
+
position: 'absolute',
|
|
1241
|
+
top: grabberOptions?.topMargin ?? DEFAULT_GRABBER_TOP_MARGIN,
|
|
1242
|
+
left: '50%',
|
|
1243
|
+
transform: 'translateX(-50%)',
|
|
1244
|
+
height: grabberHeight,
|
|
1245
|
+
width: grabberOptions?.width ?? DEFAULT_GRABBER_WIDTH,
|
|
1246
|
+
borderRadius: grabberOptions?.cornerRadius ?? grabberHeight / 2,
|
|
1247
|
+
backgroundColor: (grabberOptions?.color ?? defaultGrabberColor) as string,
|
|
1248
|
+
opacity: 1,
|
|
1249
|
+
// Above absolute-positioned headers (which often use zIndex:1 to overlay
|
|
1250
|
+
// scroll content) so the grabber stays draggable — critical when
|
|
1251
|
+
// `handleOnly` mode means only the grabber can drag the sheet.
|
|
1252
|
+
zIndex: 2,
|
|
1253
|
+
}),
|
|
1254
|
+
[grabberOptions, grabberHeight, defaultGrabberColor]
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
return (
|
|
1258
|
+
<TrueSheetPeekContext.Provider value={peekContext}>
|
|
1259
|
+
<Drawer.Root
|
|
1260
|
+
open={isOpen}
|
|
1261
|
+
onOpenChange={handleOpenChange}
|
|
1262
|
+
onPositionChange={handlePositionChange}
|
|
1263
|
+
onDrag={handleDrag}
|
|
1264
|
+
onRelease={handleRelease}
|
|
1265
|
+
dismissible={dismissible}
|
|
1266
|
+
onDismissAttempt={handleDismissAttempt}
|
|
1267
|
+
draggable={draggable}
|
|
1268
|
+
repositionInputs={false}
|
|
1269
|
+
modal={dimmed}
|
|
1270
|
+
nested={isNested}
|
|
1271
|
+
detached={effectiveDetached}
|
|
1272
|
+
detachedOffset={effectiveDetachedOffset}
|
|
1273
|
+
detachedRadius={effectiveCornerRadius}
|
|
1274
|
+
maxContentHeight={effectiveMaxContentHeight}
|
|
1275
|
+
peekHeight={peekHeight}
|
|
1276
|
+
initialAnimated={initialDetentAnimated}
|
|
1277
|
+
detachedWrapperStyle={wrapperStyle}
|
|
1278
|
+
onContentHeightChange={setMeasuredContentHeight}
|
|
1279
|
+
contentHeight={
|
|
1280
|
+
// Bounded scrollable → the auto-size wrapper measures 0 (sized
|
|
1281
|
+
// layout is absolute), so feed vaul the natural height instead.
|
|
1282
|
+
hasAutoDetent && hasBoundedScrollable && scrollableAutoHeight > 0
|
|
1283
|
+
? scrollableAutoHeight
|
|
1284
|
+
: undefined
|
|
1285
|
+
}
|
|
1286
|
+
activeSnapPoint={activeSnapPoint}
|
|
1287
|
+
setActiveSnapPoint={handleSetActiveSnapPoint}
|
|
1288
|
+
{...snapPointsProps}
|
|
1289
|
+
>
|
|
1290
|
+
<Drawer.Portal container={portalContainer ?? undefined}>
|
|
1291
|
+
<Drawer.Overlay style={overlayStyle} />
|
|
1292
|
+
<Drawer.Content
|
|
1293
|
+
ref={drawerContentRef}
|
|
1294
|
+
style={mergedContentStyle}
|
|
1295
|
+
onPointerDownOutside={handlePointerDownOutside}
|
|
1296
|
+
detachedSiblings={
|
|
1297
|
+
footer && absoluteFooter ? (
|
|
1298
|
+
<div style={footerFloatStyle}>
|
|
1299
|
+
<View ref={footerElRef} style={resolvedFooterStyle} onLayout={handleFooterLayout}>
|
|
1300
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1301
|
+
</View>
|
|
1302
|
+
</div>
|
|
1303
|
+
) : undefined
|
|
1304
|
+
}
|
|
1305
|
+
>
|
|
1306
|
+
<Drawer.Title style={visuallyHiddenStyle}>
|
|
1307
|
+
{accessibilityOptions?.paneTitle ?? 'Sheet'}
|
|
1308
|
+
</Drawer.Title>
|
|
1309
|
+
{grabber && <Drawer.Handle style={handleStyle} />}
|
|
1310
|
+
{useSizedLayout ? (
|
|
1311
|
+
// vaul wraps children in `[data-vaul-auto-size-wrapper]` (display:
|
|
1312
|
+
// flow-root) which doesn't honor descendant flex layout. Use an
|
|
1313
|
+
// absolute fill sized to the visible portion (via vaul's
|
|
1314
|
+
// `--snap-point-height` var) so the inner flex column has a
|
|
1315
|
+
// definite height for flex layouts to fill — mirrors native, where
|
|
1316
|
+
// the container is sized to the sheet's visible height per detent.
|
|
1317
|
+
<div
|
|
1318
|
+
ref={sizedLayoutRef}
|
|
1319
|
+
style={sizedLayoutStyle}
|
|
1320
|
+
data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
|
|
1321
|
+
>
|
|
1322
|
+
{header && (
|
|
1323
|
+
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1324
|
+
{isValidElement(header) ? header : createElement(header)}
|
|
1325
|
+
</View>
|
|
1326
|
+
)}
|
|
1327
|
+
{/* Content lays out naturally like native — fill only for auto
|
|
1328
|
+
detents with a plugged scrollable, where natural layout is
|
|
1329
|
+
circular (sheet height derives from the scroll content size).
|
|
1330
|
+
Unlike native, CSS won't cap the content to the container
|
|
1331
|
+
(no fit-content flex basis), so the fill is still needed here. */}
|
|
1332
|
+
<View
|
|
1333
|
+
ref={contentRef}
|
|
1334
|
+
style={hasAutoDetent && hasBoundedScrollable ? [contentFillStyle, style] : style}
|
|
1335
|
+
onLayout={handleContentLayout}
|
|
1336
|
+
>
|
|
1337
|
+
{children}
|
|
1338
|
+
</View>
|
|
1339
|
+
{footer && !absoluteFooter && (
|
|
1340
|
+
<View
|
|
1341
|
+
ref={footerElRef}
|
|
1342
|
+
style={[relativeFooterStyle, resolvedFooterStyle]}
|
|
1343
|
+
onLayout={handleFooterLayout}
|
|
1344
|
+
>
|
|
1345
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1346
|
+
</View>
|
|
1347
|
+
)}
|
|
1348
|
+
</div>
|
|
1349
|
+
) : (
|
|
1350
|
+
// Natural flow so vaul can measure content height — required for
|
|
1351
|
+
// 'auto' detents and form-sheet content-fit sizing.
|
|
1352
|
+
<>
|
|
1353
|
+
{header && (
|
|
1354
|
+
<View ref={headerElRef} style={resolvedHeaderStyle} onLayout={handleHeaderLayout}>
|
|
1355
|
+
{isValidElement(header) ? header : createElement(header)}
|
|
1356
|
+
</View>
|
|
1357
|
+
)}
|
|
1358
|
+
<View ref={contentRef} style={style} onLayout={handleContentLayout}>
|
|
1359
|
+
{children}
|
|
1360
|
+
</View>
|
|
1361
|
+
{footer && !absoluteFooter && (
|
|
1362
|
+
<View ref={footerElRef} style={resolvedFooterStyle} onLayout={handleFooterLayout}>
|
|
1363
|
+
{isValidElement(footer) ? footer : createElement(footer)}
|
|
1364
|
+
</View>
|
|
1365
|
+
)}
|
|
1366
|
+
</>
|
|
1367
|
+
)}
|
|
1368
|
+
</Drawer.Content>
|
|
1369
|
+
</Drawer.Portal>
|
|
1370
|
+
</Drawer.Root>
|
|
1371
|
+
</TrueSheetPeekContext.Provider>
|
|
1372
|
+
);
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
const overlayStyle: React.CSSProperties = {
|
|
1376
|
+
position: 'fixed',
|
|
1377
|
+
inset: 0,
|
|
1378
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
// Pinned to the sheet's bottom edge — takes up layout space below the content
|
|
1382
|
+
const relativeFooterStyle = {
|
|
1383
|
+
marginTop: 'auto',
|
|
1384
|
+
} as const;
|
|
1385
|
+
|
|
1386
|
+
// Floats over the content so it doesn't take up layout space
|
|
1387
|
+
const absoluteHeaderStyle = {
|
|
1388
|
+
position: 'absolute',
|
|
1389
|
+
top: 0,
|
|
1390
|
+
left: 0,
|
|
1391
|
+
right: 0,
|
|
1392
|
+
zIndex: 1,
|
|
1393
|
+
} as const;
|
|
1394
|
+
|
|
1395
|
+
const SIZED_LAYOUT_HEIGHT = 'calc(100% - var(--snap-point-height, 0px))';
|
|
1396
|
+
|
|
1397
|
+
const sizedLayoutStyle: React.CSSProperties = {
|
|
1398
|
+
position: 'absolute',
|
|
1399
|
+
top: 0,
|
|
1400
|
+
left: 0,
|
|
1401
|
+
right: 0,
|
|
1402
|
+
height: SIZED_LAYOUT_HEIGHT,
|
|
1403
|
+
display: 'flex',
|
|
1404
|
+
flexDirection: 'column',
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
const contentFillStyle = {
|
|
1408
|
+
flex: 1,
|
|
1409
|
+
minHeight: 0,
|
|
1410
|
+
} as const;
|
|
1411
|
+
|
|
1412
|
+
const visuallyHiddenStyle: React.CSSProperties = {
|
|
1413
|
+
position: 'absolute',
|
|
1414
|
+
width: 1,
|
|
1415
|
+
height: 1,
|
|
1416
|
+
padding: 0,
|
|
1417
|
+
margin: -1,
|
|
1418
|
+
overflow: 'hidden',
|
|
1419
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
1420
|
+
whiteSpace: 'nowrap',
|
|
1421
|
+
border: 0,
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
const STATIC_METHOD_ERROR =
|
|
1425
|
+
'Static methods are not supported on web. Use the useTrueSheet() hook instead.';
|
|
1426
|
+
|
|
1427
|
+
export const TrueSheet = TrueSheetComponent as typeof TrueSheetComponent & TrueSheetStaticMethods;
|
|
1428
|
+
|
|
1429
|
+
const rejectStatic = async (): Promise<never> => {
|
|
1430
|
+
throw new Error(STATIC_METHOD_ERROR);
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
TrueSheet.present = rejectStatic;
|
|
1434
|
+
TrueSheet.dismiss = rejectStatic;
|
|
1435
|
+
TrueSheet.dismissStack = rejectStatic;
|
|
1436
|
+
TrueSheet.resize = rejectStatic;
|
|
1437
|
+
TrueSheet.dismissAll = rejectStatic;
|