@react-native-ohos/react-native-tab-view 4.0.11-rc.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.OpenSource +11 -0
  3. package/README.md +9 -0
  4. package/lib/module/Pager.android.js +4 -0
  5. package/lib/module/Pager.android.js.map +1 -0
  6. package/lib/module/Pager.ios.js +4 -0
  7. package/lib/module/Pager.ios.js.map +1 -0
  8. package/lib/module/Pager.js +4 -0
  9. package/lib/module/Pager.js.map +1 -0
  10. package/lib/module/PagerViewAdapter.js +126 -0
  11. package/lib/module/PagerViewAdapter.js.map +1 -0
  12. package/lib/module/PanResponderAdapter.js +200 -0
  13. package/lib/module/PanResponderAdapter.js.map +1 -0
  14. package/lib/module/PlatformPressable.js +59 -0
  15. package/lib/module/PlatformPressable.js.map +1 -0
  16. package/lib/module/SceneMap.js +24 -0
  17. package/lib/module/SceneMap.js.map +1 -0
  18. package/lib/module/SceneView.js +73 -0
  19. package/lib/module/SceneView.js.map +1 -0
  20. package/lib/module/TabBar.js +472 -0
  21. package/lib/module/TabBar.js.map +1 -0
  22. package/lib/module/TabBarIndicator.js +122 -0
  23. package/lib/module/TabBarIndicator.js.map +1 -0
  24. package/lib/module/TabBarItem.js +218 -0
  25. package/lib/module/TabBarItem.js.map +1 -0
  26. package/lib/module/TabBarItemLabel.js +33 -0
  27. package/lib/module/TabBarItemLabel.js.map +1 -0
  28. package/lib/module/TabView.js +140 -0
  29. package/lib/module/TabView.js.map +1 -0
  30. package/lib/module/index.js +8 -0
  31. package/lib/module/index.js.map +1 -0
  32. package/lib/module/package.json +1 -0
  33. package/lib/module/types.js +4 -0
  34. package/lib/module/types.js.map +1 -0
  35. package/lib/module/useAnimatedValue.js +12 -0
  36. package/lib/module/useAnimatedValue.js.map +1 -0
  37. package/lib/typescript/package.json +1 -0
  38. package/lib/typescript/src/Pager.android.d.ts +2 -0
  39. package/lib/typescript/src/Pager.android.d.ts.map +1 -0
  40. package/lib/typescript/src/Pager.d.ts +2 -0
  41. package/lib/typescript/src/Pager.d.ts.map +1 -0
  42. package/lib/typescript/src/Pager.ios.d.ts +2 -0
  43. package/lib/typescript/src/Pager.ios.d.ts.map +1 -0
  44. package/lib/typescript/src/PagerViewAdapter.d.ts +15 -0
  45. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -0
  46. package/lib/typescript/src/PanResponderAdapter.d.ts +16 -0
  47. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -0
  48. package/lib/typescript/src/PlatformPressable.d.ts +13 -0
  49. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -0
  50. package/lib/typescript/src/SceneMap.d.ts +10 -0
  51. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  52. package/lib/typescript/src/SceneView.d.ts +16 -0
  53. package/lib/typescript/src/SceneView.d.ts.map +1 -0
  54. package/lib/typescript/src/TabBar.d.ts +32 -0
  55. package/lib/typescript/src/TabBar.d.ts.map +1 -0
  56. package/lib/typescript/src/TabBarIndicator.d.ts +15 -0
  57. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -0
  58. package/lib/typescript/src/TabBarItem.d.ts +19 -0
  59. package/lib/typescript/src/TabBarItem.d.ts.map +1 -0
  60. package/lib/typescript/src/TabBarItemLabel.d.ts +11 -0
  61. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -0
  62. package/lib/typescript/src/TabView.d.ts +30 -0
  63. package/lib/typescript/src/TabView.d.ts.map +1 -0
  64. package/lib/typescript/src/index.d.ts +11 -0
  65. package/lib/typescript/src/index.d.ts.map +1 -0
  66. package/lib/typescript/src/types.d.ts +70 -0
  67. package/lib/typescript/src/types.d.ts.map +1 -0
  68. package/lib/typescript/src/useAnimatedValue.d.ts +3 -0
  69. package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -0
  70. package/package.json +79 -0
  71. package/src/Pager.android.tsx +1 -0
  72. package/src/Pager.ios.tsx +1 -0
  73. package/src/Pager.tsx +1 -0
  74. package/src/PagerViewAdapter.tsx +182 -0
  75. package/src/PanResponderAdapter.tsx +339 -0
  76. package/src/PlatformPressable.tsx +75 -0
  77. package/src/SceneMap.tsx +30 -0
  78. package/src/SceneView.tsx +107 -0
  79. package/src/TabBar.tsx +729 -0
  80. package/src/TabBarIndicator.tsx +190 -0
  81. package/src/TabBarItem.tsx +305 -0
  82. package/src/TabBarItemLabel.tsx +42 -0
  83. package/src/TabView.tsx +195 -0
  84. package/src/index.tsx +15 -0
  85. package/src/types.tsx +87 -0
  86. package/src/useAnimatedValue.tsx +12 -0
