Skip to content
This repository was archived by the owner on Nov 3, 2025. It is now read-only.

Commit f3d69aa

Browse files
committed
✨ (back) add back support on Transitioners
1 parent f934cc4 commit f3d69aa

7 files changed

Lines changed: 89 additions & 26 deletions

File tree

jest/setup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ jest.mock('react-native-reanimated/src/ReanimatedModule', () => ({
1818
}));
1919
jest.mock('react-native-reanimated/src/derived/evaluateOnce');
2020
jest.mock('react-native-reanimated/src/core/AnimatedProps');
21+
22+
jest.useFakeTimers();
Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BackHandler } from 'react-native';
2-
import { fromEventPattern, Observable } from 'rxjs';
3-
import { share } from 'rxjs/operators';
2+
import { fromEventPattern, Observable, Subject } from 'rxjs';
3+
import { share, map, merge, tap } from 'rxjs/operators';
44

55
export interface BackEvent {
66
target: string | null;
@@ -16,21 +16,31 @@ export class BackHandlerDelegate {
1616
* See https://facebook.github.io/react-native/docs/backhandler.html.
1717
* @param handler
1818
*/
19-
private addEventListener = (handler: (event: BackEvent) => void) => {
19+
private addEventListener = (handler: () => void) => {
2020
BackHandler.addEventListener('hardwareBackPress', () => {
21-
handler({
22-
target: null,
23-
});
24-
if (this.onBackCallback) {
25-
this.onBackCallback();
26-
this.onBackCallback = undefined;
27-
return true;
28-
}
29-
return false;
21+
handler();
22+
return true;
3023
});
3124
};
3225

33-
private back$: Observable<BackEvent> = fromEventPattern<BackEvent>(this.addEventListener).pipe(
26+
private softwareBack$ = new Subject<undefined>();
27+
28+
private back$: Observable<BackEvent> = fromEventPattern<undefined>(this.addEventListener).pipe(
29+
merge(this.softwareBack$),
30+
tap(() => {
31+
// After the event has spread, we fire onBacks callbacks.
32+
setTimeout(() => {
33+
if (this.onBackCallback) {
34+
this.onBackCallback();
35+
this.onBackCallback = undefined;
36+
} else {
37+
BackHandler.exitApp();
38+
}
39+
}, 0);
40+
}),
41+
map(() => ({
42+
target: null,
43+
})),
3444
share()
3545
);
3646

@@ -39,5 +49,9 @@ export class BackHandlerDelegate {
3949
this.onBackCallback = cb;
4050
};
4151

52+
back = () => {
53+
this.softwareBack$.next();
54+
};
55+
4256
defaultBackContext = { back$: this.back$ };
4357
}

src/Navigation/Navigation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@ export class Navigation {
1616
backHandlerDelegate = new BackHandlerDelegate();
1717

1818
fullScreenDelegate = new FullScreenDelegate();
19+
20+
back = () => {
21+
this.backHandlerDelegate.back();
22+
};
1923
}

src/Navigation/__tests__/BackHandlerDelegate.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,32 @@ describe('BackHandlerDelegate', () => {
1515
expect.assertions(1);
1616
});
1717

18-
it('fires the registered onBackCallback after the event has been handled', () => {
18+
it('fires the registered onBackCallback after the hardware event has been handled', async () => {
1919
const spy = jest.fn();
2020
const backHandlerDelegate = new BackHandlerDelegate();
2121
backHandlerDelegate.setOnBackCallback(spy);
2222
backHandlerDelegate.defaultBackContext.back$.subscribe();
2323
BackHandler.mockPressBack();
24+
jest.runAllTimers();
25+
expect(spy).toHaveBeenCalled();
26+
});
27+
28+
it('exposes a back method to emit a back event', () => {
29+
const backHandlerDelegate = new BackHandlerDelegate();
30+
backHandlerDelegate.defaultBackContext.back$.subscribe(event => {
31+
expect(event).toStrictEqual({ target: null });
32+
});
33+
backHandlerDelegate.back();
34+
expect.assertions(1);
35+
});
36+
37+
it('fires the registered onBackCallback after the software event has been handled', async () => {
38+
const spy = jest.fn();
39+
const backHandlerDelegate = new BackHandlerDelegate();
40+
backHandlerDelegate.setOnBackCallback(spy);
41+
backHandlerDelegate.defaultBackContext.back$.subscribe();
42+
backHandlerDelegate.back();
43+
jest.runAllTimers();
2444
expect(spy).toHaveBeenCalled();
2545
});
2646
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Navigation } from '../Navigation';
2+
3+
describe('Navigation', () => {
4+
it('exposes the same store instance if it accessed twice', () => {
5+
const navigation = Navigation.instance;
6+
const otherNavigation = Navigation.instance;
7+
expect(otherNavigation).toBe(navigation);
8+
});
9+
10+
it('exposes a back method to emit a back event', () => {
11+
Navigation.instance.backHandlerDelegate.defaultBackContext.back$.subscribe(event => {
12+
expect(event).toStrictEqual({ target: null });
13+
});
14+
Navigation.instance.back();
15+
expect.assertions(1);
16+
});
17+
});

src/Screen.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@ export interface ScreenProps {
1616
visible: boolean;
1717
}
1818

19-
class ScreenComponent extends ReactComponent<WithBackContext<ScreenProps>> {
19+
class ScreenComponent extends ReactComponent<WithBackContext<ScreenProps>, ScreenProps['props']> {
20+
constructor(props: WithBackContext<ScreenProps>) {
21+
super(props);
22+
this.state = props.props;
23+
}
24+
25+
state: ScreenProps['props'];
26+
static getDerivedStateFromProps(
27+
props: WithBackContext<ScreenProps>,
28+
state: ScreenProps['props']
29+
) {
30+
if (!props.visible) return state;
31+
return props.props;
32+
}
33+
2034
static defaultProps = {
2135
isFullScreen: false,
2236
Transitioner: None,
@@ -46,7 +60,7 @@ class ScreenComponent extends ReactComponent<WithBackContext<ScreenProps>> {
4660
factory = createFactory(this.props.Component);
4761

4862
render() {
49-
const { Transitioner, onBack, props } = this.props;
63+
const { Transitioner, onBack } = this.props;
5064
return (
5165
/**
5266
* @todo Transitioner is always defined in static defaultProps.
@@ -58,11 +72,12 @@ class ScreenComponent extends ReactComponent<WithBackContext<ScreenProps>> {
5872
back$: this.back$,
5973
}}
6074
>
61-
{this.factory({ navigation: { goBack: onBack }, ...props })}
75+
{this.factory({ navigation: { goBack: onBack }, ...this.state })}
6276
</BackContext.Provider>
6377
</Transitioner>
6478
);
6579
}
6680
}
6781

82+
// @ts-ignore
6883
export const Screen = withBackContext(ScreenComponent);

src/__tests__/Navigation.test.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)