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
package/src/TabBar.tsx CHANGED
@@ -22,6 +22,7 @@ import type {
22
22
  Layout,
23
23
  Event,
24
24
  } from './types';
25
+ import useAnimatedValue from './useAnimatedValue';
25
26
 
26
27
  export type Props<T extends Route> = SceneRendererProps & {
27
28
  navigationState: NavigationState<T>;
@@ -31,10 +32,10 @@ export type Props<T extends Route> = SceneRendererProps & {
31
32
  inactiveColor?: string;
32
33
  pressColor?: string;
33
34
  pressOpacity?: number;
34
- getLabelText: (scene: Scene<T>) => string | undefined;
35
- getAccessible: (scene: Scene<T>) => boolean | undefined;
36
- getAccessibilityLabel: (scene: Scene<T>) => string | undefined;
37
- getTestID: (scene: Scene<T>) => string | undefined;
35
+ getLabelText?: (scene: Scene<T>) => string | undefined;
36
+ getAccessible?: (scene: Scene<T>) => boolean | undefined;
37
+ getAccessibilityLabel?: (scene: Scene<T>) => string | undefined;
38
+ getTestID?: (scene: Scene<T>) => string | undefined;
38
39
  renderLabel?: (
39
40
  scene: Scene<T> & {
40
41
  focused: boolean;
@@ -48,7 +49,7 @@ export type Props<T extends Route> = SceneRendererProps & {
48
49
  }
49
50
  ) => React.ReactNode;
50
51
  renderBadge?: (scene: Scene<T>) => React.ReactNode;
51
- renderIndicator: (props: IndicatorProps<T>) => React.ReactNode;
52
+ renderIndicator?: (props: IndicatorProps<T>) => React.ReactNode;
52
53
  renderTabBarItem?: (
53
54
  props: TabBarItemProps<T> & { key: string }
54
55
  ) => React.ReactElement;
@@ -63,435 +64,494 @@ export type Props<T extends Route> = SceneRendererProps & {
63
64
  gap?: number;
64
65
  };
65
66
 
66
- type State = {
67
- layout: Layout;
68
- tabWidths: { [key: string]: number };
69
- };
67
+ type FlattenedTabWidth = string | number | undefined;
70
68
 
71
69
  const Separator = ({ width }: { width: number }) => {
72
70
  return <View style={{ width }} />;
73
71
  };
74
72
 
75
- export default class TabBar<T extends Route> extends React.Component<
76
- Props<T>,
77
- State
78
- > {
79
- static defaultProps = {
80
- getLabelText: ({ route }: Scene<Route>) => route.title,
81
- getAccessible: ({ route }: Scene<Route>) =>
82
- typeof route.accessible !== 'undefined' ? route.accessible : true,
83
- getAccessibilityLabel: ({ route }: Scene<Route>) =>
84
- typeof route.accessibilityLabel === 'string'
85
- ? route.accessibilityLabel
86
- : typeof route.title === 'string'
87
- ? route.title
88
- : undefined,
89
- getTestID: ({ route }: Scene<Route>) => route.testID,
90
- renderIndicator: (props: IndicatorProps<Route>) => (
91
- <TabBarIndicator {...props} />
92
- ),
93
- gap: 0,
94
- };
73
+ const getFlattenedTabWidth = (style: StyleProp<ViewStyle>) => {
74
+ const tabStyle = StyleSheet.flatten(style);
95
75
 
96
- state: State = {
97
- layout: { width: 0, height: 0 },
98
- tabWidths: {},
99
- };
76
+ return tabStyle?.width;
77
+ };
78
+
79
+ const getComputedTabWidth = (
80
+ index: number,
81
+ layout: Layout,
82
+ routes: Route[],
83
+ scrollEnabled: boolean | undefined,
84
+ tabWidths: { [key: string]: number },
85
+ flattenedWidth: FlattenedTabWidth
86
+ ) => {
87
+ if (flattenedWidth === 'auto') {
88
+ return tabWidths[routes[index].key] || 0;
89
+ }
100
90
 
101
- componentDidUpdate(prevProps: Props<T>, prevState: State) {
102
- const { navigationState } = this.props;
103
- const { layout, tabWidths } = this.state;
104
-
105
- if (
106
- prevProps.navigationState.routes.length !==
107
- navigationState.routes.length ||
108
- prevProps.navigationState.index !== navigationState.index ||
109
- prevState.layout.width !== layout.width ||
110
- prevState.tabWidths !== tabWidths
111
- ) {
112
- if (
113
- this.getFlattenedTabWidth(this.props.tabStyle) === 'auto' &&
114
- !(
115
- layout.width &&
116
- navigationState.routes.every(
117
- (r) => typeof tabWidths[r.key] === 'number'
118
- )
119
- )
120
- ) {
121
- // When tab width is dynamic, only adjust the scroll once we have all tab widths and layout
122
- return;
91
+ switch (typeof flattenedWidth) {
92
+ case 'number':
93
+ return flattenedWidth;
94
+ case 'string':
95
+ if (flattenedWidth.endsWith('%')) {
96
+ const width = parseFloat(flattenedWidth);
97
+ if (Number.isFinite(width)) {
98
+ return layout.width * (width / 100);
99
+ }
123
100
  }
101
+ }
124
102
 
125
- this.resetScroll(navigationState.index);
126
- }
103
+ if (scrollEnabled) {
104
+ return (layout.width / 5) * 2;
127
105
  }
106
+ return layout.width / routes.length;
107
+ };
128
108
 
129
- // to store the layout.width of each tab
130
- // when all onLayout's are fired, this would be set in state
131
- private measuredTabWidths: { [key: string]: number } = {};
109
+ const getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
110
+ tabBarWidth - layoutWidth;
111
+
112
+ const getTranslateX = (
113
+ scrollAmount: Animated.Value,
114
+ maxScrollDistance: number
115
+ ) =>
116
+ Animated.multiply(
117
+ Platform.OS === 'android' && I18nManager.isRTL
118
+ ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
119
+ : scrollAmount,
120
+ I18nManager.isRTL ? 1 : -1
121
+ );
122
+
123
+ const getTabBarWidth = <T extends Route>({
124
+ navigationState,
125
+ layout,
126
+ gap,
127
+ scrollEnabled,
128
+ flattenedTabWidth,
129
+ tabWidths,
130
+ }: Pick<Props<T>, 'navigationState' | 'gap' | 'layout' | 'scrollEnabled'> & {
131
+ tabWidths: Record<string, number>;
132
+ flattenedTabWidth: FlattenedTabWidth;
133
+ }) => {
134
+ const { routes } = navigationState;
135
+
136
+ return routes.reduce<number>(
137
+ (acc, _, i) =>
138
+ acc +
139
+ (i > 0 ? gap ?? 0 : 0) +
140
+ getComputedTabWidth(
141
+ i,
142
+ layout,
143
+ routes,
144
+ scrollEnabled,
145
+ tabWidths,
146
+ flattenedTabWidth
147
+ ),
148
+ 0
149
+ );
150
+ };
132
151
 
133
- private scrollAmount = new Animated.Value(0);
152
+ const normalizeScrollValue = <T extends Route>({
153
+ layout,
154
+ navigationState,
155
+ gap,
156
+ scrollEnabled,
157
+ tabWidths,
158
+ value,
159
+ flattenedTabWidth,
160
+ }: Pick<Props<T>, 'layout' | 'navigationState' | 'gap' | 'scrollEnabled'> & {
161
+ tabWidths: Record<string, number>;
162
+ value: number;
163
+ flattenedTabWidth: FlattenedTabWidth;
164
+ }) => {
165
+ const tabBarWidth = getTabBarWidth({
166
+ layout,
167
+ navigationState,
168
+ tabWidths,
169
+ gap,
170
+ scrollEnabled,
171
+ flattenedTabWidth,
172
+ });
173
+ const maxDistance = getMaxScrollDistance(tabBarWidth, layout.width);
174
+ const scrollValue = Math.max(Math.min(value, maxDistance), 0);
175
+
176
+ if (Platform.OS === 'android' && I18nManager.isRTL) {
177
+ // On Android, scroll value is not applied in reverse in RTL
178
+ // so we need to manually adjust it to apply correct value
179
+ return maxDistance - scrollValue;
180
+ }
134
181
 
135
- private flatListRef = React.createRef<FlatList>();
182
+ return scrollValue;
183
+ };
136
184
 
137
- private getFlattenedTabWidth = (style: StyleProp<ViewStyle>) => {
138
- const tabStyle = StyleSheet.flatten(style);
185
+ const getScrollAmount = <T extends Route>({
186
+ layout,
187
+ navigationState,
188
+ gap,
189
+ scrollEnabled,
190
+ flattenedTabWidth,
191
+ tabWidths,
192
+ }: Pick<Props<T>, 'layout' | 'navigationState' | 'scrollEnabled' | 'gap'> & {
193
+ tabWidths: Record<string, number>;
194
+ flattenedTabWidth: FlattenedTabWidth;
195
+ }) => {
196
+ const centerDistance = Array.from({
197
+ length: navigationState.index + 1,
198
+ }).reduce<number>((total, _, i) => {
199
+ const tabWidth = getComputedTabWidth(
200
+ i,
201
+ layout,
202
+ navigationState.routes,
203
+ scrollEnabled,
204
+ tabWidths,
205
+ flattenedTabWidth
206
+ );
139
207
 
140
- return tabStyle ? tabStyle.width : undefined;
141
- };
208
+ // To get the current index centered we adjust scroll amount by width of indexes
209
+ // 0 through (i - 1) and add half the width of current index i
210
+ return (
211
+ total +
212
+ (navigationState.index === i
213
+ ? (tabWidth + (gap ?? 0)) / 2
214
+ : tabWidth + (gap ?? 0))
215
+ );
216
+ }, 0);
217
+
218
+ const scrollAmount = centerDistance - layout.width / 2;
219
+
220
+ return normalizeScrollValue({
221
+ layout,
222
+ navigationState,
223
+ tabWidths,
224
+ value: scrollAmount,
225
+ gap,
226
+ scrollEnabled,
227
+ flattenedTabWidth,
228
+ });
229
+ };
142
230
 
143
- private getComputedTabWidth = (
144
- index: number,
145
- layout: Layout,
146
- routes: Route[],
147
- scrollEnabled: boolean | undefined,
148
- tabWidths: { [key: string]: number },
149
- flattenedWidth: string | number | undefined
150
- ) => {
151
- if (flattenedWidth === 'auto') {
152
- return tabWidths[routes[index].key] || 0;
231
+ const getLabelTextDefault = ({ route }: Scene<Route>) => route.title;
232
+
233
+ const getAccessibleDefault = ({ route }: Scene<Route>) =>
234
+ typeof route.accessible !== 'undefined' ? route.accessible : true;
235
+
236
+ const getAccessibilityLabelDefault = ({ route }: Scene<Route>) =>
237
+ typeof route.accessibilityLabel === 'string'
238
+ ? route.accessibilityLabel
239
+ : typeof route.title === 'string'
240
+ ? route.title
241
+ : undefined;
242
+
243
+ const renderIndicatorDefault = (props: IndicatorProps<Route>) => (
244
+ <TabBarIndicator {...props} />
245
+ );
246
+
247
+ const getTestIdDefault = ({ route }: Scene<Route>) => route.testID;
248
+
249
+ export default function TabBar<T extends Route>({
250
+ getLabelText = getLabelTextDefault,
251
+ getAccessible = getAccessibleDefault,
252
+ getAccessibilityLabel = getAccessibilityLabelDefault,
253
+ getTestID = getTestIdDefault,
254
+ renderIndicator = renderIndicatorDefault,
255
+ gap = 0,
256
+ scrollEnabled,
257
+ jumpTo,
258
+ navigationState,
259
+ position,
260
+ activeColor,
261
+ bounces,
262
+ contentContainerStyle,
263
+ inactiveColor,
264
+ indicatorContainerStyle,
265
+ indicatorStyle,
266
+ labelStyle,
267
+ onTabLongPress,
268
+ onTabPress,
269
+ pressColor,
270
+ pressOpacity,
271
+ renderBadge,
272
+ renderIcon,
273
+ renderLabel,
274
+ renderTabBarItem,
275
+ style,
276
+ tabStyle,
277
+ }: Props<T>) {
278
+ const [layout, setLayout] = React.useState<Layout>({ width: 0, height: 0 });
279
+ const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
280
+ const flatListRef = React.useRef<FlatList>(null);
281
+ const isFirst = React.useRef(true);
282
+ const scrollAmount = useAnimatedValue(0);
283
+ const measuredTabWidths = React.useRef<Record<string, number>>({});
284
+
285
+ const { routes } = navigationState;
286
+ const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
287
+ const isWidthDynamic = flattenedTabWidth === 'auto';
288
+ const scrollOffset = getScrollAmount({
289
+ layout,
290
+ navigationState,
291
+ tabWidths,
292
+ gap,
293
+ scrollEnabled,
294
+ flattenedTabWidth,
295
+ });
296
+
297
+ const hasMeasuredTabWidths =
298
+ Boolean(layout.width) &&
299
+ routes.every((r) => typeof tabWidths[r.key] === 'number');
300
+
301
+ React.useEffect(() => {
302
+ if (isFirst.current) {
303
+ isFirst.current = false;
304
+ return;
153
305
  }
154
306
 
155
- switch (typeof flattenedWidth) {
156
- case 'number':
157
- return flattenedWidth;
158
- case 'string':
159
- if (flattenedWidth.endsWith('%')) {
160
- const width = parseFloat(flattenedWidth);
161
- if (Number.isFinite(width)) {
162
- return layout.width * (width / 100);
163
- }
164
- }
307
+ if (isWidthDynamic && !hasMeasuredTabWidths) {
308
+ // When tab width is dynamic, only adjust the scroll once we have all tab widths and layout
309
+ return;
165
310
  }
166
311
 
167
312
  if (scrollEnabled) {
168
- return (layout.width / 5) * 2;
169
- }
170
- return layout.width / routes.length;
171
- };
172
-
173
- private getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
174
- tabBarWidth - layoutWidth;
175
-
176
- private getTabBarWidth = (props: Props<T>, state: State) => {
177
- const { layout, tabWidths } = state;
178
- const { scrollEnabled, tabStyle } = props;
179
- const { routes } = props.navigationState;
180
-
181
- return routes.reduce<number>(
182
- (acc, _, i) =>
183
- acc +
184
- (i > 0 ? props.gap ?? 0 : 0) +
185
- this.getComputedTabWidth(
186
- i,
187
- layout,
188
- routes,
189
- scrollEnabled,
190
- tabWidths,
191
- this.getFlattenedTabWidth(tabStyle)
192
- ),
193
- 0
194
- );
195
- };
196
-
197
- private normalizeScrollValue = (
198
- props: Props<T>,
199
- state: State,
200
- value: number
201
- ) => {
202
- const { layout } = state;
203
- const tabBarWidth = this.getTabBarWidth(props, state);
204
- const maxDistance = this.getMaxScrollDistance(tabBarWidth, layout.width);
205
- const scrollValue = Math.max(Math.min(value, maxDistance), 0);
206
-
207
- if (Platform.OS === 'android' && I18nManager.isRTL) {
208
- // On Android, scroll value is not applied in reverse in RTL
209
- // so we need to manually adjust it to apply correct value
210
- return maxDistance - scrollValue;
211
- }
212
-
213
- return scrollValue;
214
- };
215
-
216
- private getScrollAmount = (props: Props<T>, state: State, index: number) => {
217
- const { layout, tabWidths } = state;
218
- const { scrollEnabled, tabStyle } = props;
219
- const { routes } = props.navigationState;
220
-
221
- const centerDistance = Array.from({ length: index + 1 }).reduce<number>(
222
- (total, _, i) => {
223
- const tabWidth = this.getComputedTabWidth(
224
- i,
225
- layout,
226
- routes,
227
- scrollEnabled,
228
- tabWidths,
229
- this.getFlattenedTabWidth(tabStyle)
230
- );
231
-
232
- // To get the current index centered we adjust scroll amount by width of indexes
233
- // 0 through (i - 1) and add half the width of current index i
234
- return (
235
- total +
236
- (index === i
237
- ? (tabWidth + (props.gap ?? 0)) / 2
238
- : tabWidth + (props.gap ?? 0))
239
- );
240
- },
241
- 0
242
- );
243
-
244
- const scrollAmount = centerDistance - layout.width / 2;
245
-
246
- return this.normalizeScrollValue(props, state, scrollAmount);
247
- };
248
-
249
- private resetScroll = (index: number) => {
250
- if (this.props.scrollEnabled) {
251
- this.flatListRef.current?.scrollToOffset({
252
- offset: this.getScrollAmount(this.props, this.state, index),
313
+ flatListRef.current?.scrollToOffset({
314
+ offset: scrollOffset,
253
315
  animated: true,
254
316
  });
255
317
  }
256
- };
318
+ }, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
257
319
 
258
- private handleLayout = (e: LayoutChangeEvent) => {
320
+ const handleLayout = (e: LayoutChangeEvent) => {
259
321
  const { height, width } = e.nativeEvent.layout;
260
322
 
261
- if (
262
- this.state.layout.width === width &&
263
- this.state.layout.height === height
264
- ) {
265
- return;
266
- }
267
-
268
- this.setState({
269
- layout: {
270
- height,
271
- width,
272
- },
273
- });
323
+ setLayout((layout) =>
324
+ layout.width === width && layout.height === height
325
+ ? layout
326
+ : { width, height }
327
+ );
274
328
  };
275
329
 
276
- private getTranslateX = (
277
- scrollAmount: Animated.Value,
278
- maxScrollDistance: number
279
- ) =>
280
- Animated.multiply(
281
- Platform.OS === 'android' && I18nManager.isRTL
282
- ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
283
- : scrollAmount,
284
- I18nManager.isRTL ? 1 : -1
285
- );
330
+ const tabBarWidth = getTabBarWidth({
331
+ layout,
332
+ navigationState,
333
+ tabWidths,
334
+ gap,
335
+ scrollEnabled,
336
+ flattenedTabWidth,
337
+ });
338
+
339
+ const separatorsWidth = Math.max(0, routes.length - 1) * gap;
340
+ const separatorPercent = (separatorsWidth / tabBarWidth) * 100;
341
+ const tabBarWidthPercent = `${routes.length * 40}%`;
342
+
343
+ const translateX = React.useMemo(
344
+ () =>
345
+ getTranslateX(
346
+ scrollAmount,
347
+ getMaxScrollDistance(tabBarWidth, layout.width)
348
+ ),
349
+ [layout.width, scrollAmount, tabBarWidth]
350
+ );
351
+
352
+ const renderItem = React.useCallback(
353
+ ({ item: route, index }: ListRenderItemInfo<T>) => {
354
+ const props: TabBarItemProps<T> & { key: string } = {
355
+ key: route.key,
356
+ position: position,
357
+ route: route,
358
+ navigationState: navigationState,
359
+ getAccessibilityLabel: getAccessibilityLabel,
360
+ getAccessible: getAccessible,
361
+ getLabelText: getLabelText,
362
+ getTestID: getTestID,
363
+ renderBadge: renderBadge,
364
+ renderIcon: renderIcon,
365
+ renderLabel: renderLabel,
366
+ activeColor: activeColor,
367
+ inactiveColor: inactiveColor,
368
+ pressColor: pressColor,
369
+ pressOpacity: pressOpacity,
370
+ onLayout: isWidthDynamic
371
+ ? (e: LayoutChangeEvent) => {
372
+ measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
373
+
374
+ // When we have measured widths for all of the tabs, we should updates the state
375
+ // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
376
+ if (
377
+ routes.every(
378
+ (r) => typeof measuredTabWidths.current[r.key] === 'number'
379
+ )
380
+ ) {
381
+ setTabWidths({ ...measuredTabWidths.current });
382
+ }
383
+ }
384
+ : undefined,
385
+ onPress: () => {
386
+ const event: Scene<T> & Event = {
387
+ route,
388
+ defaultPrevented: false,
389
+ preventDefault: () => {
390
+ event.defaultPrevented = true;
391
+ },
392
+ };
393
+
394
+ onTabPress?.(event);
395
+
396
+ if (event.defaultPrevented) {
397
+ return;
398
+ }
286
399
 
287
- render() {
288
- const {
289
- position,
290
- navigationState,
291
- jumpTo,
292
- scrollEnabled,
293
- bounces,
400
+ jumpTo(route.key);
401
+ },
402
+ onLongPress: () => onTabLongPress?.({ route }),
403
+ labelStyle: labelStyle,
404
+ style: tabStyle,
405
+ // Calculate the deafult width for tab for FlatList to work
406
+ defaultTabWidth: !isWidthDynamic
407
+ ? getComputedTabWidth(
408
+ index,
409
+ layout,
410
+ routes,
411
+ scrollEnabled,
412
+ tabWidths,
413
+ getFlattenedTabWidth(tabStyle)
414
+ )
415
+ : undefined,
416
+ };
417
+
418
+ return (
419
+ <>
420
+ {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
421
+ {renderTabBarItem ? (
422
+ renderTabBarItem(props)
423
+ ) : (
424
+ <TabBarItem {...props} />
425
+ )}
426
+ </>
427
+ );
428
+ },
429
+ [
430
+ activeColor,
431
+ gap,
294
432
  getAccessibilityLabel,
295
433
  getAccessible,
296
434
  getLabelText,
297
435
  getTestID,
436
+ inactiveColor,
437
+ isWidthDynamic,
438
+ jumpTo,
439
+ labelStyle,
440
+ layout,
441
+ navigationState,
442
+ onTabLongPress,
443
+ onTabPress,
444
+ position,
445
+ pressColor,
446
+ pressOpacity,
298
447
  renderBadge,
299
448
  renderIcon,
300
449
  renderLabel,
301
450
  renderTabBarItem,
302
- activeColor,
303
- inactiveColor,
304
- pressColor,
305
- pressOpacity,
306
- onTabPress,
307
- onTabLongPress,
451
+ routes,
452
+ scrollEnabled,
308
453
  tabStyle,
309
- labelStyle,
310
- indicatorStyle,
454
+ tabWidths,
455
+ ]
456
+ );
457
+
458
+ const keyExtractor = React.useCallback((item: T) => item.key, []);
459
+
460
+ const contentContainerStyleMemoized = React.useMemo(
461
+ () => [
462
+ styles.tabContent,
463
+ scrollEnabled
464
+ ? {
465
+ width:
466
+ tabBarWidth > separatorsWidth ? tabBarWidth : tabBarWidthPercent,
467
+ }
468
+ : styles.container,
311
469
  contentContainerStyle,
312
- style,
313
- indicatorContainerStyle,
314
- gap = 0,
315
- } = this.props;
316
- const { layout, tabWidths } = this.state;
317
- const { routes } = navigationState;
318
-
319
- const isWidthDynamic = this.getFlattenedTabWidth(tabStyle) === 'auto';
320
- const tabBarWidth = this.getTabBarWidth(this.props, this.state);
321
- const separatorsWidth = Math.max(0, routes.length - 1) * gap;
322
- const separatorPercent = (separatorsWidth / tabBarWidth) * 100;
323
-
324
- const tabBarWidthPercent = `${routes.length * 40}%`;
325
- const translateX = this.getTranslateX(
326
- this.scrollAmount,
327
- this.getMaxScrollDistance(tabBarWidth, layout.width)
328
- );
329
-
330
- return (
470
+ ],
471
+ [
472
+ contentContainerStyle,
473
+ scrollEnabled,
474
+ separatorsWidth,
475
+ tabBarWidth,
476
+ tabBarWidthPercent,
477
+ ]
478
+ );
479
+
480
+ const handleScroll = React.useMemo(
481
+ () =>
482
+ Animated.event(
483
+ [
484
+ {
485
+ nativeEvent: {
486
+ contentOffset: { x: scrollAmount },
487
+ },
488
+ },
489
+ ],
490
+ { useNativeDriver: true }
491
+ ),
492
+ [scrollAmount]
493
+ );
494
+
495
+ return (
496
+ <Animated.View onLayout={handleLayout} style={[styles.tabBar, style]}>
331
497
  <Animated.View
332
- onLayout={this.handleLayout}
333
- style={[styles.tabBar, style]}
498
+ pointerEvents="none"
499
+ style={[
500
+ styles.indicatorContainer,
501
+ scrollEnabled ? { transform: [{ translateX }] as any } : null,
502
+ tabBarWidth > separatorsWidth
503
+ ? { width: tabBarWidth - separatorsWidth }
504
+ : scrollEnabled
505
+ ? { width: tabBarWidthPercent }
506
+ : null,
507
+ indicatorContainerStyle,
508
+ ]}
334
509
  >
335
- <Animated.View
336
- pointerEvents="none"
337
- style={[
338
- styles.indicatorContainer,
339
- scrollEnabled ? { transform: [{ translateX }] as any } : null,
340
- tabBarWidth > separatorsWidth
341
- ? { width: tabBarWidth - separatorsWidth }
342
- : scrollEnabled
343
- ? { width: tabBarWidthPercent }
344
- : null,
345
- indicatorContainerStyle,
346
- ]}
347
- >
348
- {this.props.renderIndicator({
349
- position,
350
- layout,
351
- navigationState,
352
- jumpTo,
353
- width: isWidthDynamic
354
- ? 'auto'
355
- : `${(100 - separatorPercent) / routes.length}%`,
356
- style: indicatorStyle,
357
- getTabWidth: (i: number) =>
358
- this.getComputedTabWidth(
359
- i,
360
- layout,
361
- routes,
362
- scrollEnabled,
363
- tabWidths,
364
- this.getFlattenedTabWidth(tabStyle)
365
- ),
366
- gap,
367
- })}
368
- </Animated.View>
369
- <View style={styles.scroll}>
370
- <Animated.FlatList
371
- data={routes as Animated.WithAnimatedValue<T>[]}
372
- keyExtractor={(item) => item.key}
373
- horizontal
374
- accessibilityRole="tablist"
375
- keyboardShouldPersistTaps="handled"
376
- scrollEnabled={scrollEnabled}
377
- bounces={bounces}
378
- alwaysBounceHorizontal={false}
379
- scrollsToTop={false}
380
- showsHorizontalScrollIndicator={false}
381
- showsVerticalScrollIndicator={false}
382
- automaticallyAdjustContentInsets={false}
383
- overScrollMode="never"
384
- contentContainerStyle={[
385
- styles.tabContent,
386
- scrollEnabled
387
- ? {
388
- width:
389
- tabBarWidth > separatorsWidth
390
- ? tabBarWidth
391
- : tabBarWidthPercent,
392
- }
393
- : styles.container,
394
- contentContainerStyle,
395
- ]}
396
- scrollEventThrottle={16}
397
- renderItem={({ item: route, index }: ListRenderItemInfo<T>) => {
398
- const props: TabBarItemProps<T> & { key: string } = {
399
- key: route.key,
400
- position: position,
401
- route: route,
402
- navigationState: navigationState,
403
- getAccessibilityLabel: getAccessibilityLabel,
404
- getAccessible: getAccessible,
405
- getLabelText: getLabelText,
406
- getTestID: getTestID,
407
- renderBadge: renderBadge,
408
- renderIcon: renderIcon,
409
- renderLabel: renderLabel,
410
- activeColor: activeColor,
411
- inactiveColor: inactiveColor,
412
- pressColor: pressColor,
413
- pressOpacity: pressOpacity,
414
- onLayout: isWidthDynamic
415
- ? (e) => {
416
- this.measuredTabWidths[route.key] =
417
- e.nativeEvent.layout.width;
418
-
419
- // When we have measured widths for all of the tabs, we should updates the state
420
- // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
421
- if (
422
- routes.every(
423
- (r) =>
424
- typeof this.measuredTabWidths[r.key] === 'number'
425
- )
426
- ) {
427
- this.setState({
428
- tabWidths: { ...this.measuredTabWidths },
429
- });
430
- }
431
- }
432
- : undefined,
433
- onPress: () => {
434
- const event: Scene<T> & Event = {
435
- route,
436
- defaultPrevented: false,
437
- preventDefault: () => {
438
- event.defaultPrevented = true;
439
- },
440
- };
441
-
442
- onTabPress?.(event);
443
-
444
- if (event.defaultPrevented) {
445
- return;
446
- }
447
-
448
- this.props.jumpTo(route.key);
449
- },
450
- onLongPress: () => onTabLongPress?.({ route }),
451
- labelStyle: labelStyle,
452
- style: [
453
- tabStyle,
454
- // Calculate the deafult width for tab for FlatList to work.
455
- this.getFlattenedTabWidth(tabStyle) === undefined && {
456
- width: this.getComputedTabWidth(
457
- index,
458
- layout,
459
- routes,
460
- scrollEnabled,
461
- tabWidths,
462
- this.getFlattenedTabWidth(tabStyle)
463
- ),
464
- },
465
- ],
466
- };
467
-
468
- return (
469
- <React.Fragment key={route.key}>
470
- {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
471
- {renderTabBarItem ? (
472
- renderTabBarItem(props)
473
- ) : (
474
- <TabBarItem {...props} />
475
- )}
476
- </React.Fragment>
477
- );
478
- }}
479
- onScroll={Animated.event(
480
- [
481
- {
482
- nativeEvent: {
483
- contentOffset: { x: this.scrollAmount },
484
- },
485
- },
486
- ],
487
- { useNativeDriver: true }
488
- )}
489
- ref={this.flatListRef}
490
- />
491
- </View>
510
+ {renderIndicator({
511
+ position,
512
+ layout,
513
+ navigationState,
514
+ jumpTo,
515
+ width: isWidthDynamic
516
+ ? 'auto'
517
+ : `${(100 - separatorPercent) / routes.length}%`,
518
+ style: indicatorStyle,
519
+ getTabWidth: (i: number) =>
520
+ getComputedTabWidth(
521
+ i,
522
+ layout,
523
+ routes,
524
+ scrollEnabled,
525
+ tabWidths,
526
+ flattenedTabWidth
527
+ ),
528
+ gap,
529
+ })}
492
530
  </Animated.View>
493
- );
494
- }
531
+ <View style={styles.scroll}>
532
+ <Animated.FlatList
533
+ data={routes as Animated.WithAnimatedValue<T>[]}
534
+ keyExtractor={keyExtractor}
535
+ horizontal
536
+ accessibilityRole="tablist"
537
+ keyboardShouldPersistTaps="handled"
538
+ scrollEnabled={scrollEnabled}
539
+ bounces={bounces}
540
+ alwaysBounceHorizontal={false}
541
+ scrollsToTop={false}
542
+ showsHorizontalScrollIndicator={false}
543
+ showsVerticalScrollIndicator={false}
544
+ automaticallyAdjustContentInsets={false}
545
+ overScrollMode="never"
546
+ contentContainerStyle={contentContainerStyleMemoized}
547
+ scrollEventThrottle={16}
548
+ renderItem={renderItem}
549
+ onScroll={handleScroll}
550
+ ref={flatListRef}
551
+ />
552
+ </View>
553
+ </Animated.View>
554
+ );
495
555
  }
496
556
 
497
557
  const styles = StyleSheet.create({