Skip to content
This repository was archived by the owner on Feb 8, 2020. It is now read-only.

Commit 9e1104c

Browse files
committed
feat: add hook to scroll to top on tab press
1 parent 469ec31 commit 9e1104c

File tree

11 files changed

+96
-41
lines changed

11 files changed

+96
-41
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Navigators bundle a router and a view which takes the navigation state and decid
3131
A simple navigator could look like this:
3232

3333
```js
34+
import { createNavigator } from '@react-navigation/core';
35+
3436
function StackNavigator({ initialRouteName, children, ...rest }) {
3537
// The `navigation` object contains the navigation state and some helpers (e.g. push, pop)
3638
// The `descriptors` object contains the screen options and a helper for rendering a screen
@@ -127,8 +129,11 @@ It's also possible to disable bubbling of actions when dispatching them by addin
127129
## Basic usage
128130

129131
```js
132+
import { createStackNavigator } from '@react-navigation/stack';
133+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
134+
130135
const Stack = createStackNavigator();
131-
const Tab = createTabNavigator();
136+
const Tab = createBottomTabNavigator();
132137

133138
function App() {
134139
return (
@@ -210,6 +215,8 @@ function Profile({ navigation }) {
210215
}
211216
```
212217

218+
The `navigation.addListener` method returns a function to remove the listener which can be returned as the cleanup function in an effect.
219+
213220
Navigators can also emit custom events using the `emit` method in the `navigation` object passed:
214221

215222
```js
@@ -245,6 +252,8 @@ Sometimes we want to run side-effects when a screen is focused. A side effect ma
245252
To make this easier, the library exports a `useFocusEffect` hook:
246253

247254
```js
255+
import { useFocusEffect } from '@react-navigation/core';
256+
248257
function Profile({ userId }) {
249258
const [user, setUser] = React.useState(null);
250259

@@ -272,6 +281,10 @@ The `useFocusEffect` is analogous to React's `useEffect` hook. The only differen
272281
We might want to render different content based on the current focus state of the screen. The library exports a `useIsFocused` hook to make this easier:
273282

274283
```js
284+
import { useIsFocused } from '@react-navigation/core';
285+
286+
// ...
287+
275288
const isFocused = useIsFocused();
276289
```
277290

@@ -284,13 +297,35 @@ For proper UX in React Native, we need to respect platform behavior such as the
284297
When the back button on the device is pressed, we also want to navigate back in the focused navigator. The library exports a `useBackButton` hook to handle this:
285298

286299
```js
300+
import { useBackButton } from '@react-navigation/native';
301+
302+
// ...
303+
287304
const ref = React.useRef();
288305

289306
useBackButton(ref);
290307

291308
return <NavigationContainer ref={ref}>{/* content */}</NavigationContainer>;
292309
```
293310

311+
### Scroll to top on tab button press
312+
313+
When there's a scroll view in a tab and the user taps on the already focused tab bar again, we might want to scroll to top in our scroll view. The library exports a `useScrollToTop` hook to handle this:
314+
315+
```js
316+
import { useScrollToTop } from '@react-navigation/native';
317+
318+
// ...
319+
320+
const ref = React.useRef();
321+
322+
useScrollToTop(ref);
323+
324+
return <ScrollView ref={ref}>{/* content */}</ScrollView>;
325+
```
326+
327+
The hook can accept a ref object to any view that has a `scrollTo` method.
328+
294329
### Deep-link integration
295330

296331
To handle incoming links, we need to handle 2 scenarios:
@@ -325,6 +360,10 @@ For example, the path `/rooms/chat?user=jane` will be translated to a state obje
325360
The `useLinking` hooks makes it easier to handle incoming links:
326361

327362
```js
363+
import { useLinking } from '@react-navigation/native';
364+
365+
// ...
366+
328367
const ref = React.useRef();
329368

330369
const { getInitialState } = useLinking(ref, {

packages/bottom-tabs/src/types.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ import {
1717
import { TabNavigationState } from '@react-navigation/routers';
1818

1919
export type BottomTabNavigationEventMap = {
20-
/**
21-
* Event which fires on tapping on the tab for an already focused screen.
22-
*/
23-
refocus: undefined;
2420
/**
2521
* Event which fires on tapping on the tab in the tab bar.
2622
*/

packages/bottom-tabs/src/views/BottomTabView.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,10 @@ export default class BottomTabView extends React.Component<Props, State> {
138138
target: route.key,
139139
});
140140

141-
if (state.routes[state.index].key === route.key) {
142-
navigation.emit({
143-
type: 'refocus',
144-
target: route.key,
145-
});
146-
} else if (!event.defaultPrevented) {
141+
if (
142+
state.routes[state.index].key !== route.key &&
143+
!event.defaultPrevented
144+
) {
147145
navigation.dispatch({
148146
...BaseActions.navigate(route.name),
149147
target: state.key,

packages/core/src/types.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export type EventMapBase = {
187187
blur: undefined;
188188
};
189189

190-
export type EventArg<EventName extends string, Data> = {
190+
export type EventArg<EventName extends string, Data = undefined> = {
191191
/**
192192
* Type of the event (e.g. `focus`, `blur`)
193193
*/

packages/material-bottom-tabs/src/types.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
import { TabNavigationState } from '@react-navigation/routers';
99

1010
export type MaterialBottomTabNavigationEventMap = {
11-
refocus: undefined;
11+
/**
12+
* Event which fires on tapping on the tab in the tab bar.
13+
*/
1214
tabPress: undefined;
1315
};
1416

packages/material-bottom-tabs/src/views/MaterialBottomTabView.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,12 @@ export default class MaterialBottomTabView extends React.PureComponent<Props> {
6262
};
6363

6464
private handleTabPress = ({ route }: Scene) => {
65-
const { state, navigation } = this.props;
65+
const { navigation } = this.props;
6666

6767
navigation.emit({
6868
type: 'tabPress',
6969
target: route.key,
7070
});
71-
72-
if (state.routes[state.index].key === route.key) {
73-
navigation.emit({
74-
type: 'refocus',
75-
target: route.key,
76-
});
77-
}
7871
};
7972

8073
private renderIcon = ({

packages/material-top-tabs/src/types.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ import {
1010
import { TabNavigationState } from '@react-navigation/routers';
1111

1212
export type MaterialTopTabNavigationEventMap = {
13-
/**
14-
* Event which fires on tapping on the tab for an already focused screen.
15-
*/
16-
refocus: undefined;
1713
/**
1814
* Event which fires on tapping on the tab in the tab bar.
1915
*/

packages/material-top-tabs/src/views/MaterialTopTabView.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
7373
route: Route<string>;
7474
preventDefault: () => void;
7575
}) => {
76-
const { state, navigation } = this.props;
7776
const event = this.props.navigation.emit({
7877
type: 'tabPress',
7978
target: route.key,
@@ -82,13 +81,6 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
8281
if (event.defaultPrevented) {
8382
preventDefault();
8483
}
85-
86-
if (state.routes[state.index].key === route.key) {
87-
navigation.emit({
88-
type: 'refocus',
89-
target: route.key,
90-
});
91-
}
9284
};
9385

9486
private handleTabLongPress = ({ route }: { route: Route<string> }) => {

packages/native/src/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { default as NativeContainer } from './NativeContainer';
12
export { default as useBackButton } from './useBackButton';
23
export { default as useLinking } from './useLinking';
3-
export { default as NativeContainer } from './NativeContainer';
4+
export { default as useScrollToTop } from './useScrollToTop';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from 'react';
2+
import { useNavigation, EventArg } from '@react-navigation/core';
3+
4+
type ScrollableView = {
5+
scrollTo(options: { x?: number; y?: number; animated?: boolean }): void;
6+
};
7+
8+
export default function useScrollToTop(ref: React.RefObject<ScrollableView>) {
9+
const navigation = useNavigation();
10+
11+
React.useEffect(
12+
() =>
13+
// @ts-ignore
14+
// We don't wanna import tab types here to avoid extra deps
15+
// in addition, there are multiple tab implementations
16+
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
17+
// Run the operation in the next frame so we're sure all listeners have been run
18+
// This is necessary to know if preventDefault() has been called
19+
requestAnimationFrame(() => {
20+
if (navigation.isFocused() && !e.defaultPrevented && ref.current) {
21+
// When user taps on already focused tab, scroll to top
22+
ref.current.scrollTo({ y: 0 });
23+
}
24+
});
25+
}),
26+
[navigation, ref]
27+
);
28+
}

0 commit comments

Comments
 (0)