Skip to content

Commit

Permalink
feat: make useScrollToTop work when nesting multiple tab navigators (#…
Browse files Browse the repository at this point in the history
…11063)

**Motivation**

This PR answers this open issue:
#11045

When tapping on a bottom tab, the screen scrolls back to the top.
However, when adding a second tab navigator at the top of the screen,
tapping the bottom tap does not automatically scroll to top anymore.
Only tapping the top navigation tab will scroll the screen up. It is
expected that the bottom tab tap will still scroll the screen to the
top, just like tapping the status bar.

**Context**

This issue has previously been reported
[here](#8586)
by [@iirovi](https://github.com/iirovi), and a pull request to fix the
issue has been proposed by
[@Gregoirevda](https://github.com/Gregoirevda)
[here](#9434).
Recent changes on main automatically closed the pull request, so I'm
posting his solution here again at @satya164's
[request](#9434 (comment)).

We tested this solution on our app and it works perfectly well.
Previously, tapping the bottom tab would not scroll back to the top when
a top tab bar was present, but this fixes the issue. You can find the
package versions in [the open
issue](#11045).


**Behavior example**

Twitter has both top and bottom navigation bars. When tapping on the
bottom tap, the screen scrolls back to the top. Tapping the top tab also
scroll to the top.

<img
src="https://user-images.githubusercontent.com/4307396/205150557-64787dfc-ed77-4a2f-88f3-205b05b6aead.mp4"
width="300">

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
  • Loading branch information
3 people committed Dec 7, 2022
1 parent 31cb2a2 commit e391595
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 39 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test": "jest",
"clean": "lerna run clean",
"build": "lerna run prepack",
"release": "lerna publish",
"release": "lerna run publish",
"example": "yarn workspace @react-navigation/example"
},
"engines": {
Expand Down
85 changes: 47 additions & 38 deletions packages/native/src/useScrollToTop.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventArg, useNavigation, useRoute } from '@react-navigation/core';
import { EventArg, NavigationProp, useNavigation, useRoute } from '@react-navigation/core';
import * as React from 'react';

type ScrollOptions = { x?: number; y?: number; animated?: boolean };
Expand Down Expand Up @@ -49,53 +49,62 @@ export default function useScrollToTop(
const route = useRoute();

React.useEffect(() => {
let current = navigation;
let tabNavigations: NavigationProp<ReactNavigation.RootParamList>[] = [];
let currentNavigation = navigation;

// The screen might be inside another navigator such as stack nested in tabs
// We need to find the closest tab navigator and add the listener there
while (current && current.getState().type !== 'tab') {
current = current.getParent();
// If the screen is nested inside multiple tab navigators, we should scroll to top for any of them
// So we need to find all the parent tab navigators and add the listeners there
while (currentNavigation) {
if (currentNavigation.getState().type === 'tab') {
tabNavigations.push(currentNavigation);
}

currentNavigation = currentNavigation.getParent();
}

if (!current) {
if (tabNavigations.length === 0) {
return;
}

const unsubscribe = current.addListener(
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
// @ts-expect-error
'tabPress',
(e: EventArg<'tabPress', true>) => {
// We should scroll to top only when the screen is focused
const isFocused = navigation.isFocused();
const unsubscribers = tabNavigations.map((tab) => {
return tab.addListener(
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
// @ts-expect-error
'tabPress',
(e: EventArg<'tabPress', true>) => {
// We should scroll to top only when the screen is focused
const isFocused = navigation.isFocused();

// In a nested stack navigator, tab press resets the stack to first screen
// So we should scroll to top only when we are on first screen
const isFirst =
navigation === current ||
navigation.getState().routes[0].key === route.key;
// In a nested stack navigator, tab press resets the stack to first screen
// So we should scroll to top only when we are on first screen
const isFirst =
tabNavigations.includes(navigation) ||
navigation.getState().routes[0].key === route.key;

// 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(() => {
const scrollable = getScrollableNode(ref) as ScrollableWrapper;
// 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(() => {
const scrollable = getScrollableNode(ref) as ScrollableWrapper;

if (isFocused && isFirst && scrollable && !e.defaultPrevented) {
if ('scrollToTop' in scrollable) {
scrollable.scrollToTop();
} else if ('scrollTo' in scrollable) {
scrollable.scrollTo({ x: 0, y: 0, animated: true });
} else if ('scrollToOffset' in scrollable) {
scrollable.scrollToOffset({ offset: 0, animated: true });
} else if ('scrollResponderScrollTo' in scrollable) {
scrollable.scrollResponderScrollTo({ y: 0, animated: true });
if (isFocused && isFirst && scrollable && !e.defaultPrevented) {
if ('scrollToTop' in scrollable) {
scrollable.scrollToTop();
} else if ('scrollTo' in scrollable) {
scrollable.scrollTo({ y: 0, animated: true });
} else if ('scrollToOffset' in scrollable) {
scrollable.scrollToOffset({ offset: 0, animated: true });
} else if ('scrollResponderScrollTo' in scrollable) {
scrollable.scrollResponderScrollTo({ y: 0, animated: true });
}
}
}
});
}
);
});
}
);
});

return unsubscribe;
return () => {
unsubscribers.forEach((unsubscribe) => unsubscribe());
};
}, [navigation, ref, route.key]);
}

0 comments on commit e391595

Please sign in to comment.