react-native-tab-view 4.2.2 → 5.0.0-alpha.1

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 (70) hide show
  1. package/lib/module/Pager.android.js +1 -1
  2. package/lib/module/Pager.android.js.map +1 -1
  3. package/lib/module/Pager.ios.js +1 -1
  4. package/lib/module/Pager.ios.js.map +1 -1
  5. package/lib/module/PagerViewAdapter.js +3 -127
  6. package/lib/module/PagerViewAdapter.js.map +1 -1
  7. package/lib/module/PagerViewAdapter.native.js +130 -0
  8. package/lib/module/PagerViewAdapter.native.js.map +1 -0
  9. package/lib/module/PanResponderAdapter.js +43 -31
  10. package/lib/module/PanResponderAdapter.js.map +1 -1
  11. package/lib/module/PlatformPressable.js.map +1 -1
  12. package/lib/module/SceneView.js +22 -31
  13. package/lib/module/SceneView.js.map +1 -1
  14. package/lib/module/ScrollViewAdapter.js +210 -0
  15. package/lib/module/ScrollViewAdapter.js.map +1 -0
  16. package/lib/module/TabBar.js +98 -61
  17. package/lib/module/TabBar.js.map +1 -1
  18. package/lib/module/TabBarIndicator.js +6 -9
  19. package/lib/module/TabBarIndicator.js.map +1 -1
  20. package/lib/module/TabBarItem.js +4 -4
  21. package/lib/module/TabBarItem.js.map +1 -1
  22. package/lib/module/TabView.js +65 -84
  23. package/lib/module/TabView.js.map +1 -1
  24. package/lib/module/index.js +3 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/useMeasureLayout.js +36 -0
  27. package/lib/module/useMeasureLayout.js.map +1 -0
  28. package/lib/typescript/src/PagerViewAdapter.d.ts +3 -17
  29. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
  30. package/lib/typescript/src/PagerViewAdapter.native.d.ts +6 -0
  31. package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -0
  32. package/lib/typescript/src/PanResponderAdapter.d.ts +3 -17
  33. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
  34. package/lib/typescript/src/PlatformPressable.d.ts +2 -2
  35. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  36. package/lib/typescript/src/SceneView.d.ts +1 -1
  37. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  38. package/lib/typescript/src/ScrollViewAdapter.d.ts +12 -0
  39. package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -0
  40. package/lib/typescript/src/TabBar.d.ts +7 -7
  41. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  42. package/lib/typescript/src/TabBarIndicator.d.ts +1 -1
  43. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  44. package/lib/typescript/src/TabBarItem.d.ts +4 -4
  45. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  46. package/lib/typescript/src/TabBarItemLabel.d.ts +2 -2
  47. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  48. package/lib/typescript/src/TabView.d.ts +99 -5
  49. package/lib/typescript/src/TabView.d.ts.map +1 -1
  50. package/lib/typescript/src/index.d.ts +4 -1
  51. package/lib/typescript/src/index.d.ts.map +1 -1
  52. package/lib/typescript/src/types.d.ts +64 -8
  53. package/lib/typescript/src/types.d.ts.map +1 -1
  54. package/lib/typescript/src/useMeasureLayout.d.ts +5 -0
  55. package/lib/typescript/src/useMeasureLayout.d.ts.map +1 -0
  56. package/package.json +8 -8
  57. package/src/PagerViewAdapter.native.tsx +174 -0
  58. package/src/PagerViewAdapter.tsx +8 -181
  59. package/src/PanResponderAdapter.tsx +64 -77
  60. package/src/PlatformPressable.tsx +2 -1
  61. package/src/SceneView.tsx +23 -45
  62. package/src/ScrollViewAdapter.tsx +268 -0
  63. package/src/TabBar.tsx +134 -133
  64. package/src/TabBarIndicator.tsx +7 -11
  65. package/src/TabBarItem.tsx +8 -6
  66. package/src/TabBarItemLabel.tsx +2 -2
  67. package/src/TabView.tsx +173 -100
  68. package/src/index.tsx +10 -0
  69. package/src/types.tsx +75 -17
  70. package/src/useMeasureLayout.tsx +34 -0
