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

Commit

Permalink
feat: add hook to scroll to top on tab press
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Aug 24, 2019
1 parent 469ec31 commit 9e1104c
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 41 deletions.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Navigators bundle a router and a view which takes the navigation state and decid
A simple navigator could look like this:

```js
import { createNavigator } from '@react-navigation/core';

function StackNavigator({ initialRouteName, children, ...rest }) {
// The `navigation` object contains the navigation state and some helpers (e.g. push, pop)
// The `descriptors` object contains the screen options and a helper for rendering a screen
Expand Down Expand Up @@ -127,8 +129,11 @@ It's also possible to disable bubbling of actions when dispatching them by addin
## Basic usage

```js
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Stack = createStackNavigator();
const Tab = createTabNavigator();
const Tab = createBottomTabNavigator();

function App() {
return (
Expand Down Expand Up @@ -210,6 +215,8 @@ function Profile({ navigation }) {
}
```

The `navigation.addListener` method returns a function to remove the listener which can be returned as the cleanup function in an effect.

Navigators can also emit custom events using the `emit` method in the `navigation` object passed:

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

```js
import { useFocusEffect } from '@react-navigation/core';

function Profile({ userId }) {
const [user, setUser] = React.useState(null);

Expand Down Expand Up @@ -272,6 +281,10 @@ The `useFocusEffect` is analogous to React's `useEffect` hook. The only differen
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:

```js
import { useIsFocused } from '@react-navigation/core';

// ...

const isFocused = useIsFocused();
```

Expand All @@ -284,13 +297,35 @@ For proper UX in React Native, we need to respect platform behavior such as the
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:

```js
import { useBackButton } from '@react-navigation/native';

// ...

const ref = React.useRef();

useBackButton(ref);

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

### Scroll to top on tab button press

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:

```js
import { useScrollToTop } from '@react-navigation/native';

// ...

const ref = React.useRef();

useScrollToTop(ref);

return <ScrollView ref={ref}>{/* content */}</ScrollView>;
```

The hook can accept a ref object to any view that has a `scrollTo` method.

### Deep-link integration

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

```js
import { useLinking } from '@react-navigation/native';

// ...

const ref = React.useRef();

const { getInitialState } = useLinking(ref, {
Expand Down
4 changes: 0 additions & 4 deletions packages/bottom-tabs/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import {
import { TabNavigationState } from '@react-navigation/routers';

export type BottomTabNavigationEventMap = {
/**
* Event which fires on tapping on the tab for an already focused screen.
*/
refocus: undefined;
/**
* Event which fires on tapping on the tab in the tab bar.
*/
Expand Down
10 changes: 4 additions & 6 deletions packages/bottom-tabs/src/views/BottomTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,10 @@ export default class BottomTabView extends React.Component<Props, State> {
target: route.key,
});

if (state.routes[state.index].key === route.key) {
navigation.emit({
type: 'refocus',
target: route.key,
});
} else if (!event.defaultPrevented) {
if (
state.routes[state.index].key !== route.key &&
!event.defaultPrevented
) {
navigation.dispatch({
...BaseActions.navigate(route.name),
target: state.key,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export type EventMapBase = {
blur: undefined;
};

export type EventArg<EventName extends string, Data> = {
export type EventArg<EventName extends string, Data = undefined> = {
/**
* Type of the event (e.g. `focus`, `blur`)
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/material-bottom-tabs/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
import { TabNavigationState } from '@react-navigation/routers';

export type MaterialBottomTabNavigationEventMap = {
refocus: undefined;
/**
* Event which fires on tapping on the tab in the tab bar.
*/
tabPress: undefined;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,12 @@ export default class MaterialBottomTabView extends React.PureComponent<Props> {
};

private handleTabPress = ({ route }: Scene) => {
const { state, navigation } = this.props;
const { navigation } = this.props;

navigation.emit({
type: 'tabPress',
target: route.key,
});

if (state.routes[state.index].key === route.key) {
navigation.emit({
type: 'refocus',
target: route.key,
});
}
};

private renderIcon = ({
Expand Down
4 changes: 0 additions & 4 deletions packages/material-top-tabs/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import {
import { TabNavigationState } from '@react-navigation/routers';

export type MaterialTopTabNavigationEventMap = {
/**
* Event which fires on tapping on the tab for an already focused screen.
*/
refocus: undefined;
/**
* Event which fires on tapping on the tab in the tab bar.
*/
Expand Down
8 changes: 0 additions & 8 deletions packages/material-top-tabs/src/views/MaterialTopTabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
route: Route<string>;
preventDefault: () => void;
}) => {
const { state, navigation } = this.props;
const event = this.props.navigation.emit({
type: 'tabPress',
target: route.key,
Expand All @@ -82,13 +81,6 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
if (event.defaultPrevented) {
preventDefault();
}

if (state.routes[state.index].key === route.key) {
navigation.emit({
type: 'refocus',
target: route.key,
});
}
};

private handleTabLongPress = ({ route }: { route: Route<string> }) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/native/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as NativeContainer } from './NativeContainer';
export { default as useBackButton } from './useBackButton';
export { default as useLinking } from './useLinking';
export { default as NativeContainer } from './NativeContainer';
export { default as useScrollToTop } from './useScrollToTop';
28 changes: 28 additions & 0 deletions packages/native/src/useScrollToTop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from 'react';
import { useNavigation, EventArg } from '@react-navigation/core';

type ScrollableView = {
scrollTo(options: { x?: number; y?: number; animated?: boolean }): void;
};

export default function useScrollToTop(ref: React.RefObject<ScrollableView>) {
const navigation = useNavigation();

React.useEffect(
() =>
// @ts-ignore
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (navigation.isFocused() && !e.defaultPrevented && ref.current) {
// When user taps on already focused tab, scroll to top
ref.current.scrollTo({ y: 0 });
}
});
}),
[navigation, ref]
);
}
24 changes: 17 additions & 7 deletions packages/stack/src/navigators/createStackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,23 @@ function StackNavigator({
React.useEffect(
() =>
navigation.addListener &&
navigation.addListener('refocus', (e: EventArg<'refocus', undefined>) => {
if (state.index > 0 && !e.defaultPrevented) {
navigation.dispatch({
...StackActions.popToTop(),
target: state.key,
});
}
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (
state.index > 0 &&
navigation.isFocused() &&
!e.defaultPrevented
) {
// When user taps on already focused tab and we're inside the tab,
// reset the stack to replicate native behaviour
navigation.dispatch({
...StackActions.popToTop(),
target: state.key,
});
}
});
}),
[navigation, state.index, state.key]
);
Expand Down

0 comments on commit 9e1104c

Please sign in to comment.