package/src/TabBar.tsx ADDED
@@ -0,0 +1,729 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Animated,
4
+ type DimensionValue,
5
+ FlatList,
6
+ I18nManager,
7
+ type LayoutChangeEvent,
8
+ type ListRenderItemInfo,
9
+ Platform,
10
+ type PressableAndroidRippleConfig,
11
+ type StyleProp,
12
+ StyleSheet,
13
+ View,
14
+ type ViewStyle,
15
+ type ViewToken,
16
+ } from 'react-native';
17
+ import useLatestCallback from 'use-latest-callback';
18
+
19
+ import {
20
+ type Props as IndicatorProps,
21
+ TabBarIndicator,
22
+ } from './TabBarIndicator';
23
+ import { type Props as TabBarItemProps, TabBarItem } from './TabBarItem';
24
+ import type {
25
+ Event,
26
+ Layout,
27
+ LocaleDirection,
28
+ NavigationState,
29
+ Route,
30
+ Scene,
31
+ SceneRendererProps,
32
+ TabDescriptor,
33
+ } from './types';
34
+ import { useAnimatedValue } from './useAnimatedValue';
35
+
36
+ export type Props<T extends Route> = SceneRendererProps & {
37
+ navigationState: NavigationState<T>;
38
+ scrollEnabled?: boolean;
39
+ bounces?: boolean;
40
+ activeColor?: string;
41
+ inactiveColor?: string;
42
+ pressColor?: string;
43
+ pressOpacity?: number;
44
+ options?: Record<string, TabDescriptor<T>>;
45
+ renderIndicator?: (props: IndicatorProps<T>) => React.ReactNode;
46
+ renderTabBarItem?: (
47
+ props: TabBarItemProps<T> & { key: string }
48
+ ) => React.ReactElement;
49
+ onTabPress?: (scene: Scene<T> & Event) => void;
50
+ onTabLongPress?: (scene: Scene<T>) => void;
51
+ tabStyle?: StyleProp<ViewStyle>;
52
+ indicatorStyle?: StyleProp<ViewStyle>;
53
+ indicatorContainerStyle?: StyleProp<ViewStyle>;
54
+ contentContainerStyle?: StyleProp<ViewStyle>;
55
+ style?: StyleProp<ViewStyle>;
56
+ direction?: LocaleDirection;
57
+ gap?: number;
58
+ testID?: string;
59
+ android_ripple?: PressableAndroidRippleConfig;
60
+ };
61
+
62
+ const useNativeDriver = Platform.OS !== 'web';
63
+
64
+ const Separator = ({ width }: { width: number }) => {
65
+ return <View style={{ width }} />;
66
+ };
67
+
68
+ const getFlattenedTabWidth = (style: StyleProp<ViewStyle>) => {
69
+ const tabStyle = StyleSheet.flatten(style);
70
+
71
+ return tabStyle?.width;
72
+ };
73
+
74
+ const getFlattenedPaddingStart = (style: StyleProp<ViewStyle>) => {
75
+ const flattenStyle = StyleSheet.flatten(style);
76
+
77
+ return flattenStyle
78
+ ? flattenStyle.paddingLeft ||
79
+ flattenStyle.paddingStart ||
80
+ flattenStyle.paddingHorizontal ||
81
+ 0
82
+ : 0;
83
+ };
84
+
85
+ const getFlattenedPaddingEnd = (style: StyleProp<ViewStyle>) => {
86
+ const flattenStyle = StyleSheet.flatten(style);
87
+
88
+ return flattenStyle
89
+ ? flattenStyle.paddingRight ||
90
+ flattenStyle.paddingEnd ||
91
+ flattenStyle.paddingHorizontal ||
92
+ 0
93
+ : 0;
94
+ };
95
+
96
+ const convertPaddingPercentToSize = (
97
+ value: DimensionValue | undefined,
98
+ layout: Layout
99
+ ): number => {
100
+ switch (typeof value) {
101
+ case 'number':
102
+ return value;
103
+ case 'string':
104
+ if (value.endsWith('%')) {
105
+ const width = parseFloat(value);
106
+ if (Number.isFinite(width)) {
107
+ return layout.width * (width / 100);
108
+ }
109
+ }
110
+ }
111
+ return 0;
112
+ };
113
+
114
+ const getComputedTabWidth = (
115
+ index: number,
116
+ layout: Layout,
117
+ routes: Route[],
118
+ scrollEnabled: boolean | undefined,
119
+ tabWidths: { [key: string]: number },
120
+ flattenedWidth: DimensionValue | undefined,
121
+ flattenedPaddingStart: DimensionValue | undefined,
122
+ flattenedPaddingEnd: DimensionValue | undefined,
123
+ gap?: number
124
+ ) => {
125
+ if (flattenedWidth === 'auto') {
126
+ return tabWidths[routes[index].key] || 0;
127
+ }
128
+
129
+ switch (typeof flattenedWidth) {
130
+ case 'number':
131
+ return flattenedWidth;
132
+ case 'string':
133
+ if (flattenedWidth.endsWith('%')) {
134
+ const width = parseFloat(flattenedWidth);
135
+ if (Number.isFinite(width)) {
136
+ return layout.width * (width / 100);
137
+ }
138
+ }
139
+ }
140
+
141
+ if (scrollEnabled) {
142
+ return (layout.width / 5) * 2;
143
+ }
144
+
145
+ const gapTotalWidth = (gap ?? 0) * (routes.length - 1);
146
+ const paddingTotalWidth =
147
+ convertPaddingPercentToSize(flattenedPaddingStart, layout) +
148
+ convertPaddingPercentToSize(flattenedPaddingEnd, layout);
149
+
150
+ return (layout.width - gapTotalWidth - paddingTotalWidth) / routes.length;
151
+ };
152
+
153
+ const getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
154
+ tabBarWidth - layoutWidth;
155
+
156
+ const getTranslateX = (
157
+ scrollAmount: Animated.Value,
158
+ maxScrollDistance: number,
159
+ direction: LocaleDirection
160
+ ) =>
161
+ Animated.multiply(
162
+ Platform.OS === 'android' && direction === 'rtl'
163
+ ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
164
+ : scrollAmount,
165
+ direction === 'rtl' ? 1 : -1
166
+ );
167
+
168
+ const getTabBarWidth = <T extends Route>({
169
+ navigationState,
170
+ layout,
171
+ gap,
172
+ scrollEnabled,
173
+ flattenedTabWidth,
174
+ flattenedPaddingStart,
175
+ flattenedPaddingEnd,
176
+ tabWidths,
177
+ }: Pick<Props<T>, 'navigationState' | 'gap' | 'layout' | 'scrollEnabled'> & {
178
+ tabWidths: Record<string, number>;
179
+ flattenedPaddingStart: DimensionValue | undefined;
180
+ flattenedPaddingEnd: DimensionValue | undefined;
181
+ flattenedTabWidth: DimensionValue | undefined;
182
+ }) => {
183
+ const { routes } = navigationState;
184
+
185
+ const paddingsWidth = Math.max(
186
+ 0,
187
+ convertPaddingPercentToSize(flattenedPaddingStart, layout) +
188
+ convertPaddingPercentToSize(flattenedPaddingEnd, layout)
189
+ );
190
+
191
+ return routes.reduce<number>(
192
+ (acc, _, i) =>
193
+ acc +
194
+ (i > 0 ? gap ?? 0 : 0) +
195
+ getComputedTabWidth(
196
+ i,
197
+ layout,
198
+ routes,
199
+ scrollEnabled,
200
+ tabWidths,
201
+ flattenedTabWidth,
202
+ flattenedPaddingStart,
203
+ flattenedPaddingEnd,
204
+ gap
205
+ ),
206
+ paddingsWidth
207
+ );
208
+ };
209
+
210
+ const normalizeScrollValue = <T extends Route>({
211
+ layout,
212
+ navigationState,
213
+ gap,
214
+ scrollEnabled,
215
+ tabWidths,
216
+ value,
217
+ flattenedTabWidth,
218
+ flattenedPaddingStart,
219
+ flattenedPaddingEnd,
220
+ direction,
221
+ }: Pick<Props<T>, 'layout' | 'navigationState' | 'gap' | 'scrollEnabled'> & {
222
+ tabWidths: Record<string, number>;
223
+ value: number;
224
+ flattenedTabWidth: DimensionValue | undefined;
225
+ flattenedPaddingStart: DimensionValue | undefined;
226
+ flattenedPaddingEnd: DimensionValue | undefined;
227
+ direction: LocaleDirection;
228
+ }) => {
229
+ const tabBarWidth = getTabBarWidth({
230
+ layout,
231
+ navigationState,
232
+ tabWidths,
233
+ gap,
234
+ scrollEnabled,
235
+ flattenedTabWidth,
236
+ flattenedPaddingStart,
237
+ flattenedPaddingEnd,
238
+ });
239
+ const maxDistance = getMaxScrollDistance(tabBarWidth, layout.width);
240
+ const scrollValue = Math.max(Math.min(value, maxDistance), 0);
241
+
242
+ if (Platform.OS === 'android' && direction === 'rtl') {
243
+ // On Android, scroll value is not applied in reverse in RTL
244
+ // so we need to manually adjust it to apply correct value
245
+ return maxDistance - scrollValue;
246
+ }
247
+
248
+ return scrollValue;
249
+ };
250
+
251
+ const getScrollAmount = <T extends Route>({
252
+ layout,
253
+ navigationState,
254
+ gap,
255
+ scrollEnabled,
256
+ flattenedTabWidth,
257
+ tabWidths,
258
+ flattenedPaddingStart,
259
+ flattenedPaddingEnd,
260
+ direction,
261
+ }: Pick<Props<T>, 'layout' | 'navigationState' | 'scrollEnabled' | 'gap'> & {
262
+ tabWidths: Record<string, number>;
263
+ flattenedTabWidth: DimensionValue | undefined;
264
+ flattenedPaddingStart: DimensionValue | undefined;
265
+ flattenedPaddingEnd: DimensionValue | undefined;
266
+ direction: LocaleDirection;
267
+ }) => {
268
+ const paddingInitial =
269
+ direction === 'rtl'
270
+ ? convertPaddingPercentToSize(flattenedPaddingEnd, layout)
271
+ : convertPaddingPercentToSize(flattenedPaddingStart, layout);
272
+
273
+ const centerDistance = Array.from({
274
+ length: navigationState.index + 1,
275
+ }).reduce<number>((total, _, i) => {
276
+ const tabWidth = getComputedTabWidth(
277
+ i,
278
+ layout,
279
+ navigationState.routes,
280
+ scrollEnabled,
281
+ tabWidths,
282
+ flattenedTabWidth,
283
+ flattenedPaddingStart,
284
+ flattenedPaddingEnd,
285
+ gap
286
+ );
287
+
288
+ // To get the current index centered we adjust scroll amount by width of indexes
289
+ // 0 through (i - 1) and add half the width of current index i
290
+ return (
291
+ total +
292
+ (i > 0 ? gap ?? 0 : 0) +
293
+ (navigationState.index === i ? tabWidth / 2 : tabWidth)
294
+ );
295
+ }, paddingInitial);
296
+
297
+ const scrollAmount = centerDistance - layout.width / 2;
298
+
299
+ return normalizeScrollValue({
300
+ layout,
301
+ navigationState,
302
+ tabWidths,
303
+ value: scrollAmount,
304
+ gap,
305
+ scrollEnabled,
306
+ flattenedTabWidth,
307
+ flattenedPaddingStart,
308
+ flattenedPaddingEnd,
309
+ direction,
310
+ });
311
+ };
312
+ const getLabelTextDefault = ({ route }: Scene<Route>) => route.title;
313
+
314
+ const getAccessibleDefault = ({ route }: Scene<Route>) =>
315
+ typeof route.accessible !== 'undefined' ? route.accessible : true;
316
+
317
+ const getAccessibilityLabelDefault = ({ route }: Scene<Route>) =>
318
+ typeof route.accessibilityLabel === 'string'
319
+ ? route.accessibilityLabel
320
+ : typeof route.title === 'string'
321
+ ? route.title
322
+ : undefined;
323
+
324
+ const renderIndicatorDefault = (props: IndicatorProps<Route>) => (
325
+ <TabBarIndicator {...props} />
326
+ );
327
+
328
+ const getTestIdDefault = ({ route }: Scene<Route>) => route.testID;
329
+
330
+ // How many items measurements should we update per batch.
331
+ // Defaults to 10, since that's whats FlatList is using in initialNumToRender.
332
+ const MEASURE_PER_BATCH = 10;
333
+
334
+ export function TabBar<T extends Route>({
335
+ renderIndicator = renderIndicatorDefault,
336
+ gap = 0,
337
+ scrollEnabled,
338
+ jumpTo,
339
+ navigationState,
340
+ position,
341
+ activeColor,
342
+ bounces,
343
+ contentContainerStyle,
344
+ inactiveColor,
345
+ indicatorContainerStyle,
346
+ indicatorStyle,
347
+ onTabLongPress,
348
+ onTabPress,
349
+ pressColor,
350
+ pressOpacity,
351
+ direction = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr',
352
+ renderTabBarItem,
353
+ style,
354
+ tabStyle,
355
+ layout: propLayout,
356
+ testID,
357
+ android_ripple,
358
+ options,
359
+ }: Props<T>) {
360
+ const [layout, setLayout] = React.useState<Layout>(
361
+ propLayout ?? { width: 0, height: 0 }
362
+ );
363
+ const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
364
+ const flatListRef = React.useRef<FlatList | null>(null);
365
+ const isFirst = React.useRef(true);
366
+ const scrollAmount = useAnimatedValue(0);
367
+ const measuredTabWidths = React.useRef<Record<string, number>>({});
368
+ const { routes } = navigationState;
369
+ const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
370
+ const isWidthDynamic = flattenedTabWidth === 'auto';
371
+ const flattenedPaddingEnd = getFlattenedPaddingEnd(contentContainerStyle);
372
+ const flattenedPaddingStart = getFlattenedPaddingStart(contentContainerStyle);
373
+ const scrollOffset = getScrollAmount({
374
+ layout,
375
+ navigationState,
376
+ tabWidths,
377
+ gap,
378
+ scrollEnabled,
379
+ flattenedTabWidth,
380
+ flattenedPaddingStart,
381
+ flattenedPaddingEnd,
382
+ direction,
383
+ });
384
+
385
+ const hasMeasuredTabWidths =
386
+ Boolean(layout.width) &&
387
+ routes
388
+ .slice(0, navigationState.index)
389
+ .every((r) => typeof tabWidths[r.key] === 'number');
390
+
391
+ React.useEffect(() => {
392
+ if (isFirst.current) {
393
+ isFirst.current = false;
394
+ return;
395
+ }
396
+
397
+ if (isWidthDynamic && !hasMeasuredTabWidths) {
398
+ return;
399
+ }
400
+
401
+ if (scrollEnabled) {
402
+ flatListRef.current?.scrollToOffset({
403
+ offset: scrollOffset,
404
+ animated: true,
405
+ });
406
+ }
407
+ }, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
408
+
409
+ const handleLayout = (e: LayoutChangeEvent) => {
410
+ const { height, width } = e.nativeEvent.layout;
411
+
412
+ setLayout((layout) =>
413
+ layout.width === width && layout.height === height
414
+ ? layout
415
+ : { width, height }
416
+ );
417
+ };
418
+
419
+ const tabBarWidth = getTabBarWidth({
420
+ layout,
421
+ navigationState,
422
+ tabWidths,
423
+ gap,
424
+ scrollEnabled,
425
+ flattenedTabWidth,
426
+ flattenedPaddingStart,
427
+ flattenedPaddingEnd,
428
+ });
429
+
430
+ const separatorsWidth = Math.max(0, routes.length - 1) * gap;
431
+ const paddingsWidth = Math.max(
432
+ 0,
433
+ convertPaddingPercentToSize(flattenedPaddingStart, layout) +
434
+ convertPaddingPercentToSize(flattenedPaddingEnd, layout)
435
+ );
436
+
437
+ const translateX = React.useMemo(
438
+ () =>
439
+ getTranslateX(
440
+ scrollAmount,
441
+ getMaxScrollDistance(tabBarWidth, layout.width),
442
+ direction
443
+ ),
444
+ [direction, layout.width, scrollAmount, tabBarWidth]
445
+ );
446
+
447
+ const renderItem = React.useCallback(
448
+ ({ item: route, index }: ListRenderItemInfo<T>) => {
449
+ const {
450
+ testID = getTestIdDefault({ route }),
451
+ labelText = getLabelTextDefault({ route }),
452
+ accessible = getAccessibleDefault({ route }),
453
+ accessibilityLabel = getAccessibilityLabelDefault({ route }),
454
+ ...rest
455
+ } = options?.[route.key] ?? {};
456
+
457
+ const onLayout = isWidthDynamic
458
+ ? (e: LayoutChangeEvent) => {
459
+ measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
460
+
461
+ // When we have measured widths for all of the tabs, we should updates the state
462
+ // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
463
+ // If we have more than 10 routes divide updating tabWidths into multiple batches. Here we update only first batch of 10 items.
464
+ if (
465
+ routes.length > MEASURE_PER_BATCH &&
466
+ index === MEASURE_PER_BATCH &&
467
+ routes
468
+ .slice(0, MEASURE_PER_BATCH)
469
+ .every(
470
+ (r) => typeof measuredTabWidths.current[r.key] === 'number'
471
+ )
472
+ ) {
473
+ setTabWidths({ ...measuredTabWidths.current });
474
+ } else if (
475
+ routes.every(
476
+ (r) => typeof measuredTabWidths.current[r.key] === 'number'
477
+ )
478
+ ) {
479
+ // When we have measured widths for all of the tabs, we should updates the state
480
+ // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
481
+ setTabWidths({ ...measuredTabWidths.current });
482
+ }
483
+ }
484
+ : undefined;
485
+
486
+ const onPress = () => {
487
+ const event: Scene<T> & Event = {
488
+ route,
489
+ defaultPrevented: false,
490
+ preventDefault: () => {
491
+ event.defaultPrevented = true;
492
+ },
493
+ };
494
+
495
+ onTabPress?.(event);
496
+
497
+ if (event.defaultPrevented) {
498
+ return;
499
+ }
500
+
501
+ jumpTo(route.key);
502
+ };
503
+
504
+ const onLongPress = () => onTabLongPress?.({ route });
505
+
506
+ // Calculate the default width for tab for FlatList to work
507
+ const defaultTabWidth = !isWidthDynamic
508
+ ? getComputedTabWidth(
509
+ index,
510
+ layout,
511
+ routes,
512
+ scrollEnabled,
513
+ tabWidths,
514
+ getFlattenedTabWidth(tabStyle),
515
+ getFlattenedPaddingEnd(contentContainerStyle),
516
+ getFlattenedPaddingStart(contentContainerStyle),
517
+ gap
518
+ )
519
+ : undefined;
520
+
521
+ const props = {
522
+ ...rest,
523
+ key: route.key,
524
+ position,
525
+ route,
526
+ navigationState,
527
+ testID,
528
+ labelText,
529
+ accessible,
530
+ accessibilityLabel,
531
+ activeColor,
532
+ inactiveColor,
533
+ pressColor,
534
+ pressOpacity,
535
+ onLayout,
536
+ onPress,
537
+ onLongPress,
538
+ style: tabStyle,
539
+ defaultTabWidth,
540
+ android_ripple,
541
+ } satisfies TabBarItemProps<T> & { key: string };
542
+
543
+ return (
544
+ <>
545
+ {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
546
+ {renderTabBarItem ? (
547
+ renderTabBarItem(props)
548
+ ) : (
549
+ <TabBarItem {...props} key={props.key} />
550
+ )}
551
+ </>
552
+ );
553
+ },
554
+ [
555
+ position,
556
+ navigationState,
557
+ options,
558
+ activeColor,
559
+ inactiveColor,
560
+ pressColor,
561
+ pressOpacity,
562
+ isWidthDynamic,
563
+ tabStyle,
564
+ layout,
565
+ routes,
566
+ scrollEnabled,
567
+ tabWidths,
568
+ contentContainerStyle,
569
+ gap,
570
+ android_ripple,
571
+ renderTabBarItem,
572
+ onTabPress,
573
+ jumpTo,
574
+ onTabLongPress,
575
+ ]
576
+ );
577
+
578
+ const keyExtractor = React.useCallback((item: T) => item.key, []);
579
+
580
+ const contentContainerStyleMemoized = React.useMemo(
581
+ () => [
582
+ styles.tabContent,
583
+ scrollEnabled ? { width: tabBarWidth } : null,
584
+ contentContainerStyle,
585
+ ],
586
+ [contentContainerStyle, scrollEnabled, tabBarWidth]
587
+ );
588
+
589
+ const handleScroll = React.useMemo(
590
+ () =>
591
+ Animated.event(
592
+ [
593
+ {
594
+ nativeEvent: {
595
+ contentOffset: { x: scrollAmount },
596
+ },
597
+ },
598
+ ],
599
+ { useNativeDriver }
600
+ ),
601
+ [scrollAmount]
602
+ );
603
+
604
+ const handleViewableItemsChanged = useLatestCallback(
605
+ ({ changed }: { changed: ViewToken[] }) => {
606
+ if (routes.length <= MEASURE_PER_BATCH) {
607
+ return;
608
+ }
609
+ // Get next vievable item
610
+ const item = changed[changed.length - 1];
611
+ const index = item?.index || 0;
612
+ if (
613
+ item.isViewable &&
614
+ (index % 10 === 0 ||
615
+ index === navigationState.index ||
616
+ index === routes.length - 1)
617
+ ) {
618
+ setTabWidths({ ...measuredTabWidths.current });
619
+ }
620
+ }
621
+ );
622
+
623
+ return (
624
+ <Animated.View onLayout={handleLayout} style={[styles.tabBar, style]}>
625
+ <Animated.View
626
+ pointerEvents="none"
627
+ style={[
628
+ styles.indicatorContainer,
629
+ scrollEnabled ? { transform: [{ translateX }] as any } : null,
630
+ scrollEnabled ? { width: tabBarWidth } : null,
631
+ indicatorContainerStyle,
632
+ ]}
633
+ >
634
+ {renderIndicator({
635
+ position,
636
+ layout,
637
+ navigationState,
638
+ jumpTo,
639
+ direction,
640
+ width: isWidthDynamic
641
+ ? 'auto'
642
+ : Math.max(
643
+ 0,
644
+ (tabBarWidth - separatorsWidth - paddingsWidth) / routes.length
645
+ ),
646
+ style: [
647
+ indicatorStyle,
648
+ { start: flattenedPaddingStart, end: flattenedPaddingEnd },
649
+ ],
650
+ getTabWidth: (i: number) =>
651
+ getComputedTabWidth(
652
+ i,
653
+ layout,
654
+ routes,
655
+ scrollEnabled,
656
+ tabWidths,
657
+ flattenedTabWidth,
658
+ flattenedPaddingEnd,
659
+ flattenedPaddingStart,
660
+ gap
661
+ ),
662
+ gap,
663
+ })}
664
+ </Animated.View>
665
+ <View style={styles.scroll}>
666
+ <Animated.FlatList
667
+ data={routes as Animated.WithAnimatedValue<T>[]}
668
+ keyExtractor={keyExtractor}
669
+ horizontal
670
+ accessibilityRole="tablist"
671
+ keyboardShouldPersistTaps="handled"
672
+ scrollEnabled={scrollEnabled}
673
+ bounces={bounces}
674
+ initialNumToRender={MEASURE_PER_BATCH}
675
+ onViewableItemsChanged={handleViewableItemsChanged}
676
+ alwaysBounceHorizontal={false}
677
+ scrollsToTop={false}
678
+ showsHorizontalScrollIndicator={false}
679
+ showsVerticalScrollIndicator={false}
680
+ automaticallyAdjustContentInsets={false}
681
+ overScrollMode="never"
682
+ contentContainerStyle={contentContainerStyleMemoized}
683
+ scrollEventThrottle={16}
684
+ renderItem={renderItem}
685
+ onScroll={handleScroll}
686
+ ref={flatListRef}
687
+ testID={testID}
688
+ />
689
+ </View>
690
+ </Animated.View>
691
+ );
692
+ }
693
+
694
+ const styles = StyleSheet.create({
695
+ scroll: {
696
+ overflow: Platform.select({ default: 'scroll', web: undefined }),
697
+ },
698
+ tabBar: {
699
+ zIndex: 1,
700
+ backgroundColor: '#2196f3',
701
+ elevation: 4,
702
+ ...Platform.select({
703
+ default: {
704
+ shadowColor: 'black',
705
+ shadowOpacity: 0.1,
706
+ shadowRadius: StyleSheet.hairlineWidth,
707
+ shadowOffset: {
708
+ height: StyleSheet.hairlineWidth,
709
+ width: 0,
710
+ },
711
+ },
712
+ web: {
713
+ boxShadow: '0 1px 1px rgba(0, 0, 0, 0.1)',
714
+ },
715
+ }),
716
+ },
717
+ tabContent: {
718
+ flexGrow: 1,
719
+ flexDirection: 'row',
720
+ flexWrap: 'nowrap',
721
+ },
722
+ indicatorContainer: {
723
+ position: 'absolute',
724
+ top: 0,
725
+ start: 0,
726
+ end: 0,
727
+ bottom: 0,
728
+ },
729
+ });