feat(android): implement preventNativeDismiss / onNativeDismissCancelled for formSheet#3873
Open
wildseansy wants to merge 3 commits intosoftware-mansion:mainfrom
Open
Conversation
Android's BottomSheetBehavior has no synchronous 'shouldDismiss' hook analogous to iOS's presentationControllerShouldDismiss, so the gestureEnabled and preventNativeDismiss props were previously no-ops on Android formSheet screens. This commit wires up preventNativeDismiss for formSheet on Android using the closest available equivalent: - When preventNativeDismiss=true, BottomSheetBehavior.isHideable is set to false so the sheet cannot reach STATE_HIDDEN via a drag gesture. The sheet instead bounces back to its last stable detent. - SheetStateObserver tracks downward drag attempts via onSlide (slideOffset < DISMISS_ATTEMPT_SLIDE_THRESHOLD). When the sheet subsequently settles to a non-hidden stable state, the new onNativeDismissCancelled event is dispatched to JS — matching the existing iOS behaviour. - isPreventNativeDismiss uses a property setter so runtime changes (e.g. a form transitioning from clean → dirty) are reflected immediately in BottomSheetBehavior without requiring a re-mount. - gestureEnabled is also respected during initial behaviour setup (isDraggable = screen.isGestureEnabled) so both props work correctly together. Files changed: - Screen.kt: add isPreventNativeDismiss property with live setter - ScreenViewManager.kt: connect setPreventNativeDismiss + register event - SheetDelegate.kt: apply isHideable, track drag attempts, fire event - ScreenEventEmitter.kt: add dispatchOnNativeDismissCancelled() - ScreenNativeDismissCancelledEvent.kt: new event class - src/types.tsx: extend @platform annotations to include android Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gestureEnabled / isDraggable support is already tracked in software-mansion#2937. This PR focuses solely on preventNativeDismiss / onNativeDismissCancelled to support usePreventRemove on Android formSheet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous approach set isHideable=false when preventNativeDismiss=true to block BottomSheetBehavior from reaching STATE_HIDDEN. On a 1-detent sheet, BottomSheetBehavior responds by settling to STATE_COLLAPSED instead, which SheetUtils.detentIndexFromSheetState does not handle (only STATE_HIDDEN and STATE_EXPANDED are valid for detentCount=1), causing an IllegalArgumentException crash. The fix keeps isHideable=true so STATE_COLLAPSED is never reached. SheetStateObserver now intercepts STATE_HIDDEN directly: when isPreventNativeDismiss is true and the sheet reaches STATE_HIDDEN, we immediately re-expand to lastStableState and dispatch onNativeDismissCancelled so the JS layer can react (e.g. show a "discard changes?" dialog). Also removes the onSlide hasDraggedTowardDismiss tracking and the DISMISS_ATTEMPT_SLIDE_THRESHOLD constant, which were artefacts of the old approach. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
preventNativeDismissandonNativeDismissCancelledwere no-ops on Android. Because of this, navigation libraries (e.g. React Navigation) that rely onusePreventRemove+onNativeDismissCancelledto implement "unsaved changes" guards work correctly on iOS but are completely bypassed on AndroidformSheet— the sheet dismisses natively before the JS layer can intercept it.Root cause
iOS exposes
UIAdaptivePresentationControllerDelegate.presentationControllerShouldDismiss— a synchronous hook that can returnNObefore any visual motion occurs. Android'sBottomSheetBehaviorhas no equivalent. By the time any callback fires, the sheet has already moved.Solution
Implement the closest Android equivalent using
BottomSheetBehavior.isHideable:preventNativeDismiss=truesetBottomSheetBehavior.isHideable = false→ sheet cannot reachSTATE_HIDDENvia gestureonSlidefires;slideOffset < -0.05fflags a dismiss attemptonNativeDismissCancelledis dispatched to JSRuntime changes to
preventNativeDismiss(e.g. a form going from clean → dirty) are reflected immediately via a property setter that updatesBottomSheetBehavior.isHideablewithout requiring a re-mount.The sheet moves slightly before bouncing back (unlike iOS where the gesture is cancelled before any visual motion), but the JS contract is identical on both platforms:
preventNativeDismissblocks the dismissal andonNativeDismissCancelledfires so the app can respond.Example usage
The following pattern works on iOS today. After this PR, it also works on Android
formSheetwith no changes required in application code:Demo of fix in action
demo.mov
Files changed
Screen.ktisPreventNativeDismisswith a setter that keepsBottomSheetBehavior.isHideablein sync at runtimeScreenViewManager.ktsetPreventNativeDismiss(was a no-op); registeronNativeDismissCancelledeventSheetDelegate.ktisHideable = !screen.isPreventNativeDismissduring setup;SheetStateObservertracks drag-toward-dismiss and dispatches event on bounce-backScreenEventEmitter.ktdispatchOnNativeDismissCancelled()ScreenNativeDismissCancelledEvent.kttopNativeDismissCancelled)src/types.tsx@platformannotations forpreventNativeDismissandonNativeDismissCancelledto include AndroidKnown differences from iOS
presentationControllerShouldDismiss. Android has no pre-gesture synchronous interception point.DISMISS_ATTEMPT_SLIDE_THRESHOLD = -0.05fis a heuristic. Open to feedback on whether this value needs tuning.formSheetpresentation is affected.pageSheetfalls back tomodalon Android and is already JS-navigation-controlled.