react-native-tab-view 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +4 -0
  2. package/lib/commonjs/PagerViewAdapter.js +25 -8
  3. package/lib/commonjs/PagerViewAdapter.js.map +1 -1
  4. package/lib/commonjs/PanResponderAdapter.js +29 -20
  5. package/lib/commonjs/PanResponderAdapter.js.map +1 -1
  6. package/lib/commonjs/SceneMap.js +9 -12
  7. package/lib/commonjs/SceneMap.js.map +1 -1
  8. package/lib/commonjs/SceneView.js +54 -101
  9. package/lib/commonjs/SceneView.js.map +1 -1
  10. package/lib/commonjs/TabBar.js +327 -327
  11. package/lib/commonjs/TabBar.js.map +1 -1
  12. package/lib/commonjs/TabBarIndicator.js +81 -99
  13. package/lib/commonjs/TabBarIndicator.js.map +1 -1
  14. package/lib/commonjs/TabBarItem.js +184 -161
  15. package/lib/commonjs/TabBarItem.js.map +1 -1
  16. package/lib/commonjs/TabView.js +3 -1
  17. package/lib/commonjs/TabView.js.map +1 -1
  18. package/lib/commonjs/types.js.map +1 -1
  19. package/lib/module/PagerViewAdapter.js +25 -8
  20. package/lib/module/PagerViewAdapter.js.map +1 -1
  21. package/lib/module/PanResponderAdapter.js +29 -19
  22. package/lib/module/PanResponderAdapter.js.map +1 -1
  23. package/lib/module/SceneMap.js +9 -14
  24. package/lib/module/SceneMap.js.map +1 -1
  25. package/lib/module/SceneView.js +53 -98
  26. package/lib/module/SceneView.js.map +1 -1
  27. package/lib/module/TabBar.js +323 -323
  28. package/lib/module/TabBar.js.map +1 -1
  29. package/lib/module/TabBarIndicator.js +74 -92
  30. package/lib/module/TabBarIndicator.js.map +1 -1
  31. package/lib/module/TabBarItem.js +178 -154
  32. package/lib/module/TabBarItem.js.map +1 -1
  33. package/lib/module/TabView.js +3 -1
  34. package/lib/module/TabView.js.map +1 -1
  35. package/lib/module/types.js.map +1 -1
  36. package/lib/typescript/PagerViewAdapter.d.ts +1 -1
  37. package/lib/typescript/PanResponderAdapter.d.ts +1 -1
  38. package/lib/typescript/SceneMap.d.ts +5 -3
  39. package/lib/typescript/SceneView.d.ts +1 -18
  40. package/lib/typescript/TabBar.d.ts +7 -38
  41. package/lib/typescript/TabBarIndicator.d.ts +2 -10
  42. package/lib/typescript/TabBarItem.d.ts +3 -5
  43. package/lib/typescript/TabView.d.ts +1 -1
  44. package/lib/typescript/types.d.ts +1 -0
  45. package/package.json +4 -1
  46. package/src/PagerViewAdapter.tsx +29 -10
  47. package/src/PanResponderAdapter.tsx +29 -20
  48. package/src/SceneMap.tsx +11 -7
  49. package/src/SceneView.tsx +70 -106
  50. package/src/TabBar.tsx +451 -391
  51. package/src/TabBarIndicator.tsx +108 -114
  52. package/src/TabBarItem.tsx +214 -185
  53. package/src/TabView.tsx +2 -0
  54. package/src/types.tsx +1 -0
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ /// <reference types="react" />
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
3
  import type { Route, SceneRendererProps, NavigationState } from './types';
4
4
  export declare type GetTabWidth = (index: number) => number;
@@ -9,12 +9,4 @@ export declare type Props<T extends Route> = SceneRendererProps & {
9
9
  getTabWidth: GetTabWidth;
10
10
  gap?: number;
11
11
  };
