UNPKG

3.66 kBTypeScriptView Raw
1import { EventArg, useNavigation, useRoute } from '@react-navigation/core';
2import * as React from 'react';
3
4type ScrollOptions = { x?: number; y?: number; animated?: boolean };
5
6type ScrollableView =
7 | { scrollToTop(): void }
8 | { scrollTo(options: ScrollOptions): void }
9 | { scrollToOffset(options: { offset?: number; animated?: boolean }): void }
10 | { scrollResponderScrollTo(options: ScrollOptions): void };
11
12type ScrollableWrapper =
13 | { getScrollResponder(): React.ReactNode }
14 | { getNode(): ScrollableView }
15 | ScrollableView;
16
17function getScrollableNode(ref: React.RefObject<ScrollableWrapper>) {
18 if (ref.current == null) {
19 return null;
20 }
21
22 if (
23 'scrollToTop' in ref.current ||
24 'scrollTo' in ref.current ||
25 'scrollToOffset' in ref.current ||
26 'scrollResponderScrollTo' in ref.current
27 ) {
28 // This is already a scrollable node.
29 return ref.current;
30 } else if ('getScrollResponder' in ref.current) {
31 // If the view is a wrapper like FlatList, SectionList etc.
32 // We need to use `getScrollResponder` to get access to the scroll responder
33 return ref.current.getScrollResponder();
34 } else if ('getNode' in ref.current) {
35 // When a `ScrollView` is wraped in `Animated.createAnimatedComponent`
36 // we need to use `getNode` to get the ref to the actual scrollview.
37 // Note that `getNode` is deprecated in newer versions of react-native
38 // this is why we check if we already have a scrollable node above.
39 return ref.current.getNode();
40 } else {
41 return ref.current;
42 }
43}
44
45export default function useScrollToTop(
46 ref: React.RefObject<ScrollableWrapper>
47) {
48 const navigation = useNavigation();
49 const route = useRoute();
50
51 React.useEffect(() => {
52 let current = navigation;
53
54 // The screen might be inside another navigator such as stack nested in tabs
55 // We need to find the closest tab navigator and add the listener there
56 while (current && current.getState().type !== 'tab') {
57 current = current.getParent();
58 }
59
60 if (!current) {
61 return;
62 }
63
64 const unsubscribe = current.addListener(
65 // We don't wanna import tab types here to avoid extra deps
66 // in addition, there are multiple tab implementations
67 // @ts-expect-error
68 'tabPress',
69 (e: EventArg<'tabPress', true>) => {
70 // We should scroll to top only when the screen is focused
71 const isFocused = navigation.isFocused();
72
73 // In a nested stack navigator, tab press resets the stack to first screen
74 // So we should scroll to top only when we are on first screen
75 const isFirst =
76 navigation === current ||
77 navigation.getState().routes[0].key === route.key;
78
79 // Run the operation in the next frame so we're sure all listeners have been run
80 // This is necessary to know if preventDefault() has been called
81 requestAnimationFrame(() => {
82 const scrollable = getScrollableNode(ref) as ScrollableWrapper;
83
84 if (isFocused && isFirst && scrollable && !e.defaultPrevented) {
85 if ('scrollToTop' in scrollable) {
86 scrollable.scrollToTop();
87 } else if ('scrollTo' in scrollable) {
88 scrollable.scrollTo({ x: 0, y: 0, animated: true });
89 } else if ('scrollToOffset' in scrollable) {
90 scrollable.scrollToOffset({ offset: 0, animated: true });
91 } else if ('scrollResponderScrollTo' in scrollable) {
92 scrollable.scrollResponderScrollTo({ y: 0, animated: true });
93 }
94 }
95 });
96 }
97 );
98
99 return unsubscribe;
100 }, [navigation, ref, route.key]);
101}