Skip to content

Commit e9da86e

Browse files
authored
feat: add canGoBack (facebook#50)
1 parent f3b6d1f commit e9da86e

File tree

8 files changed

+214
-7
lines changed

8 files changed

+214
-7
lines changed

packages/core/src/BaseRouter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ const BaseRouter = {
7878
shouldActionChangeFocus(action: CommonAction) {
7979
return action.type === 'NAVIGATE';
8080
},
81+
82+
canGoBack() {
83+
return false;
84+
},
8185
};
8286

8387
export default BaseRouter;

packages/core/src/NavigationContainer.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,7 @@ const Container = React.forwardRef(function NavigationContainer(
8888

8989
const dispatch = (action: NavigationAction) =>
9090
context.performTransaction(() => {
91-
for (let i = 0; i < actionListeners.length; i++) {
92-
actionListeners[i](action, undefined, null);
93-
}
91+
actionListeners[0](action, undefined, null);
9492
});
9593

9694
React.useImperativeHandle(ref, () => ({

packages/core/src/__tests__/__fixtures__/MockRouter.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ export default function MockRouter(options: DefaultRouterOptions) {
9797
return false;
9898
},
9999

100-
actionCreators: {},
100+
canGoBack() {
101+
return false;
102+
},
101103
};
102104

103105
return router;

packages/core/src/__tests__/useDescriptors.test.tsx

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { render, act } from 'react-native-testing-library';
33
import useNavigationBuilder from '../useNavigationBuilder';
44
import NavigationContainer from '../NavigationContainer';
55
import Screen from '../Screen';
6-
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
7-
import { NavigationState } from '../types';
6+
import MockRouter, {
7+
MockActions,
8+
MockRouterKey,
9+
} from './__fixtures__/MockRouter';
10+
import { DefaultRouterOptions, NavigationState, Router } from '../types';
811

912
jest.useFakeTimers();
1013

@@ -54,6 +57,180 @@ it('sets options with options prop as an object', () => {
5457
`);
5558
});
5659

60+
it("returns correct value for canGoBack when it's not overridden", () => {
61+
const TestNavigator = (props: any) => {
62+
const { state, descriptors } = useNavigationBuilder<
63+
NavigationState,
64+
{ title?: string },
65+
any
66+
>(MockRouter, props);
67+
const { render, options } = descriptors[state.routes[state.index].key];
68+
69+
return (
70+
<main>
71+
<h1>{options.title}</h1>
72+
<div>{render()}</div>
73+
</main>
74+
);
75+
};
76+
77+
let result = true;
78+
79+
const TestScreen = ({ navigation }: any): any => {
80+
React.useEffect(() => {
81+
result = navigation.canGoBack();
82+
});
83+
84+
return null;
85+
};
86+
87+
const root = (
88+
<NavigationContainer>
89+
<TestNavigator>
90+
<Screen
91+
name="foo"
92+
component={TestScreen}
93+
options={{ title: 'Hello world' }}
94+
/>
95+
<Screen name="bar" component={jest.fn()} />
96+
</TestNavigator>
97+
</NavigationContainer>
98+
);
99+
100+
render(root).update(root);
101+
102+
expect(result).toEqual(false);
103+
});
104+
105+
it("returns correct value for canGoBack when it's overridden", () => {
106+
function ParentRouter(options: DefaultRouterOptions) {
107+
const CurrentMockRouter = MockRouter(options);
108+
const ChildRouter: Router<NavigationState, MockActions> = {
109+
...CurrentMockRouter,
110+
111+
canGoBack() {
112+
return true;
113+
},
114+
};
115+
return ChildRouter;
116+
}
117+
118+
const ParentNavigator = (props: any) => {
119+
const { state, descriptors } = useNavigationBuilder<
120+
NavigationState,
121+
{ title?: string },
122+
any
123+
>(ParentRouter, props);
124+
return descriptors[state.routes[state.index].key].render();
125+
};
126+
127+
const ChildNavigator = (props: any) => {
128+
const { state, descriptors } = useNavigationBuilder<
129+
NavigationState,
130+
{ title?: string },
131+
any
132+
>(MockRouter, props);
133+
134+
return descriptors[state.routes[state.index].key].render();
135+
};
136+
137+
let result = false;
138+
139+
const TestScreen = ({ navigation }: any): any => {
140+
React.useEffect(() => {
141+
result = navigation.canGoBack();
142+
});
143+
144+
return null;
145+
};
146+
147+
const root = (
148+
<NavigationContainer>
149+
<ParentNavigator>
150+
<Screen name="baz">
151+
{() => (
152+
<ChildNavigator>
153+
<Screen name="qux" component={TestScreen} />
154+
</ChildNavigator>
155+
)}
156+
</Screen>
157+
</ParentNavigator>
158+
</NavigationContainer>
159+
);
160+
161+
render(root).update(root);
162+
163+
expect(result).toEqual(true);
164+
});
165+
166+
it('returns correct value for canGoBack when parent router overrides it', () => {
167+
function OverrodeRouter(options: DefaultRouterOptions) {
168+
const CurrentMockRouter = MockRouter(options);
169+
const ChildRouter: Router<NavigationState, MockActions> = {
170+
...CurrentMockRouter,
171+
172+
canGoBack() {
173+
return true;
174+
},
175+
};
176+
return ChildRouter;
177+
}
178+
179+
const OverrodeNavigator = (props: any) => {
180+
const { state, descriptors } = useNavigationBuilder<
181+
NavigationState,
182+
{ title?: string },
183+
any
184+
>(OverrodeRouter, props);
185+
return descriptors[state.routes[state.index].key].render();
186+
};
187+
188+
const TestNavigator = (props: any) => {
189+
const { state, descriptors } = useNavigationBuilder<
190+
NavigationState,
191+
{ title?: string },
192+
any
193+
>(MockRouter, props);
194+
195+
return descriptors[state.routes[state.index].key].render();
196+
};
197+
198+
let result = true;
199+
200+
const TestScreen = ({ navigation }: any): any => {
201+
React.useEffect(() => {
202+
result = navigation.canGoBack();
203+
});
204+
205+
return null;
206+
};
207+
208+
const root = (
209+
<NavigationContainer>
210+
<TestNavigator>
211+
<Screen name="baz">
212+
{() => (
213+
<TestNavigator>
214+
<Screen name="qux" component={TestScreen} />
215+
</TestNavigator>
216+
)}
217+
</Screen>
218+
<Screen name="qux">
219+
{() => (
220+
<OverrodeNavigator>
221+
<Screen name="qux" component={() => null} />
222+
</OverrodeNavigator>
223+
)}
224+
</Screen>
225+
</TestNavigator>
226+
</NavigationContainer>
227+
);
228+
229+
render(root).update(root);
230+
231+
expect(result).toEqual(false);
232+
});
233+
57234
it('sets options with options prop as a fuction', () => {
58235
const TestNavigator = (props: any) => {
59236
const { state, descriptors } = useNavigationBuilder<

packages/core/src/types.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ export type Router<
154154
*/
155155
shouldActionChangeFocus(action: NavigationAction): boolean;
156156

157+
/**
158+
* Whether the back action will be handled by navigation
159+
*
160+
* @param state State object to check.
161+
*/
162+
canGoBack(state: State): boolean;
163+
157164
/**
158165
* Action creators for the router.
159166
*/
@@ -264,6 +271,12 @@ type NavigationHelpersCommon<
264271
* To get notified of focus changes, use `addListener('focus', cb)` and `addListener('blur', cb)`.
265272
*/
266273
isFocused(): boolean;
274+
275+
/**
276+
* Check if dispatching back action will be handled by navigation.
277+
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
278+
*/
279+
canGoBack(): boolean;
267280
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
268281

269282
export type NavigationHelpers<

packages/core/src/useNavigationBuilder.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export default function useNavigationBuilder<
198198
getState,
199199
setState,
200200
emitter,
201+
router,
201202
actionCreators: router.actionCreators,
202203
});
203204

packages/core/src/useNavigationHelpers.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
NavigationState,
1111
ActionCreators,
1212
ParamListBase,
13+
Router,
1314
} from './types';
1415

1516
type Options<Action extends NavigationAction> = {
@@ -21,6 +22,7 @@ type Options<Action extends NavigationAction> = {
2122
setState: (state: NavigationState) => void;
2223
actionCreators?: ActionCreators<Action>;
2324
emitter: NavigationEventEmitter;
25+
router: Router<NavigationState, Action>;
2426
};
2527

2628
export default function useNavigationHelpers<Action extends NavigationAction>({
@@ -29,6 +31,7 @@ export default function useNavigationHelpers<Action extends NavigationAction>({
2931
setState,
3032
actionCreators,
3133
emitter,
34+
router,
3235
}: Options<Action>) {
3336
const parentNavigationHelpers = React.useContext(NavigationContext);
3437
const { performTransaction } = React.useContext(NavigationStateContext);
@@ -68,14 +71,19 @@ export default function useNavigationHelpers<Action extends NavigationAction>({
6871
isFocused: parentNavigationHelpers
6972
? parentNavigationHelpers.isFocused
7073
: () => true,
74+
canGoBack: () =>
75+
router.canGoBack(getState()) ||
76+
(parentNavigationHelpers && parentNavigationHelpers.canGoBack()) ||
77+
false,
7178
};
7279
}, [
7380
actionCreators,
81+
router,
82+
getState,
7483
parentNavigationHelpers,
7584
emitter.emit,
7685
performTransaction,
7786
setState,
78-
getState,
7987
onAction,
8088
]);
8189
}

packages/routers/src/StackRouter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ export default function StackRouter(options: StackRouterOptions) {
225225
}
226226
},
227227

228+
canGoBack(state) {
229+
return state.routes.length > 1;
230+
},
231+
228232
actionCreators: StackActions,
229233
};
230234

0 commit comments

Comments
 (0)