12
- export default class TabBarIndicator<T extends Route> extends React.Component<Props<T>> {
13
- componentDidMount(): void;
14
- componentDidUpdate(): void;
15
- private fadeInIndicator;
16
- private isIndicatorShown;
17
- private opacity;
18
- private getTranslateX;
19
- render(): JSX.Element;
20
- }
12
+ export default function TabBarIndicator<T extends Route>({ getTabWidth, layout, navigationState, position, width, gap, style, }: Props<T>): JSX.Element;
@@ -27,11 +27,9 @@ export declare type Props<T extends Route> = {
27
27
  onLayout?: (event: LayoutChangeEvent) => void;
28
28
  onPress: () => void;
29
29
  onLongPress: () => void;
30
+ defaultTabWidth?: number;
30
31
  labelStyle?: StyleProp<TextStyle>;
31
32
  style: StyleProp<ViewStyle>;
32
33
  };
33
- export default class TabBarItem<T extends Route> extends React.Component<Props<T>> {
34
- private getActiveOpacity;
35
- private getInactiveOpacity;
36
- render(): JSX.Element;
37
- }
34
+ declare function TabBarItem<T extends Route>(props: Props<T>): JSX.Element;
35
+ export default TabBarItem;
@@ -23,4 +23,4 @@ export declare type Props<T extends Route> = PagerProps & {
23
23
  pagerStyle?: StyleProp<ViewStyle>;
24
24
  style?: StyleProp<ViewStyle>;
25
25
  };
26
- export default function TabView<T extends Route>({ onIndexChange, navigationState, renderScene, initialLayout, keyboardDismissMode, lazy, lazyPreloadDistance, onSwipeStart, onSwipeEnd, renderLazyPlaceholder, renderTabBar, sceneContainerStyle, pagerStyle, style, swipeEnabled, tabBarPosition, }: Props<T>): JSX.Element;
26
+ export default function TabView<T extends Route>({ onIndexChange, navigationState, renderScene, initialLayout, keyboardDismissMode, lazy, lazyPreloadDistance, onSwipeStart, onSwipeEnd, renderLazyPlaceholder, renderTabBar, sceneContainerStyle, pagerStyle, style, swipeEnabled, tabBarPosition, animationEnabled, }: Props<T>): JSX.Element;
@@ -35,6 +35,7 @@ export declare type EventEmitterProps = {
35
35
  export declare type PagerProps = Omit<PagerViewProps, 'initialPage' | 'scrollEnabled' | 'onPageScroll' | 'onPageSelected' | 'onPageScrollStateChanged' | 'keyboardDismissMode' | 'children'> & {
36
36
  keyboardDismissMode?: 'none' | 'on-drag' | 'auto';
37
37
  swipeEnabled?: boolean;
38
+ animationEnabled?: boolean;
38
39
  onSwipeStart?: () => void;
39
40
  onSwipeEnd?: () => void;
40
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tab-view",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Tab view component for React Native",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "react-native": "src/index.tsx",
@@ -97,5 +97,8 @@
97
97
  }
98
98
  ]
99
99
  ]
100
+ },
101
+ "dependencies": {
102
+ "use-latest-callback": "^0.1.5"
100
103
  }
101
104
  }
