Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide a way to prevent native navigation #801

Merged
merged 28 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
887c084
feat: make Android back button fire event
WoLewicki Feb 5, 2021
0982c9f
feat: update options for iOS swipe and modal
WoLewicki Feb 11, 2021
79f6cde
feat: merge current master
WoLewicki Apr 23, 2021
d60bd8f
feat: add another implementation
WoLewicki Apr 26, 2021
a46ea56
chore: merge current master
WoLewicki Jul 6, 2021
535c967
Merge branch 'master' into @wolewicki/android-back-button-event
WoLewicki Aug 9, 2021
656efb3
feat: another implementation
WoLewicki Aug 9, 2021
9b0e9eb
feat: not ready version
WoLewicki Aug 17, 2021
d6dfce5
chore: merge current master
WoLewicki Aug 23, 2021
b4d6bf0
feat: add context for preventing dismiss
WoLewicki Aug 23, 2021
9e2778b
fix: prop name
WoLewicki Aug 23, 2021
37fe875
chore: merge current master
WoLewicki Aug 24, 2021
46223ce
fix: android compiling after master merge
WoLewicki Aug 24, 2021
1e53113
fix: add missing prop from master merge
WoLewicki Aug 25, 2021
ead6546
Merge branch 'master' into @wolewicki/android-back-button-event
WoLewicki Aug 25, 2021
255d7c5
fix: clean kotlin code and move contexts to utils of screens package
WoLewicki Aug 25, 2021
ae355b7
feat: apply changes
WoLewicki Aug 25, 2021
faef2ab
fix: cleanup, use map instead of obj, memo context
kacperkapusciak Aug 25, 2021
648f250
fix: remove symbol desc, use map, use state callback
kacperkapusciak Aug 26, 2021
879d770
fix: group related variables, typo
kacperkapusciak Aug 26, 2021
7efbb42
fix: documentation and minor changes
WoLewicki Aug 26, 2021
2136003
chore: merge current master
kacperkapusciak Aug 26, 2021
5e87c70
fix: newline issues
kacperkapusciak Aug 26, 2021
bf2b558
fix: order Screen props alphabetically
kacperkapusciak Aug 27, 2021
1101a8e
fix: move prop alphabetically
kacperkapusciak Aug 27, 2021
3f2a077
chore: merge current master
WoLewicki Sep 6, 2021
69a2472
feat: revert JS changes
WoLewicki Sep 6, 2021
e199dcb
fix: wrong return and some more cleaning
WoLewicki Sep 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion TestsExample/src/Test556.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ function Second({navigation}) {
<Button title="Tap me for second screen" onPress={() => navigation.navigate('First')} />
);
}

