diff --git a/README.md b/README.md index ed60a80f..30f7b5d9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 ( @@ -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 @@ -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); @@ -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(); ``` @@ -284,6 +297,10 @@ 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); @@ -291,6 +308,24 @@ useBackButton(ref); return {/* content */}; ``` +### 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 {/* content */}; +``` + +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: @@ -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, { diff --git a/packages/bottom-tabs/src/types.tsx b/packages/bottom-tabs/src/types.tsx index 1613fb99..60aeb760 100644 --- a/packages/bottom-tabs/src/types.tsx +++ b/packages/bottom-tabs/src/types.tsx @@ -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. */ diff --git a/packages/bottom-tabs/src/views/BottomTabView.tsx b/packages/bottom-tabs/src/views/BottomTabView.tsx index 9d288dc2..092c1431 100644 --- a/packages/bottom-tabs/src/views/BottomTabView.tsx +++ b/packages/bottom-tabs/src/views/BottomTabView.tsx @@ -138,12 +138,10 @@ export default class BottomTabView extends React.Component { 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, diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 52c6f812..edd6a00c 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -187,7 +187,7 @@ export type EventMapBase = { blur: undefined; }; -export type EventArg = { +export type EventArg = { /** * Type of the event (e.g. `focus`, `blur`) */ diff --git a/packages/material-bottom-tabs/src/types.tsx b/packages/material-bottom-tabs/src/types.tsx index 432176b8..51270a2b 100644 --- a/packages/material-bottom-tabs/src/types.tsx +++ b/packages/material-bottom-tabs/src/types.tsx @@ -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; }; diff --git a/packages/material-bottom-tabs/src/views/MaterialBottomTabView.tsx b/packages/material-bottom-tabs/src/views/MaterialBottomTabView.tsx index f39b3ea5..44b0f37f 100644 --- a/packages/material-bottom-tabs/src/views/MaterialBottomTabView.tsx +++ b/packages/material-bottom-tabs/src/views/MaterialBottomTabView.tsx @@ -62,19 +62,12 @@ export default class MaterialBottomTabView extends React.PureComponent { }; 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 = ({ diff --git a/packages/material-top-tabs/src/types.tsx b/packages/material-top-tabs/src/types.tsx index 1088f659..5ee0730a 100644 --- a/packages/material-top-tabs/src/types.tsx +++ b/packages/material-top-tabs/src/types.tsx @@ -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. */ diff --git a/packages/material-top-tabs/src/views/MaterialTopTabView.tsx b/packages/material-top-tabs/src/views/MaterialTopTabView.tsx index 8df4f603..bc8fb2f2 100644 --- a/packages/material-top-tabs/src/views/MaterialTopTabView.tsx +++ b/packages/material-top-tabs/src/views/MaterialTopTabView.tsx @@ -73,7 +73,6 @@ export default class MaterialTopTabView extends React.PureComponent { route: Route; preventDefault: () => void; }) => { - const { state, navigation } = this.props; const event = this.props.navigation.emit({ type: 'tabPress', target: route.key, @@ -82,13 +81,6 @@ export default class MaterialTopTabView extends React.PureComponent { if (event.defaultPrevented) { preventDefault(); } - - if (state.routes[state.index].key === route.key) { - navigation.emit({ - type: 'refocus', - target: route.key, - }); - } }; private handleTabLongPress = ({ route }: { route: Route }) => { diff --git a/packages/native/src/index.tsx b/packages/native/src/index.tsx index 9de4c02e..f383652c 100644 --- a/packages/native/src/index.tsx +++ b/packages/native/src/index.tsx @@ -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'; diff --git a/packages/native/src/useScrollToTop.tsx b/packages/native/src/useScrollToTop.tsx new file mode 100644 index 00000000..e8b65f10 --- /dev/null +++ b/packages/native/src/useScrollToTop.tsx @@ -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) { + 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] + ); +} diff --git a/packages/stack/src/navigators/createStackNavigator.tsx b/packages/stack/src/navigators/createStackNavigator.tsx index bd22923d..7c3591c2 100644 --- a/packages/stack/src/navigators/createStackNavigator.tsx +++ b/packages/stack/src/navigators/createStackNavigator.tsx @@ -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] );