package/src/TabView.tsx CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import {
3
3
  I18nManager,
4
- type LayoutChangeEvent,
5
4
  Platform,
6
5
  type StyleProp,
7
6
  StyleSheet,
@@ -13,35 +12,131 @@ import { Pager } from './Pager';
13
12
  import { SceneView } from './SceneView';
14
13
  import { TabBar } from './TabBar';
15
14
  import type {
16
- Layout,
15
+ AdapterCommonProps,
16
+ AdapterProps,
17
+ EventEmitterProps,
17
18
  LocaleDirection,
18
19
  NavigationState,
19
- PagerProps,
20
20
  Route,
21
21
  SceneRendererProps,
22
22
  TabDescriptor,
23
23
  } from './types';
24
24
 
25
- export type Props<T extends Route> = Omit<PagerProps, 'layoutDirection'> & {
25
+ export type Props<T extends Route> = AdapterCommonProps & {
26
+ /**
27
+ * Callback which is called when the index of the active tab changes.
28
+ * Must update the `navigationState.index` prop to the new index.
29
+ *
30
+ * Example:
31
+ * ```js
32
+ * onIndexChange={(index) => setState({ ...state, index })}
33
+ * ```
34
+ */
26
35
  onIndexChange: (index: number) => void;
36
+ /**
37
+ * Callback which is called when the tab navigation animation ends.
38
+ * Useful for side effects that should run after the tab change animation.
39
+ *
40
+ * Unlike `onIndexChange`, this is called regardless of whether the index changed or not.
41
+ */
27
42
  onTabSelect?: (props: { index: number }) => void;
43
+ /**
44
+ * State for the tab view containing the current index and routes.
45
+ *
46
+ * Example:
47
+ * ```js
48
+ * {
49
+ * index: 0,
50
+ * routes: [
51
+ * { key: 'first' },
52
+ * { key: 'second' },
53
+ * ],
54
+ * }
55
+ * ```
56
+ */
28
57
  navigationState: NavigationState<T>;
58
+ /**
59
+ * Callback which returns a custom placeholder element.
60
+ * The placeholder is shown when a scene is not yet loaded when `lazy` is enabled.
61
+ */
29
62
  renderLazyPlaceholder?: (props: { route: T }) => React.ReactNode;
63
+ /**
64
+ * Callback which returns a custom tab bar element to display.
65
+ */
30
66
  renderTabBar?: (
31
- props: SceneRendererProps & {
32
- navigationState: NavigationState<T>;
33
- options: Record<string, TabDescriptor<T>> | undefined;
34
- }
67
+ props: SceneRendererProps &
68
+ EventEmitterProps & {
69
+ navigationState: NavigationState<T>;
70
+ options: Record<string, TabDescriptor<T>> | undefined;
71
+ }
35
72
  ) => React.ReactNode;
73
+ /**
74
+ * Callback which returns a custom adapter to use for the tab view.
75
+ * Adapters are responsible for handling gestures and animations between tabs.
76
+ *
77
+ * The following adapters are provided out of the box:
78
+ * - `PagerViewAdapter`: Uses `react-native-pager-view` for native experience.
79
+ * - `PanResponderAdapter`: Uses `PanResponder` for a JS-based implementation.
80
+ * - `ScrollViewAdapter`: Uses `ScrollView` for an implementation based on `ScrollView`.
81
+ *
82
+ * Defaults to `PagerViewAdapter` on Android and iOS, and `PanResponderAdapter` on other platforms.
83
+ */
84
+ renderAdapter?: (props: AdapterProps) => React.ReactElement;
85
+ /**
86
+ * Position of the tab bar in the tab view.
87
+ * Defaults to `'top'`.
88
+ */
36
89
  tabBarPosition?: 'top' | 'bottom';
37
- initialLayout?: Partial<Layout>;
90
+ /**
91
+ * Whether to lazily render the scenes.
92
+ * When enabled, scenes are rendered only when they come into view.
93
+ *
94
+ * Can be a boolean or a function that receives the route and returns a boolean.
95
+ * Defaults to `false`.
96
+ */
38
97
  lazy?: ((props: { route: T }) => boolean) | boolean;
98
+ /**
99
+ * How many screens to preload when `lazy` is enabled.
100
+ *
101
+ * Defaults to `0`.
102
+ */
39
103
  lazyPreloadDistance?: number;
104
+ /**
105
+ * The layout direction of the tab view.
106
+ *
107
+ * Defaults to the app's locale direction (RTL or LTR).
108
+ */
40
109
  direction?: LocaleDirection;
110
+ /**
111
+ * Style to apply to the pager container.
112
+ */
41
113
  pagerStyle?: StyleProp<ViewStyle>;
114
+ /**
115
+ * Style to apply to the tab view container.
116
+ */
42
117
  style?: StyleProp<ViewStyle>;
118
+ /**
119
+ * Callback which returns a React element to render for each route.
120
+ */
43
121
  renderScene: (props: SceneRendererProps & { route: T }) => React.ReactNode;
122
+ /**
123
+ * Options for individual tabs, keyed by route key.
124
+ *
125
+ * Example:
126
+ * ```js
127
+ * {
128
+ * first: { labelText: 'First Tab' },
129
+ * second: { labelText: 'Second Tab' },
130
+ * }
131
+ *
132
+ * These options are merged with `commonOptions`.
133
+ */
44
134
  options?: Record<string, TabDescriptor<T>>;
135
+ /**
136
+ * Options that apply to all tabs.
137
+ *
138
+ * Individual tab options from `options` will override these.
139
+ */
45
140
  commonOptions?: TabDescriptor<T>;
46
141
  };
47
142
 
@@ -52,7 +147,6 @@ export function TabView<T extends Route>({
52
147
  onTabSelect,
53
148
  navigationState,
54
149
  renderScene,
55
- initialLayout,
56
150
  keyboardDismissMode = 'auto',
57
151
  lazy = false,
58
152
  lazyPreloadDistance = 0,
@@ -61,13 +155,14 @@ export function TabView<T extends Route>({
61
155
  renderLazyPlaceholder = renderLazyPlaceholderDefault,
62
156
  // eslint-disable-next-line @eslint-react/no-unstable-default-props
63
157
  renderTabBar = (props) => <TabBar {...props} />,
158
+ // eslint-disable-next-line @eslint-react/no-unstable-default-props
159
+ renderAdapter = (props) => <Pager {...props} />,
64
160
  pagerStyle,
65
161
  style,
66
162
  direction = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr',
67
163
  swipeEnabled = true,
68
164
  tabBarPosition = 'top',
69
165
  animationEnabled = true,
70
- overScrollMode,
71
166
  options: sceneOptions,
72
167
  commonOptions,
73
168
  }: Props<T>) {
@@ -82,30 +177,12 @@ export function TabView<T extends Route>({
82
177
  );
83
178
  }
84
179
 
85
- const [layout, setLayout] = React.useState({
86
- width: 0,
87
- height: 0,
88
- ...initialLayout,
89
- });
90
-
91
180
  const jumpToIndex = (index: number) => {
92
181
  if (index !== navigationState.index) {
93
182
  onIndexChange(index);
94
183
  }
95
184
  };
96
185
 
97
- const handleLayout = (e: LayoutChangeEvent) => {
98
- const { height, width } = e.nativeEvent.layout;
99
-
100
- setLayout((prevLayout) => {
101
- if (prevLayout.width === width && prevLayout.height === height) {
102
- return prevLayout;
103
- }
104
-
105
- return { height, width };
106
- });
107
- };
108
-
109
186
  const options = Object.fromEntries(
110
187
  navigationState.routes.map((route) => [
111
188
  route.key,
@@ -116,82 +193,78 @@ export function TabView<T extends Route>({
116
193
  ])
117
194
  );
118
195
 
119
- return (
120
- <View onLayout={handleLayout} style={[styles.pager, style]}>
121
- <Pager
122
- layout={layout}
123
- navigationState={navigationState}
124
- keyboardDismissMode={keyboardDismissMode}
125
- swipeEnabled={swipeEnabled}
126
- onSwipeStart={onSwipeStart}
127
- onSwipeEnd={onSwipeEnd}
128
- onIndexChange={jumpToIndex}
129
- onTabSelect={onTabSelect}
130
- animationEnabled={animationEnabled}
131
- overScrollMode={overScrollMode}
132
- style={pagerStyle}
133
- layoutDirection={direction}
134
- >
135
- {({ position, render, addEnterListener, jumpTo }) => {
136
- // All the props here must not change between re-renders
137
- // This is crucial to optimizing the routes with PureComponent
138
- const sceneRendererProps = {
139
- position,
140
- layout,
141
- jumpTo,
142
- };
196
+ const element = renderAdapter({
197
+ navigationState,
198
+ keyboardDismissMode,
199
+ swipeEnabled,
200
+ onSwipeStart,
201
+ onSwipeEnd,
202
+ onIndexChange: jumpToIndex,
203
+ onTabSelect,
204
+ animationEnabled,
205
+ layoutDirection: direction,
206
+ style: pagerStyle,
207
+ children: ({ position, render, subscribe, jumpTo }) => {
208
+ // All the props here must not change between re-renders
209
+ // This is crucial to optimizing the routes with PureComponent
210
+ const sceneRendererProps = {
211
+ position,
212
+ jumpTo,
213
+ };
143
214
 
144
- return (
145
- <React.Fragment>
146
- {tabBarPosition === 'top' &&
147
- renderTabBar({
148
- ...sceneRendererProps,
149
- options,
150
- navigationState,
151
- })}
152
- {render(
153
- navigationState.routes.map((route, i) => {
154
- const { sceneStyle } = options?.[route.key] ?? {};
215
+ return (
216
+ <React.Fragment>
217
+ {tabBarPosition === 'top' &&
218
+ renderTabBar({
219
+ ...sceneRendererProps,
220
+ subscribe,
221
+ options,
222
+ navigationState,
223
+ })}
224
+ {render(
225
+ navigationState.routes.map((route, i) => {
226
+ const { sceneStyle } = options?.[route.key] ?? {};
155
227
 
156
- return (
157
- <SceneView
158
- key={route.key}
159
- {...sceneRendererProps}
160
- addEnterListener={addEnterListener}
161
- index={i}
162
- lazy={typeof lazy === 'function' ? lazy({ route }) : lazy}
163
- lazyPreloadDistance={lazyPreloadDistance}
164
- navigationState={navigationState}
165
- style={sceneStyle}
166
- >
167
- {({ loading }) =>
168
- loading
169
- ? renderLazyPlaceholder({ route })
170
- : renderScene({
171
- ...sceneRendererProps,
172
- route,
173
- })
174
- }
175
- </SceneView>
176
- );
177
- })
178
- )}
179
- {tabBarPosition === 'bottom' &&
180
- renderTabBar({
181
- ...sceneRendererProps,
182
- options,
183
- navigationState,
184
- })}
185
- </React.Fragment>
186
- );
187
- }}
188
- </Pager>
189
- </View>
190
- );
228
+ return (
229
+ <SceneView
230
+ key={route.key}
231
+ {...sceneRendererProps}
232
+ subscribe={subscribe}
233
+ index={i}
234
+ lazy={typeof lazy === 'function' ? lazy({ route }) : lazy}
235
+ lazyPreloadDistance={lazyPreloadDistance}
236
+ navigationState={navigationState}
237
+ style={sceneStyle}
238
+ >
239
+ {({ loading }) =>
240
+ loading
241
+ ? renderLazyPlaceholder({ route })
242
+ : renderScene({
243
+ ...sceneRendererProps,
244
+ route,
245
+ })
246
+ }
247
+ </SceneView>
248
+ );
249
+ })
250
+ )}
251
+ {tabBarPosition === 'bottom' &&
252
+ renderTabBar({
253
+ ...sceneRendererProps,
254
+ subscribe,
255
+ options,
256
+ navigationState,
257
+ })}
258
+ </React.Fragment>
259
+ );
260
+ },
261
+ });
262
+
263
+ return <View style={[styles.container, style]}>{element}</View>;
191
264
  }
192
265
 
193
266
  const styles = StyleSheet.create({
194
- pager: {
267
+ container: {
195
268
  flex: 1,
196
269
  overflow: 'hidden',
197
270
  },
package/src/index.tsx CHANGED
@@ -1,4 +1,13 @@
1
+ export {
2
+ PagerViewAdapter,
3
+ type PagerViewAdapterProps,
4
+ } from './PagerViewAdapter';
5
+ export { PanResponderAdapter } from './PanResponderAdapter';
1
6
  export { SceneMap } from './SceneMap';
7
+ export {
8
+ ScrollViewAdapter,
9
+ type ScrollViewAdapterProps,
10
+ } from './ScrollViewAdapter';
2
11
  export type { Props as TabBarProps } from './TabBar';
3
12
  export { TabBar } from './TabBar';
4
13
  export type { Props as TabBarIndicatorProps } from './TabBarIndicator';
@@ -8,6 +17,7 @@ export { TabBarItem } from './TabBarItem';
8
17
  export type { Props as TabViewProps } from './TabView';
9
18
  export { TabView } from './TabView';
10
19
  export type {
20
+ AdapterProps,
11
21
  NavigationState,
12
22
  Route,
13
23
  SceneRendererProps,
package/src/types.tsx CHANGED
@@ -1,5 +1,11 @@
1
- import type { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';
2
- import type { PagerViewProps } from 'react-native-pager-view';
1
+ import * as React from 'react';
2
+ import type {
3
+ Animated,
4
+ ColorValue,
5
+ StyleProp,
6
+ TextStyle,
7
+ ViewStyle,
8
+ } from 'react-native';
3
9
 
4
10
  export type TabDescriptor<T extends Route> = {
5
11
  accessibilityLabel?: string;
@@ -12,7 +18,7 @@ export type TabDescriptor<T extends Route> = {
12
18
  route: T;
13
19
  labelText?: string;
14
20
  focused: boolean;
15
- color: string;
21
+ color: ColorValue;
16
22
  allowFontScaling?: boolean;
17
23
  style?: StyleProp<TextStyle>;
18
24
  }) => React.ReactNode;
@@ -20,7 +26,7 @@ export type TabDescriptor<T extends Route> = {
20
26
  icon?: (props: {
21
27
  route: T;
22
28
  focused: boolean;
23
- color: string;
29
+ color: ColorValue;
24
30
  size: number;
25
31
  }) => React.ReactNode;
26
32
  badge?: (props: { route: T }) => React.ReactElement;
@@ -57,31 +63,83 @@ export type Layout = {
57
63
  height: number;
58
64
  };
59
65
 
60
- export type Listener = (value: number) => void;
66
+ export type Listener = (event: { type: 'enter'; index: number }) => void;
61
67
 
62
68
  export type SceneRendererProps = {
63
- layout: Layout;
64
69
  position: Animated.AnimatedInterpolation<number>;
65
70
  jumpTo: (key: string) => void;
66
71
  };
67
72
 
68
73
  export type EventEmitterProps = {
69
- addEnterListener: (listener: Listener) => () => void;
74
+ subscribe: (listener: Listener) => () => void;
70
75
  };
71
76
 
72
- export type PagerProps = Omit<
73
- PagerViewProps,
74
- | 'initialPage'
75
- | 'scrollEnabled'
76
- | 'onPageScroll'
77
- | 'onPageSelected'
78
- | 'onPageScrollStateChanged'
79
- | 'keyboardDismissMode'
80
- | 'children'
81
- > & {
77
+ export type AdapterCommonProps = {
78
+ /**
79
+ * Determines whether the keyboard gets dismissed in response to a drag.
80
+ * - 'auto' (default) - the keyboard is dismissed on drag and tab changes
81
+ * - 'on-drag' - the keyboard is dismissed when a drag begins
82
+ * - 'none' - drags and tab changes do not dismiss the keyboard
83
+ */
82
84
  keyboardDismissMode?: 'none' | 'on-drag' | 'auto';
85
+ /**
86
+ * Whether swiping between tabs is enabled.
87
+ */
83
88
  swipeEnabled?: boolean;
89
+ /**
90
+ * Whether the tab switch animation is enabled.
91
+ * If set to false, the tab switch will happen immediately without animation.
92
+ */
84
93
  animationEnabled?: boolean;
94
+ /**
95
+ * Callback that is called when the swipe gesture starts.
96
+ */
85
97
  onSwipeStart?: () => void;
98
+ /**
99
+ * Callback that is called when the swipe gesture ends.
100
+ */
86
101
  onSwipeEnd?: () => void;
102
+ /**
103
+ * Callback that is called when a tab is selected.
104
+ * This is called regardless of whether the index has changed or not.
105
+ */
106
+ onTabSelect?: (props: { index: number }) => void;
107
+ /**
108
+ * Style for the pager adapter.
109
+ */
110
+ style?: StyleProp<ViewStyle>;
87
111
  };
112
+
113
+ export type AdapterRendererProps = {
114
+ /**
115
+ * Callback to call when the index changes.
116
+ */
117
+ onIndexChange: (index: number) => void;
118
+ /**
119
+ * The current navigation state of the tab view.
120
+ */
121
+ navigationState: NavigationState<Route>;
122
+ /**
123
+ * The writing direction of the layout.
124
+ * This can be 'ltr' or 'rtl' based on tab view's `direction` prop.
125
+ */
126
+ layoutDirection?: LocaleDirection;
127
+ /**
128
+ * Render callback that should render the pages of the tab view.
129
+ */
130
+ children: (
131
+ props: EventEmitterProps & {
132
+ // Animated value which represents the state of current index
133
+ // It can include fractional digits as it represents the intermediate value
134
+ position: Animated.AnimatedInterpolation<number>;
135
+ // Function to actually render the content of the pager
136
+ // The parent component takes care of rendering
137
+ render: (children: React.ReactElement[]) => React.ReactNode;
138
+ // Callback to call when switching the tab
139
+ // The tab switch animation is performed even if the index in state is unchanged
140
+ jumpTo: (key: string) => void;
141
+ }
142
+ ) => React.ReactElement;
143
+ };
144
+
145
+ export type AdapterProps = AdapterRendererProps & AdapterCommonProps;
@@ -0,0 +1,34 @@
1
+ import * as React from 'react';
2
+ import { type LayoutChangeEvent, type View } from 'react-native';
3
+ import useLatestCallback from 'use-latest-callback';
4
+
5
+ import type { Layout } from './types';
6
+
7
+ export function useMeasureLayout(
8
+ ref: React.RefObject<View | null>,
9
+ onMeasure?: (layout: Layout) => void
10
+ ) {
11
+ const [layout, setLayout] = React.useState<Layout>({ width: 0, height: 0 });
12
+
13
+ const onMeasureLatest = useLatestCallback(({ width, height }: Layout) => {
14
+ setLayout((layout) =>
15
+ layout.width === width && layout.height === height
16
+ ? layout
17
+ : { width, height }
18
+ );
19
+
20
+ onMeasure?.({ width, height });
21
+ });
22
+
23
+ React.useLayoutEffect(() => {
24
+ ref.current?.measure((_x, _y, width, height) => {
25
+ onMeasureLatest({ width, height });
26
+ });
27
+ }, [onMeasureLatest, ref]);
28
+
29
+ const onLayout = useLatestCallback((event: LayoutChangeEvent) => {
30
+ onMeasureLatest(event.nativeEvent.layout);
31
+ });
32
+
33
+ return [layout, onLayout] as const;
34
+ }