7 changes: 7 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Screen constructor(context: ReactContext?) : ViewGroup(context) {
private var mStatusBarTranslucent: Boolean? = null
private var mStatusBarColor: Int? = null
var isStatusBarAnimated: Boolean? = null
private var mNativeBackButtonDismissalEnabled = true

init {
// we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
Expand Down Expand Up @@ -228,6 +229,12 @@ class Screen constructor(context: ReactContext?) : ViewGroup(context) {
fragment?.let { ScreenWindowTraits.setColor(this, it.tryGetActivity(), it.tryGetContext()) }
}

var nativeBackButtonDismissalEnabled: Boolean
get() = mNativeBackButtonDismissalEnabled
set(enableNativeBackButtonDismissal) {
mNativeBackButtonDismissalEnabled = enableNativeBackButtonDismissal
}

enum class StackPresentation {
PUSH, MODAL, TRANSPARENT_MODAL
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.events.Event
import com.swmansion.rnscreens.events.HeaderBackButtonClickedEvent
import com.swmansion.rnscreens.events.ScreenAppearEvent
import com.swmansion.rnscreens.events.ScreenDisappearEvent
import com.swmansion.rnscreens.events.ScreenDismissedEvent
Expand Down Expand Up @@ -179,6 +180,13 @@ open class ScreenFragment : Fragment {
}
}

fun dispatchHeaderBackButtonClickedEvent() {
(screen.context as ReactContext)
.getNativeModule(UIManagerModule::class.java)
?.eventDispatcher
?.dispatchEvent(HeaderBackButtonClickedEvent(screen.id))
}

fun dispatchTransitionProgress(alpha: Float, closing: Boolean) {
if (this is ScreenStackFragment) {
if (mProgress != alpha) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,18 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
if (stack != null && stack.rootScreen == it.screen) {
val parentFragment = it.parentFragment
if (parentFragment is ScreenStackFragment) {
parentFragment.dismiss()
if (parentFragment.screen.nativeBackButtonDismissalEnabled) {
parentFragment.dismiss()
} else {
parentFragment.dispatchHeaderBackButtonClickedEvent()
}
}
} else {
it.dismiss()
if (it.screen.nativeBackButtonDismissalEnabled) {
it.dismiss()
} else {
it.dispatchHeaderBackButtonClickedEvent()
}
}
}
}
Expand Down
18 changes: 15 additions & 3 deletions android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactProp
import com.swmansion.rnscreens.events.HeaderBackButtonClickedEvent
import com.swmansion.rnscreens.events.ScreenAppearEvent
import com.swmansion.rnscreens.events.ScreenDisappearEvent
import com.swmansion.rnscreens.events.ScreenDismissedEvent
Expand Down Expand Up @@ -111,8 +112,16 @@ class ScreenViewManager : ViewGroupManager<Screen>() {
view.isStatusBarHidden = statusBarHidden
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? {
return MapBuilder.of(
@ReactProp(name = "nativeBackButtonDismissalEnabled")
fun setNativeBackButtonDismissalEnabled(
view: Screen,
nativeBackButtonDismissalEnabled: Boolean
) {
view.nativeBackButtonDismissalEnabled = nativeBackButtonDismissalEnabled
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
val map: MutableMap<String, Any> = MapBuilder.of(
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"),
ScreenWillAppearEvent.EVENT_NAME,
Expand All @@ -126,8 +135,11 @@ class ScreenViewManager : ViewGroupManager<Screen>() {
StackFinishTransitioningEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onFinishTransitioning"),
ScreenTransitionProgressEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onTransitionProgress"),
MapBuilder.of("registrationName", "onTransitionProgress")
)
// there is no `MapBuilder.of` with more than 7 items
map[HeaderBackButtonClickedEvent.EVENT_NAME] = MapBuilder.of("registrationName", "onHeaderBackButtonClicked")
return map
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.swmansion.rnscreens.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

class HeaderBackButtonClickedEvent(viewId: Int) : Event<ScreenAppearEvent>(viewId) {
override fun getEventName(): String {
return EVENT_NAME
}

override fun getCoalescingKey(): Short {
// All events for a given view can be coalesced.
return 0
}

override fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(viewTag, eventName, Arguments.createMap())
}

companion object {
const val EVENT_NAME = "topHeaderBackButtonClickedEvent"
}
}
9 changes: 8 additions & 1 deletion createNativeStackNavigator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Whether to show or hide the header for the screen. The header is shown by defaul
Style object for the header. Supported properties:

- `backgroundColor`
- `blurEffect` (iOS only). Possible values can be checked in `index.d.ts` file.
- `blurEffect` (iOS only).

#### `headerTintColor`

Expand All @@ -189,6 +189,13 @@ A Boolean to that lets you opt out of insetting the header. You may want to * se

Boolean indicating whether the navigation bar is translucent.

#### `nativeBackButtonDismissalEnabled` (Android only)

Boolean indicating whether, when the Android default back button is clicked, the `pop` action should be performed on the native side or on the JS side to be able to prevent it.
Unfortunately the same behavior is not available on iOS since the behavior of native back button cannot be changed there.

Defaults to `false`.

#### `replaceAnimation`

How should the screen replacing another screen animate.
Expand Down
11 changes: 11 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ Boolean indicating whether the swipe gesture should work on whole screen. Swipin

When set to `false` the back swipe gesture will be disabled. The default value is `true`.

### `nativeBackButtonDismissalEnabled` (Android only)

Boolean indicating whether, when the Android default back button is clicked, the `pop` action should be performed on the native side or on the JS side to be able to prevent it.
Unfortunately the same behavior is not available on iOS since the behavior of native back button cannot be changed there.

Defaults to `false`.

### `onAppear`

A callback that gets called when the current screen appears.
Expand All @@ -60,6 +67,10 @@ A callback that gets called when the current screen disappears.

A callback that gets called when the current screen is dismissed by hardware back (on Android) or dismiss gesture (swipe back or down). The callback takes no arguments.

### `onHeaderBackButtonClicked` (Android only)

A callback that gets called when the native header back button is clicked on Android and `enableNativeBackButtonDismissal` is set to `false`.

### `onWillAppear`

A callback that gets called when the current screen will appear. This is called as soon as the transition begins.
Expand Down
3 changes: 3 additions & 0 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ typedef NS_ENUM(NSInteger, RNSWindowTrait) {
@property (nonatomic, copy) RCTDirectEventBlock onDismissed;
@property (nonatomic, copy) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy) RCTDirectEventBlock onNativeDismissCancelled;
@property (nonatomic, copy) RCTDirectEventBlock onTransitionProgress;

@property (weak, nonatomic) UIView<RNSScreenContainerDelegate> *reactSuperview;
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic, readonly) BOOL dismissed;
Expand All @@ -88,6 +90,7 @@ typedef NS_ENUM(NSInteger, RNSWindowTrait) {
@property (nonatomic) RNSScreenStackAnimation stackAnimation;
@property (nonatomic) RNSScreenStackPresentation stackPresentation;
@property (nonatomic) RNSScreenReplaceAnimation replaceAnimation;
@property (nonatomic) BOOL preventNativeDismiss;
@property (nonatomic) BOOL hasOrientationSet;
@property (nonatomic) BOOL hasStatusBarStyleSet;
@property (nonatomic) BOOL hasStatusBarAnimationSet;
Expand Down
35 changes: 28 additions & 7 deletions ios/RNSScreen.m
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ - (void)notifyDisappear
}
}

- (void)notifyDismissCancelledWithDismissCount:(int)dismissCount
{
if (self.onNativeDismissCancelled) {
self.onNativeDismissCancelled(@{@"dismissCount" : @(dismissCount)});
}
}

- (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingForward:(BOOL)goingForward
{
if (self.onTransitionProgress) {
Expand Down Expand Up @@ -342,6 +349,10 @@ - (RCTTouchHandler *)touchHandler

- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
if (_preventNativeDismiss) {
[self notifyDismissCancelledWithDismissCount:1];
return NO;
}
return _gestureEnabled;
}

Expand Down Expand Up @@ -621,8 +632,15 @@ - (void)viewDidDisappear:(BOOL)animated
[super viewDidDisappear:animated];

if (self.parentViewController == nil && self.presentingViewController == nil) {
// screen dismissed, send event
[((RNSScreenView *)self.view) notifyDismissedWithCount:_dismissCount];
if (((RNSScreenView *)self.view).preventNativeDismiss) {
// if we want to prevent the native dismiss, we do not send dismissal event,
// but instead call `updateContainer`, which restores the JS navigation stack
[((RNSScreenView *)self.view).reactSuperview updateContainer];
[((RNSScreenView *)self.view) notifyDismissCancelledWithDismissCount:_dismissCount];
} else {
// screen dismissed, send event
[((RNSScreenView *)self.view) notifyDismissedWithCount:_dismissCount];
}
}

// same flow as in viewDidAppear
Expand Down Expand Up @@ -711,24 +729,27 @@ @implementation RNSScreenManager

// we want to handle the case when activityState is nil
RCT_REMAP_VIEW_PROPERTY(activityState, activityStateOrNil, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(customAnimationOnSwipe, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(preventNativeDismiss, BOOL)
RCT_EXPORT_VIEW_PROPERTY(replaceAnimation, RNSScreenReplaceAnimation)
RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock);

WoLewicki marked this conversation as resolved.
Show resolved Hide resolved
RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDisappear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onNativeDismissCancelled, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onTransitionProgress, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(customAnimationOnSwipe, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock);

#if !TARGET_OS_TV
RCT_EXPORT_VIEW_PROPERTY(screenOrientation, UIInterfaceOrientationMask)
RCT_EXPORT_VIEW_PROPERTY(statusBarStyle, RNSStatusBarStyle)
RCT_EXPORT_VIEW_PROPERTY(statusBarAnimation, UIStatusBarAnimation)
RCT_EXPORT_VIEW_PROPERTY(statusBarHidden, BOOL)
RCT_EXPORT_VIEW_PROPERTY(statusBarStyle, RNSStatusBarStyle)
#endif

- (UIView *)view
Expand Down
1 change: 1 addition & 0 deletions ios/RNSScreenContainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@protocol RNSScreenContainerDelegate

- (void)markChildUpdated;
- (void)updateContainer;

@end

Expand Down
1 change: 1 addition & 0 deletions ios/RNSScreenStack.m
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
if (!topScreen.gestureEnabled || _controller.viewControllers.count < 2) {
return NO;
}

#if TARGET_OS_TV
[self cancelTouchesInParent];
return YES;
Expand Down
9 changes: 8 additions & 1 deletion native-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Whether to show or hide the header for the screen. The header is shown by defaul
Style object for the header. Supported properties:

- `backgroundColor`
- `blurEffect` (iOS only). Possible values can be checked in `index.d.ts` file.
- `blurEffect` (iOS only).

#### `headerTintColor`

Expand All @@ -182,6 +182,13 @@ A Boolean to that lets you opt out of insetting the header. You may want to * se

Boolean indicating whether the navigation bar is translucent.

#### `nativeBackButtonDismissalEnabled` (Android only)

Boolean indicating whether, when the Android default back button is clicked, the `pop` action should be performed on the native side or on the JS side to be able to prevent it.
Unfortunately the same behavior is not available on iOS since the behavior of native back button cannot be changed there.

Defaults to `false`.

#### `replaceAnimation`

How should the screen replacing another screen animate.
Expand Down
6 changes: 6 additions & 0 deletions src/createNativeStackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ function StackView({
? true
: options.gestureEnabled
}
nativeBackButtonDismissalEnabled={
options.nativeBackButtonDismissalEnabled
}
fullScreenSwipeEnabled={options.fullScreenSwipeEnabled}
screenOrientation={options.screenOrientation}
statusBarAnimation={options.statusBarAnimation}
Expand All @@ -489,6 +492,9 @@ function StackView({
onWillAppear={() => options?.onWillAppear?.()}
onWillDisappear={() => options?.onWillDisappear?.()}
onDisappear={() => options?.onDisappear?.()}
onHeaderBackButtonClicked={() =>
removeScene(route, 1, routeNavigationProp)
}
onDismissed={(e) =>
removeScene(
route,
Expand Down
8 changes: 8 additions & 0 deletions src/native-stack/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ export type NativeStackNavigationOptions = {
* Boolean indicating whether the navigation bar is translucent.
*/
headerTranslucent?: boolean;
/**
* Boolean indicating whether, when the Android default back button is clicked, the `pop` action should be performed on the native side or on the JS side to be able to prevent it.
* Unfortunately the same behavior is not available on iOS since the behavior of native back button cannot be changed there.
* Defaults to `false`.
*
* @platform android
*/
nativeBackButtonDismissalEnabled?: boolean;
/**
* How should the screen replacing another screen animate. Defaults to `pop`.
* The following values are currently supported:
Expand Down
13 changes: 11 additions & 2 deletions src/native-stack/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const RouteView = ({
fullScreenSwipeEnabled,
gestureEnabled,
headerShown,
nativeBackButtonDismissalEnabled = false,
replaceAnimation = 'pop',
screenOrientation,
stackAnimation,
Expand Down Expand Up @@ -189,11 +190,12 @@ const RouteView = ({
<Screen
key={route.key}
enabled
style={StyleSheet.absoluteFill}
isNativeStack
style={StyleSheet.absoluteFill}
customAnimationOnSwipe={customAnimationOnSwipe}
gestureEnabled={isAndroid ? false : gestureEnabled}
fullScreenSwipeEnabled={fullScreenSwipeEnabled}
gestureEnabled={isAndroid ? false : gestureEnabled}
nativeBackButtonDismissalEnabled={nativeBackButtonDismissalEnabled}
replaceAnimation={replaceAnimation}
screenOrientation={screenOrientation}
stackAnimation={stackAnimation}
Expand All @@ -203,6 +205,13 @@ const RouteView = ({
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
statusBarTranslucent={statusBarTranslucent}
onHeaderBackButtonClicked={() => {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
target: stateKey,
});
}}
onWillAppear={() => {
navigation.emit({
type: 'transitionStart',
Expand Down
Loading