@@ -41,6 +41,7 @@ export default function PagerViewAdapter<T extends Route>({
41
41
  onSwipeEnd,
42
42
  children,
43
43
  style,
44
+ animationEnabled,
44
45
  ...rest
45
46
  }: Props<T>) {
46
47
  const { index } = navigationState;
@@ -58,13 +59,21 @@ export default function PagerViewAdapter<T extends Route>({
58
59
  navigationStateRef.current = navigationState;
59
60
  });
60
61
 
61
- const jumpTo = React.useCallback((key: string) => {
62
- const index = navigationStateRef.current.routes.findIndex(
63
- (route: { key: string }) => route.key === key
64
- );
65
-
66
- pagerRef.current?.setPage(index);
67
- }, []);
62
+ const jumpTo = React.useCallback(
63
+ (key: string) => {
64
+ const index = navigationStateRef.current.routes.findIndex(
65
+ (route: { key: string }) => route.key === key
66
+ );
67
+
68
+ if (animationEnabled) {
69
+ pagerRef.current?.setPage(index);
70
+ } else {
71
+ pagerRef.current?.setPageWithoutAnimation(index);
72
+ position.setValue(index);
73
+ }
74
+ },
75
+ [animationEnabled, position]
76
+ );
68
77
 
69
78
  React.useEffect(() => {
70
79
  if (keyboardDismissMode === 'auto') {
@@ -72,9 +81,14 @@ export default function PagerViewAdapter<T extends Route>({
72
81
  }
73
82
 
74
83
  if (indexRef.current !== index) {
75
- pagerRef.current?.setPage(index);
84
+ if (animationEnabled) {
85
+ pagerRef.current?.setPage(index);
86
+ } else {
87
+ pagerRef.current?.setPageWithoutAnimation(index);
88
+ position.setValue(index);
89
+ }
76
90
  }
77
- }, [keyboardDismissMode, index]);
91
+ }, [keyboardDismissMode, index, animationEnabled, position]);
78
92
 
79
93
  const onPageScrollStateChanged = (
80
94
  state: PageScrollStateChangedNativeEvent
@@ -115,8 +129,13 @@ export default function PagerViewAdapter<T extends Route>({
115
129
  };
116
130
  }, []);
117
131
 
132
+ const memoizedPosition = React.useMemo(
133
+ () => Animated.add(position, offset),
134
+ [offset, position]
135
+ );
136
+
118
137
  return children({
119
- position: Animated.add(position, offset),
138
+ position: memoizedPosition,
120
139
  addEnterListener,
121
140
  jumpTo,
122
141
  render: (children) => (
@@ -58,6 +58,7 @@ export default function PanResponderAdapter<T extends Route>({
58
58
  onSwipeEnd,
59
59
  children,
60
60
  style,
61
+ animationEnabled = false,
61
62
  }: Props<T>) {
62
63
  const { routes, index } = navigationState;
63
64
 
@@ -76,27 +77,32 @@ export default function PanResponderAdapter<T extends Route>({
76
77
  const swipeDistanceThreshold = layout.width / 1.75;
77
78
 
78
79
  const jumpToIndex = React.useCallback(
79
- (index: number) => {
80
+ (index: number, animate = animationEnabled) => {
80
81
  const offset = -index * layoutRef.current.width;
81
82
 
82
83
  const { timing, ...transitionConfig } = DefaultTransitionSpec;
83
84
 
84
- Animated.parallel([
85
- timing(panX, {
86
- ...transitionConfig,
87
- toValue: offset,
88
- useNativeDriver: false,
89
- }),
90
- ]).start(({ finished }) => {
91
- if (finished) {
92
- onIndexChangeRef.current(index);
93
- pendingIndexRef.current = undefined;
94
- }
95
- });
96
-
97
- pendingIndexRef.current = index;
85
+ if (animate) {
86
+ Animated.parallel([
87
+ timing(panX, {
88
+ ...transitionConfig,
89
+ toValue: offset,
90
+ useNativeDriver: false,
91
+ }),
92
+ ]).start(({ finished }) => {
93
+ if (finished) {
94
+ onIndexChangeRef.current(index);
95
+ pendingIndexRef.current = undefined;
96
+ }
97
+ });
98
+ pendingIndexRef.current = index;
99
+ } else {
100
+ panX.setValue(offset);
101
+ onIndexChangeRef.current(index);
102
+ pendingIndexRef.current = undefined;
103
+ }
98
104
  },
99
- [panX]
105
+ [animationEnabled, panX]
100
106
  );
101
107
 
102
108
  React.useEffect(() => {
@@ -230,7 +236,7 @@ export default function PanResponderAdapter<T extends Route>({
230
236
  nextIndex = currentIndex;
231
237
  }
232
238
 
233
- jumpToIndex(nextIndex);
239
+ jumpToIndex(nextIndex, true);
234
240
  };
235
241
 
236
242
  // TODO: use the listeners
@@ -277,10 +283,13 @@ export default function PanResponderAdapter<T extends Route>({
277
283
  I18nManager.isRTL ? -1 : 1
278
284
  );
279
285
 
286
+ const position = React.useMemo(
287
+ () => (layout.width ? Animated.divide(panX, -layout.width) : null),
288
+ [layout.width, panX]
289
+ );
290
+
280
291
  return children({
281
- position: layout.width
282
- ? Animated.divide(panX, -layout.width)
283
- : new Animated.Value(index),
292
+ position: position ?? new Animated.Value(index),
284
293
  addEnterListener,
285
294
  jumpTo,
286
295
  render: (children) => (
package/src/SceneMap.tsx CHANGED
@@ -1,19 +1,23 @@
1
1
  import * as React from 'react';
2
2
  import type { SceneRendererProps } from './types';
3
3
 
4
- class SceneComponent<
5
- T extends { component: React.ComponentType<any> }
6
- > extends React.PureComponent<T> {
7
- render() {
8
- const { component, ...rest } = this.props;
4
+ type SceneProps = {
5
+ route: any;
6
+ } & Omit<SceneRendererProps, 'layout'>;
7
+
8
+ const SceneComponent = React.memo(
9
+ <T extends { component: React.ComponentType<any> } & SceneProps>({
10
+ component,
11
+ ...rest
12
+ }: T) => {
9
13
  return React.createElement(component, rest);
10
14
  }
11
- }
15
+ );
12
16
 
13
17
  export default function SceneMap<T extends any>(scenes: {
14
18
  [key: string]: React.ComponentType<T>;
15
19
  }) {
16
- return ({ route, jumpTo, position }: SceneRendererProps & { route: any }) => (
20
+ return ({ route, jumpTo, position }: SceneProps) => (
17
21
  <SceneComponent
18
22
  key={route.key}
19
23
  component={scenes[route.key]}
package/src/SceneView.tsx CHANGED
@@ -17,121 +17,85 @@ type Props<T extends Route> = SceneRendererProps &
17
17
  style?: StyleProp<ViewStyle>;
18
18
  };
19
19
 
20
- type State = {
21
- loading: boolean;
22
- };
23
-
24
- export default class SceneView<T extends Route> extends React.Component<
25
- Props<T>,
26
- State
27
- > {
28
- static getDerivedStateFromProps(props: Props<Route>, state: State) {
29
- if (
30
- state.loading &&
31
- Math.abs(props.navigationState.index - props.index) <=
32
- props.lazyPreloadDistance
33
- ) {
34
- // Always render the route when it becomes focused
35
- return { loading: false };
36
- }
37
-
38
- return null;
20
+ export default function SceneView<T extends Route>({
21
+ children,
22
+ navigationState,
23
+ lazy,
24
+ layout,
25
+ index,
26
+ lazyPreloadDistance,
27
+ addEnterListener,
28
+ style,
29
+ }: Props<T>) {
30
+ const [isLoading, setIsLoading] = React.useState(
31
+ Math.abs(navigationState.index - index) > lazyPreloadDistance
32
+ );
33
+
34
+ if (
35
+ isLoading &&
36
+ Math.abs(navigationState.index - index) <= lazyPreloadDistance
37
+ ) {
38
+ // Always render the route when it becomes focused
39
+ setIsLoading(false);
39
40
  }
40
41
 
41
- state = {
42
- loading:
43
- Math.abs(this.props.navigationState.index - this.props.index) >
44
- this.props.lazyPreloadDistance,
45
- };
42
+ React.useEffect(() => {
43
+ const handleEnter = (value: number) => {
44
+ // If we're entering the current route, we need to load it
45
+ if (value === index) {
46
+ setIsLoading((prevState) => {
47
+ if (prevState) {
48
+ return false;
49
+ }
50
+ return prevState;
51
+ });
52
+ }
53
+ };
54
+
55
+ let unsubscribe: (() => void) | undefined;
56
+ let timer: NodeJS.Timeout;
46
57
 
47
- componentDidMount() {
48
- if (this.props.lazy) {
58
+ if (lazy && isLoading) {
49
59
  // If lazy mode is enabled, listen to when we enter screens
50
- this.unsubscribe = this.props.addEnterListener(this.handleEnter);
51
- } else if (this.state.loading) {
60
+ unsubscribe = addEnterListener(handleEnter);
61
+ } else if (isLoading) {
52
62
  // If lazy mode is not enabled, render the scene with a delay if not loaded already
53
63
  // This improves the initial startup time as the scene is no longer blocking
54
- this.timerHandler = setTimeout(
55
- () => this.setState({ loading: false }),
56
- 0
57
- );
64
+ timer = setTimeout(() => setIsLoading(false), 0);
58
65
  }
59
- }
60
66
 
61
- componentDidUpdate(prevProps: Props<T>, prevState: State) {
62
- if (
63
- this.props.lazy !== prevProps.lazy ||
64
- this.state.loading !== prevState.loading
65
- ) {
66
- // We only need the listener if the tab hasn't loaded yet and lazy is enabled
67
- if (this.props.lazy && this.state.loading) {
68
- this.unsubscribe?.();
69
- this.unsubscribe = this.props.addEnterListener(this.handleEnter);
70
- } else {
71
- this.unsubscribe?.();
67
+ return () => {
68
+ unsubscribe?.();
69
+ clearTimeout(timer);
70
+ };
71
+ }, [addEnterListener, index, isLoading, lazy]);
72
+
73
+ const focused = navigationState.index === index;
74
+
75
+ return (
76
+ <View
77
+ accessibilityElementsHidden={!focused}
78
+ importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
79
+ style={[
80
+ styles.route,
81
+ // If we don't have the layout yet, make the focused screen fill the container
82
+ // This avoids delay before we are able to render pages side by side
83
+ layout.width
84
+ ? { width: layout.width }
85
+ : focused
86
+ ? StyleSheet.absoluteFill
87
+ : null,
88
+ style,
89
+ ]}
90
+ >
91
+ {
92
+ // Only render the route only if it's either focused or layout is available
93
+ // When layout is not available, we must not render unfocused routes
94
+ // so that the focused route can fill the screen
95
+ focused || layout.width ? children({ loading: isLoading }) : null
72
96
  }
73
- }
74
- }
75
-
76
- componentWillUnmount() {
77
- this.unsubscribe?.();
78
-
79
- if (this.timerHandler) {
80
- clearTimeout(this.timerHandler);
81
- this.timerHandler = undefined;
82
- }
83
- }
84
-
85
- private timerHandler: NodeJS.Timeout | undefined;
86
-
87
- private unsubscribe: (() => void) | null = null;
88
-
89
- private handleEnter = (value: number) => {
90
- const { index } = this.props;
91
-
92
- // If we're entering the current route, we need to load it
93
- if (value === index) {
94
- this.setState((prevState) => {
95
- if (prevState.loading) {
96
- return { loading: false };
97
- }
98
-
99
- return null;
100
- });
101
- }
102
- };
103
-
104
- render() {
105
- const { navigationState, index, layout, style } = this.props;
106
- const { loading } = this.state;
107
-
108
- const focused = navigationState.index === index;
109
-
110
- return (
111
- <View
112
- accessibilityElementsHidden={!focused}
113
- importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
114
- style={[
115
- styles.route,
116
- // If we don't have the layout yet, make the focused screen fill the container
117
- // This avoids delay before we are able to render pages side by side
118
- layout.width
119
- ? { width: layout.width }
120
- : focused
121
- ? StyleSheet.absoluteFill
122
- : null,
123
- style,
124
- ]}
125
- >
126
- {
127
- // Only render the route only if it's either focused or layout is available
128
- // When layout is not available, we must not render unfocused routes
129
- // so that the focused route can fill the screen
130
- focused || layout.width ? this.props.children({ loading }) : null
131
- }
132
- </View>
133
- );
134
- }
97
+ </View>
98
+ );
135
99
  }
136
100
 
137
101
  const styles = StyleSheet.create({