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

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 (108) hide show
  1. package/lib/module/DefaultAdapter.android.js +4 -0
  2. package/lib/module/DefaultAdapter.android.js.map +1 -0
  3. package/lib/module/DefaultAdapter.ios.js +4 -0
  4. package/lib/module/DefaultAdapter.ios.js.map +1 -0
  5. package/lib/module/DefaultAdapter.js +5 -0
  6. package/lib/module/DefaultAdapter.js.map +1 -0
  7. package/lib/module/PagerViewAdapter.native.js +29 -13
  8. package/lib/module/PagerViewAdapter.native.js.map +1 -1
  9. package/lib/module/PanResponderAdapter.js +3 -0
  10. package/lib/module/PanResponderAdapter.js.map +1 -1
  11. package/lib/module/PlatformPressable.js +1 -1
  12. package/lib/module/PlatformPressable.js.map +1 -1
  13. package/lib/module/SceneMap.js +12 -6
  14. package/lib/module/SceneMap.js.map +1 -1
  15. package/lib/module/SceneView.js +25 -1
  16. package/lib/module/SceneView.js.map +1 -1
  17. package/lib/module/ScrollViewAdapter.js +49 -18
  18. package/lib/module/ScrollViewAdapter.js.map +1 -1
  19. package/lib/module/TabBar.js +279 -169
  20. package/lib/module/TabBar.js.map +1 -1
  21. package/lib/module/TabBarIndicator.js +314 -90
  22. package/lib/module/TabBarIndicator.js.map +1 -1
  23. package/lib/module/TabBarItem.js +94 -44
  24. package/lib/module/TabBarItem.js.map +1 -1
  25. package/lib/module/TabBarItemLabel.js +3 -2
  26. package/lib/module/TabBarItemLabel.js.map +1 -1
  27. package/lib/module/TabView.js +2 -2
  28. package/lib/module/TabView.js.map +1 -1
  29. package/lib/module/constants.js +10 -0
  30. package/lib/module/constants.js.map +1 -0
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/useLayoutWidths.js +46 -0
  34. package/lib/module/useLayoutWidths.js.map +1 -0
  35. package/lib/typescript/src/DefaultAdapter.android.d.ts +2 -0
  36. package/lib/typescript/src/DefaultAdapter.android.d.ts.map +1 -0
  37. package/lib/typescript/src/DefaultAdapter.d.ts +6 -0
  38. package/lib/typescript/src/DefaultAdapter.d.ts.map +1 -0
  39. package/lib/typescript/src/DefaultAdapter.ios.d.ts +2 -0
  40. package/lib/typescript/src/DefaultAdapter.ios.d.ts.map +1 -0
  41. package/lib/typescript/src/PagerViewAdapter.d.ts +1 -1
  42. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
  43. package/lib/typescript/src/PagerViewAdapter.native.d.ts +2 -2
  44. package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -1
  45. package/lib/typescript/src/PanResponderAdapter.d.ts +1 -1
  46. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
  47. package/lib/typescript/src/PlatformPressable.d.ts +5 -5
  48. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  49. package/lib/typescript/src/SceneMap.d.ts +2 -2
  50. package/lib/typescript/src/SceneMap.d.ts.map +1 -1
  51. package/lib/typescript/src/SceneView.d.ts +2 -2
  52. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  53. package/lib/typescript/src/ScrollViewAdapter.d.ts +2 -3
  54. package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -1
  55. package/lib/typescript/src/TabBar.d.ts +26 -25
  56. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  57. package/lib/typescript/src/TabBarIndicator.d.ts +5 -7
  58. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  59. package/lib/typescript/src/TabBarItem.d.ts +17 -10
  60. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  61. package/lib/typescript/src/TabBarItemLabel.d.ts +4 -3
  62. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  63. package/lib/typescript/src/TabView.d.ts +17 -17
  64. package/lib/typescript/src/TabView.d.ts.map +1 -1
  65. package/lib/typescript/src/constants.d.ts +8 -0
  66. package/lib/typescript/src/constants.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts +13 -12
  68. package/lib/typescript/src/index.d.ts.map +1 -1
  69. package/lib/typescript/src/types.d.ts +31 -31
  70. package/lib/typescript/src/types.d.ts.map +1 -1
  71. package/lib/typescript/src/useLayoutWidths.d.ts +2 -0
  72. package/lib/typescript/src/useLayoutWidths.d.ts.map +1 -0
  73. package/lib/typescript/src/useMeasureLayout.d.ts +1 -1
  74. package/lib/typescript/src/useMeasureLayout.d.ts.map +1 -1
  75. package/package.json +14 -13
  76. package/src/DefaultAdapter.android.tsx +1 -0
  77. package/src/DefaultAdapter.ios.tsx +1 -0
  78. package/src/DefaultAdapter.tsx +13 -0
  79. package/src/PagerViewAdapter.native.tsx +36 -18
  80. package/src/PanResponderAdapter.tsx +4 -0
  81. package/src/PlatformPressable.tsx +11 -7
  82. package/src/SceneMap.tsx +17 -9
  83. package/src/SceneView.tsx +41 -2
  84. package/src/ScrollViewAdapter.tsx +88 -22
  85. package/src/TabBar.tsx +440 -235
  86. package/src/TabBarIndicator.tsx +389 -124
  87. package/src/TabBarItem.tsx +107 -46
  88. package/src/TabBarItemLabel.tsx +5 -4
  89. package/src/TabView.tsx +24 -20
  90. package/src/constants.tsx +8 -0
  91. package/src/index.tsx +5 -1
  92. package/src/types.tsx +40 -36
  93. package/src/useLayoutWidths.tsx +51 -0
  94. package/lib/module/Pager.android.js +0 -4
  95. package/lib/module/Pager.android.js.map +0 -1
  96. package/lib/module/Pager.ios.js +0 -4
  97. package/lib/module/Pager.ios.js.map +0 -1
  98. package/lib/module/Pager.js +0 -4
  99. package/lib/module/Pager.js.map +0 -1
  100. package/lib/typescript/src/Pager.android.d.ts +0 -2
  101. package/lib/typescript/src/Pager.android.d.ts.map +0 -1
  102. package/lib/typescript/src/Pager.d.ts +0 -2
  103. package/lib/typescript/src/Pager.d.ts.map +0 -1
  104. package/lib/typescript/src/Pager.ios.d.ts +0 -2
  105. package/lib/typescript/src/Pager.ios.d.ts.map +0 -1
  106. package/src/Pager.android.tsx +0 -1
  107. package/src/Pager.ios.tsx +0 -1
  108. package/src/Pager.tsx +0 -1
package/src/TabBar.tsx CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  type DimensionValue,
6
6
  FlatList,
7
7
  I18nManager,
8
- type LayoutChangeEvent,
9
8
  type ListRenderItemInfo,
10
9
  Platform,
11
10
  type PressableAndroidRippleConfig,
@@ -13,10 +12,14 @@ import {
13
12
  StyleSheet,
14
13
  View,
15
14
  type ViewStyle,
16
- type ViewToken,
17
15
  } from 'react-native';
18
- import useLatestCallback from 'use-latest-callback';
19
16
 
17
+ import {
18
+ PRIMARY_INDICATOR_MIN_WIDTH,
19
+ TAB_BAR_BACKGROUND_COLOR,
20
+ TAB_BAR_BORDER_COLOR,
21
+ TAB_MIN_WIDTH,
22
+ } from './constants';
20
23
  import {
21
24
  type Props as IndicatorProps,
22
25
  TabBarIndicator,
@@ -33,33 +36,37 @@ import type {
33
36
  TabDescriptor,
34
37
  } from './types';
35
38
  import { useAnimatedValue } from './useAnimatedValue';
39
+ import { useLayoutWidths } from './useLayoutWidths';
36
40
  import { useMeasureLayout } from './useMeasureLayout';
37
41
 
38
42
  export type Props<T extends Route> = SceneRendererProps &
39
43
  EventEmitterProps & {
44
+ variant?: 'primary' | 'secondary' | undefined;
40
45
  navigationState: NavigationState<T>;
41
- scrollEnabled?: boolean;
42
- bounces?: boolean;
43
- activeColor?: ColorValue;
44
- inactiveColor?: ColorValue;
45
- pressColor?: ColorValue;
46
- pressOpacity?: number;
47
- options?: Record<string, TabDescriptor<T>>;
48
- renderIndicator?: (props: IndicatorProps<T>) => React.ReactNode;
49
- renderTabBarItem?: (
50
- props: TabBarItemProps<T> & { key: string }
51
- ) => React.ReactElement;
52
- onTabPress?: (scene: Scene<T> & Event) => void;
53
- onTabLongPress?: (scene: Scene<T>) => void;
54
- tabStyle?: StyleProp<ViewStyle>;
55
- indicatorStyle?: StyleProp<ViewStyle>;
56
- indicatorContainerStyle?: StyleProp<ViewStyle>;
57
- contentContainerStyle?: StyleProp<ViewStyle>;
58
- style?: StyleProp<ViewStyle>;
59
- direction?: LocaleDirection;
60
- gap?: number;
61
- testID?: string;
62
- android_ripple?: PressableAndroidRippleConfig;
46
+ scrollEnabled?: boolean | undefined;
47
+ bounces?: boolean | undefined;
48
+ activeColor?: ColorValue | undefined;
49
+ inactiveColor?: ColorValue | undefined;
50
+ pressColor?: ColorValue | undefined;
51
+ pressOpacity?: number | undefined;
52
+ options?: Record<string, TabDescriptor<T>> | undefined;
53
+ renderIndicator?:
54
+ | ((props: IndicatorProps<T>) => React.ReactNode)
55
+ | undefined;
56
+ renderTabBarItem?:
57
+ | ((props: TabBarItemProps<T> & { key: string }) => React.ReactElement)
58
+ | undefined;
59
+ onTabPress?: ((scene: Scene<T> & Event) => void) | undefined;
60
+ onTabLongPress?: ((scene: Scene<T>) => void) | undefined;
61
+ tabStyle?: StyleProp<ViewStyle> | undefined;
62
+ indicatorStyle?: StyleProp<ViewStyle> | undefined;
63
+ indicatorContainerStyle?: StyleProp<ViewStyle> | undefined;
64
+ contentContainerStyle?: StyleProp<ViewStyle> | undefined;
65
+ style?: StyleProp<ViewStyle> | undefined;
66
+ direction?: LocaleDirection | undefined;
67
+ gap?: number | undefined;
68
+ testID?: string | undefined;
69
+ android_ripple?: PressableAndroidRippleConfig | undefined;
63
70
  };
64
71
 
65
72
  type CalculationOptions = {
@@ -72,8 +79,6 @@ type CalculationOptions = {
72
79
  flattenedTabWidth: DimensionValue | undefined;
73
80
  };
74
81
 
75
- const useNativeDriver = Platform.OS !== 'web';
76
-
77
82
  const Separator = ({ width }: { width: number }) => {
78
83
  return <View style={{ width }} />;
79
84
  };
@@ -138,8 +143,14 @@ const getComputedTabWidth = ({
138
143
  index: number;
139
144
  routes: Route[];
140
145
  }) => {
146
+ const route = routes[index];
147
+
148
+ if (route == null) {
149
+ throw new Error(`Couldn't find a route at index ${index}.`);
150
+ }
151
+
141
152
  if (flattenedTabWidth === 'auto') {
142
- return tabWidths[routes[index].key] || 0;
153
+ return tabWidths[route.key] || 0;
143
154
  }
144
155
 
145
156
  switch (typeof flattenedTabWidth) {
@@ -155,7 +166,7 @@ const getComputedTabWidth = ({
155
166
  }
156
167
 
157
168
  if (scrollEnabled) {
158
- return (layoutWidth / 5) * 2;
169
+ return tabWidths[route.key] || TAB_MIN_WIDTH;
159
170
  }
160
171
 
161
172
  const gapTotalWidth = (gap ?? 0) * (routes.length - 1);
@@ -166,6 +177,25 @@ const getComputedTabWidth = ({
166
177
  return (layoutWidth - gapTotalWidth - paddingTotalWidth) / routes.length;
167
178
  };
168
179
 
180
+ const calculateSize = (
181
+ value: ViewStyle['width'] | undefined,
182
+ referenceWidth: number
183
+ ): number | undefined => {
184
+ if (typeof value === 'number') {
185
+ return value;
186
+ }
187
+
188
+ if (typeof value === 'string' && value.endsWith('%')) {
189
+ const parsed = parseFloat(value);
190
+
191
+ if (Number.isFinite(parsed)) {
192
+ return referenceWidth * (parsed / 100);
193
+ }
194
+ }
195
+
196
+ return undefined;
197
+ };
198
+
169
199
  const getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
170
200
  tabBarWidth - layoutWidth;
171
201
 
@@ -173,13 +203,15 @@ const getTranslateX = (
173
203
  scrollAmount: Animated.Value,
174
204
  maxScrollDistance: number,
175
205
  direction: LocaleDirection
176
- ) =>
177
- Animated.multiply(
206
+ ) => {
207
+ const amount =
208
+ // Android reports scroll from the opposite side in RTL
178
209
  Platform.OS === 'android' && direction === 'rtl'
179
210
  ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
180
- : scrollAmount,
181
- direction === 'rtl' ? 1 : -1
182
- );
211
+ : scrollAmount;
212
+
213
+ return Animated.multiply(amount, direction === 'rtl' ? 1 : -1);
214
+ };
183
215
 
184
216
  const getTabBarWidth = <T extends Route>({
185
217
  routes,
@@ -228,11 +260,9 @@ const normalizeScrollValue = <T extends Route>({
228
260
  flattenedTabWidth,
229
261
  flattenedPaddingStart,
230
262
  flattenedPaddingEnd,
231
- direction,
232
263
  }: CalculationOptions & {
233
264
  routes: T[];
234
265
  value: number;
235
- direction: LocaleDirection;
236
266
  }) => {
237
267
  const tabBarWidth = getTabBarWidth({
238
268
  layoutWidth,
@@ -247,12 +277,6 @@ const normalizeScrollValue = <T extends Route>({
247
277
  const maxDistance = getMaxScrollDistance(tabBarWidth, layoutWidth);
248
278
  const scrollValue = Math.max(Math.min(value, maxDistance), 0);
249
279
 
250
- if (Platform.OS === 'android' && direction === 'rtl') {
251
- // On Android, scroll value is not applied in reverse in RTL
252
- // so we need to manually adjust it to apply correct value
253
- return maxDistance - scrollValue;
254
- }
255
-
256
280
  return scrollValue;
257
281
  };
258
282
 
@@ -311,7 +335,6 @@ const getScrollAmount = <T extends Route>({
311
335
  flattenedTabWidth,
312
336
  flattenedPaddingStart,
313
337
  flattenedPaddingEnd,
314
- direction,
315
338
  });
316
339
  };
317
340
  const getLabelTextDefault = ({ route }: Scene<Route>) => route.title;
@@ -334,9 +357,10 @@ const getTestIdDefault = ({ route }: Scene<Route>) => route.testID;
334
357
 
335
358
  // How many items measurements should we update per batch.
336
359
  // Defaults to 10, since that's whats FlatList is using in initialNumToRender.
337
- const MEASURE_PER_BATCH = 10;
360
+ const RENDER_PER_BATCH = 10;
338
361
 
339
362
  export function TabBar<T extends Route>({
363
+ variant = 'primary',
340
364
  renderIndicator = renderIndicatorDefault,
341
365
  gap = 0,
342
366
  scrollEnabled,
@@ -364,17 +388,40 @@ export function TabBar<T extends Route>({
364
388
  const containerRef = React.useRef<View>(null);
365
389
  const [layout, onLayout] = useMeasureLayout(containerRef);
366
390
 
367
- const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
391
+ // Prioritize measuring tabs upto focused item
392
+ // Since we need those measurements for calculation
393
+ const priorityKeysForLayout = navigationState.routes
394
+ .slice(0, navigationState.index + 1)
395
+ .map((r) => r.key);
396
+
397
+ const [tabWidths, onMeasureTabWidth] = useLayoutWidths(priorityKeysForLayout);
398
+ const [labelWidths, onMeasureLabelWidth] = useLayoutWidths(
399
+ priorityKeysForLayout
400
+ );
401
+
368
402
  const flatListRef = React.useRef<FlatList | null>(null);
369
403
  const isFirst = React.useRef(true);
370
- const scrollAmount = useAnimatedValue(0);
371
- const measuredTabWidths = React.useRef<Record<string, number>>({});
404
+
372
405
  const { routes } = navigationState;
406
+
373
407
  const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
374
- const isWidthDynamic = flattenedTabWidth === 'auto';
408
+ const isWidthAuto = flattenedTabWidth === 'auto';
409
+ const isWidthDynamic =
410
+ isWidthAuto || (scrollEnabled && flattenedTabWidth == null);
411
+
375
412
  const flattenedPaddingEnd = getFlattenedPaddingEnd(contentContainerStyle);
376
413
  const flattenedPaddingStart = getFlattenedPaddingStart(contentContainerStyle);
377
414
 
415
+ const paddingEnd = convertPaddingPercentToSize(
416
+ flattenedPaddingEnd,
417
+ layout.width
418
+ );
419
+
420
+ const paddingStart = convertPaddingPercentToSize(
421
+ flattenedPaddingStart,
422
+ layout.width
423
+ );
424
+
378
425
  const scrollOffset = getScrollAmount({
379
426
  layoutWidth: layout.width,
380
427
  routes,
@@ -400,7 +447,7 @@ export function TabBar<T extends Route>({
400
447
  return;
401
448
  }
402
449
 
403
- if (isWidthDynamic && !hasMeasuredTabWidths) {
450
+ if (isWidthAuto && !hasMeasuredTabWidths) {
404
451
  return;
405
452
  }
406
453
 
@@ -410,7 +457,7 @@ export function TabBar<T extends Route>({
410
457
  animated: true,
411
458
  });
412
459
  }
413
- }, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
460
+ }, [hasMeasuredTabWidths, isWidthAuto, scrollEnabled, scrollOffset]);
414
461
 
415
462
  const tabBarWidth = getTabBarWidth({
416
463
  layoutWidth: layout.width,
@@ -423,152 +470,86 @@ export function TabBar<T extends Route>({
423
470
  flattenedPaddingEnd,
424
471
  });
425
472
 
426
- const separatorsWidth = Math.max(0, routes.length - 1) * gap;
427
- const paddingsWidth = Math.max(
428
- 0,
429
- convertPaddingPercentToSize(flattenedPaddingStart, layout.width) +
430
- convertPaddingPercentToSize(flattenedPaddingEnd, layout.width)
431
- );
473
+ const maxScrollDistance = getMaxScrollDistance(tabBarWidth, layout.width);
474
+ const scrollAmount = useAnimatedValue(0);
475
+
476
+ React.useLayoutEffect(() => {
477
+ scrollAmount.setValue(
478
+ Platform.OS === 'android' && direction === 'rtl' ? maxScrollDistance : 0
479
+ );
480
+ }, [direction, maxScrollDistance, scrollAmount]);
432
481
 
433
482
  const translateX = React.useMemo(
434
- () =>
435
- getTranslateX(
436
- scrollAmount,
437
- getMaxScrollDistance(tabBarWidth, layout.width),
438
- direction
439
- ),
440
- [direction, layout.width, scrollAmount, tabBarWidth]
483
+ () => getTranslateX(scrollAmount, maxScrollDistance, direction),
484
+ [direction, maxScrollDistance, scrollAmount]
441
485
  );
442
486
 
443
- const renderItem = React.useCallback(
444
- ({ item: route, index }: ListRenderItemInfo<T>) => {
445
- const {
446
- testID = getTestIdDefault({ route }),
447
- labelText = getLabelTextDefault({ route }),
448
- accessible = getAccessibleDefault({ route }),
449
- accessibilityLabel = getAccessibilityLabelDefault({ route }),
450
- ...rest
451
- } = options?.[route.key] ?? {};
452
-
453
- const onLayout = isWidthDynamic
454
- ? (e: LayoutChangeEvent) => {
455
- measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
456
-
457
- // When we have measured widths for all of the tabs, we should updates the state
458
- // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
459
- // If we have more than 10 routes divide updating tabWidths into multiple batches. Here we update only first batch of 10 items.
460
- if (
461
- routes.length > MEASURE_PER_BATCH &&
462
- index === MEASURE_PER_BATCH &&
463
- routes
464
- .slice(0, MEASURE_PER_BATCH)
465
- .every(
466
- (r) => typeof measuredTabWidths.current[r.key] === 'number'
467
- )
468
- ) {
469
- setTabWidths({ ...measuredTabWidths.current });
470
- } else if (
471
- routes.every(
472
- (r) => typeof measuredTabWidths.current[r.key] === 'number'
473
- )
474
- ) {
475
- // When we have measured widths for all of the tabs, we should updates the state
476
- // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
477
- setTabWidths({ ...measuredTabWidths.current });
478
- }
479
- }
480
- : undefined;
481
-
482
- const onPress = () => {
483
- const event: Scene<T> & Event = {
484
- route,
485
- defaultPrevented: false,
486
- preventDefault: () => {
487
- event.defaultPrevented = true;
488
- },
489
- };
490
-
491
- onTabPress?.(event);
487
+ const flattenedTabStyle = StyleSheet.flatten(tabStyle);
488
+ const isTabWidthSet = flattenedTabStyle?.width !== undefined;
492
489
 
493
- if (event.defaultPrevented) {
494
- return;
495
- }
490
+ // Calculate the default width for tab for FlatList to work.
491
+ const defaultTabWidth = !isWidthDynamic
492
+ ? getComputedTabWidth({
493
+ // When `isWidthDynamic` is false, every tab gets the same width and
494
+ // `getComputedTabWidth` ignores `index`, so we compute it once with index 0.
495
+ index: 0,
496
+ layoutWidth: layout.width,
497
+ routes,
498
+ scrollEnabled,
499
+ tabWidths,
500
+ flattenedTabWidth,
501
+ flattenedPaddingStart,
502
+ flattenedPaddingEnd,
503
+ gap,
504
+ })
505
+ : undefined;
496
506
 
497
- jumpTo(route.key);
498
- };
499
-
500
- const onLongPress = () => onTabLongPress?.({ route });
501
-
502
- // Calculate the default width for tab for FlatList to work
503
- const defaultTabWidth = !isWidthDynamic
504
- ? getComputedTabWidth({
505
- index,
506
- layoutWidth: layout.width,
507
- routes,
508
- scrollEnabled,
509
- tabWidths,
510
- flattenedTabWidth: getFlattenedTabWidth(tabStyle),
511
- flattenedPaddingStart: getFlattenedPaddingStart(
512
- contentContainerStyle
513
- ),
514
- flattenedPaddingEnd: getFlattenedPaddingEnd(contentContainerStyle),
515
- gap,
516
- })
517
- : undefined;
518
-
519
- const props = {
520
- ...rest,
521
- position,
522
- route,
523
- navigationState,
524
- testID,
525
- labelText,
526
- accessible,
527
- accessibilityLabel,
528
- activeColor,
529
- inactiveColor,
530
- pressColor,
531
- pressOpacity,
532
- onLayout,
533
- onPress,
534
- onLongPress,
535
- style: tabStyle,
536
- defaultTabWidth,
537
- android_ripple,
538
- } satisfies TabBarItemProps<T>;
539
-
540
- return (
541
- <>
542
- {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
543
- {renderTabBarItem ? (
544
- renderTabBarItem({ key: route.key, ...props })
545
- ) : (
546
- <TabBarItem key={route.key} {...props} />
547
- )}
548
- </>
549
- );
550
- },
507
+ const renderItem = React.useCallback(
508
+ ({ item: route, index }: ListRenderItemInfo<T>) => (
509
+ <MemoizedTabBarItemWrapper
510
+ route={route}
511
+ index={index}
512
+ option={options?.[route.key]}
513
+ position={position}
514
+ navigationState={navigationState}
515
+ variant={variant}
516
+ activeColor={activeColor}
517
+ inactiveColor={inactiveColor}
518
+ pressColor={pressColor}
519
+ pressOpacity={pressOpacity}
520
+ android_ripple={android_ripple}
521
+ tabStyle={tabStyle}
522
+ defaultTabWidth={defaultTabWidth}
523
+ isWidthSet={isTabWidthSet}
524
+ gap={gap}
525
+ onMeasureTabWidth={onMeasureTabWidth}
526
+ onMeasureLabelWidth={onMeasureLabelWidth}
527
+ onTabPress={onTabPress}
528
+ onTabLongPress={onTabLongPress}
529
+ jumpTo={jumpTo}
530
+ renderTabBarItem={renderTabBarItem}
531
+ />
532
+ ),
551
533
  [
534
+ options,
552
535
  position,
553
536
  navigationState,
554
- options,
537
+ variant,
555
538
  activeColor,
556
539
  inactiveColor,
557
540
  pressColor,
558
541
  pressOpacity,
559
- isWidthDynamic,
542
+ android_ripple,
560
543
  tabStyle,
561
- layout,
562
- routes,
563
- scrollEnabled,
564
- tabWidths,
565
- contentContainerStyle,
544
+ defaultTabWidth,
545
+ isTabWidthSet,
566
546
  gap,
567
- android_ripple,
568
- renderTabBarItem,
547
+ onMeasureTabWidth,
548
+ onMeasureLabelWidth,
569
549
  onTabPress,
570
- jumpTo,
571
550
  onTabLongPress,
551
+ jumpTo,
552
+ renderTabBarItem,
572
553
  ]
573
554
  );
574
555
 
@@ -593,35 +574,138 @@ export function TabBar<T extends Route>({
593
574
  },
594
575
  },
595
576
  ],
596
- { useNativeDriver }
577
+ { useNativeDriver: Platform.OS !== 'web' }
597
578
  ),
598
579
  [scrollAmount]
599
580
  );
600
581
 
601
- const handleViewableItemsChanged = useLatestCallback(
602
- ({ changed }: { changed: ViewToken[] }) => {
603
- if (routes.length <= MEASURE_PER_BATCH) {
604
- return;
605
- }
606
- // Get next vievable item
607
- const item = changed[changed.length - 1];
608
- const index = item?.index || 0;
609
- if (
610
- item.isViewable &&
611
- (index % 10 === 0 ||
612
- index === navigationState.index ||
613
- index === routes.length - 1)
614
- ) {
615
- setTabWidths({ ...measuredTabWidths.current });
582
+ const flattenedIndicatorStyle = StyleSheet.flatten(indicatorStyle);
583
+ const defaultIndicatorStyle =
584
+ variant === 'primary' ? styles.primaryIndicator : styles.secondaryIndicator;
585
+
586
+ const tabWidthByIndex = routes.map((_, i) =>
587
+ getComputedTabWidth({
588
+ index: i,
589
+ layoutWidth: layout.width,
590
+ routes,
591
+ scrollEnabled,
592
+ tabWidths,
593
+ flattenedTabWidth,
594
+ flattenedPaddingEnd,
595
+ flattenedPaddingStart,
596
+ gap,
597
+ })
598
+ );
599
+
600
+ const indicatorBaseWidths = tabWidthByIndex.map((tabWidth, i) => {
601
+ const customIndicatorWidth = calculateSize(
602
+ flattenedIndicatorStyle?.width,
603
+ tabWidth
604
+ );
605
+
606
+ if (customIndicatorWidth != null) {
607
+ return customIndicatorWidth;
608
+ }
609
+
610
+ if (variant === 'primary') {
611
+ const route = routes[i];
612
+
613
+ if (route == null) {
614
+ throw new Error(`Couldn't find a route at index ${i}.`);
616
615
  }
616
+
617
+ const labelWidth = labelWidths[route.key];
618
+
619
+ return labelWidth ? Math.max(PRIMARY_INDICATOR_MIN_WIDTH, labelWidth) : 0;
617
620
  }
618
- );
621
+
622
+ return tabWidth;
623
+ });
624
+
625
+ const indicatorMargins = indicatorBaseWidths.map((width) => {
626
+ if (!width) {
627
+ return { left: 0, right: 0 };
628
+ }
629
+
630
+ const marginHorizontal =
631
+ flattenedIndicatorStyle?.marginHorizontal ??
632
+ flattenedIndicatorStyle?.margin;
633
+
634
+ const leftMargin =
635
+ (direction === 'ltr'
636
+ ? flattenedIndicatorStyle?.marginStart
637
+ : flattenedIndicatorStyle?.marginEnd) ??
638
+ flattenedIndicatorStyle?.marginLeft ??
639
+ marginHorizontal;
640
+
641
+ const rightMargin =
642
+ (direction === 'rtl'
643
+ ? flattenedIndicatorStyle?.marginStart
644
+ : flattenedIndicatorStyle?.marginEnd) ??
645
+ flattenedIndicatorStyle?.marginRight ??
646
+ marginHorizontal;
647
+
648
+ return {
649
+ left: calculateSize(leftMargin, width) ?? 0,
650
+ right: calculateSize(rightMargin, width) ?? 0,
651
+ };
652
+ });
653
+
654
+ const indicatorWidths = indicatorBaseWidths.map((width, i) => {
655
+ if (!width) {
656
+ return 0;
657
+ }
658
+
659
+ const margin = indicatorMargins[i];
660
+
661
+ if (margin == null) {
662
+ throw new Error(`Couldn't find an indicator margin at index ${i}.`);
663
+ }
664
+
665
+ return Math.max(0, width - margin.left - margin.right);
666
+ });
667
+
668
+ const indicatorOffsets = tabWidthByIndex.map((tabWidth, i) => {
669
+ const precedingTabsWidth = tabWidthByIndex
670
+ .slice(0, i)
671
+ .reduce((sum, width) => sum + width, 0);
672
+
673
+ const tabStart = precedingTabsWidth + gap * i;
674
+ const customIndicatorWidth = calculateSize(
675
+ flattenedIndicatorStyle?.width,
676
+ tabWidth
677
+ );
678
+
679
+ const margin = indicatorMargins[i];
680
+
681
+ if (margin == null) {
682
+ throw new Error(`Couldn't find an indicator margin at index ${i}.`);
683
+ }
684
+
685
+ const indicatorBaseWidth = indicatorBaseWidths[i];
686
+
687
+ if (indicatorBaseWidth == null) {
688
+ throw new Error(`Couldn't find an indicator width at index ${i}.`);
689
+ }
690
+
691
+ const shouldCenterIndicator =
692
+ variant === 'primary' ||
693
+ (customIndicatorWidth != null &&
694
+ (flattenedIndicatorStyle?.margin === 'auto' ||
695
+ flattenedIndicatorStyle?.marginHorizontal === 'auto'));
696
+
697
+ const baseOffset = shouldCenterIndicator
698
+ ? (tabWidth - indicatorBaseWidth) / 2
699
+ : 0;
700
+
701
+ return tabStart + baseOffset + margin.left;
702
+ });
619
703
 
620
704
  return (
621
705
  <Animated.View
622
706
  ref={containerRef}
623
707
  onLayout={onLayout}
624
- style={[styles.tabBar, style]}
708
+ style={[styles.tabBar, { direction }, style]}
625
709
  >
626
710
  <Animated.View
627
711
  style={[
@@ -632,33 +716,18 @@ export function TabBar<T extends Route>({
632
716
  ]}
633
717
  >
634
718
  {renderIndicator({
719
+ variant,
635
720
  position,
636
721
  navigationState,
637
722
  jumpTo,
638
723
  direction,
639
- width: isWidthDynamic
640
- ? 'auto'
641
- : Math.max(
642
- 0,
643
- (tabBarWidth - separatorsWidth - paddingsWidth) / routes.length
644
- ),
724
+ widths: indicatorWidths,
725
+ offsets: indicatorOffsets,
645
726
  style: [
727
+ defaultIndicatorStyle,
646
728
  indicatorStyle,
647
- { start: flattenedPaddingStart, end: flattenedPaddingEnd },
729
+ { start: paddingStart, end: paddingEnd },
648
730
  ],
649
- getTabWidth: (i: number) =>
650
- getComputedTabWidth({
651
- index: i,
652
- layoutWidth: layout.width,
653
- routes,
654
- scrollEnabled,
655
- tabWidths,
656
- flattenedTabWidth,
657
- flattenedPaddingEnd,
658
- flattenedPaddingStart,
659
- gap,
660
- }),
661
- gap,
662
731
  })}
663
732
  </Animated.View>
664
733
  <View style={styles.scroll}>
@@ -670,8 +739,7 @@ export function TabBar<T extends Route>({
670
739
  keyboardShouldPersistTaps="handled"
671
740
  scrollEnabled={scrollEnabled}
672
741
  bounces={bounces}
673
- initialNumToRender={MEASURE_PER_BATCH}
674
- onViewableItemsChanged={handleViewableItemsChanged}
742
+ initialNumToRender={RENDER_PER_BATCH}
675
743
  alwaysBounceHorizontal={false}
676
744
  scrollsToTop={false}
677
745
  showsHorizontalScrollIndicator={false}
@@ -690,28 +758,155 @@ export function TabBar<T extends Route>({
690
758
  );
691
759
  }
692
760
 
761
+ type TabBarItemWrapperProps<T extends Route> = {
762
+ route: T;
763
+ index: number;
764
+ option: TabDescriptor<T> | undefined;
765
+ position: Animated.AnimatedInterpolation<number>;
766
+ navigationState: NavigationState<T>;
767
+ variant: 'primary' | 'secondary';
768
+ activeColor: ColorValue | undefined;
769
+ inactiveColor: ColorValue | undefined;
770
+ pressColor: ColorValue | undefined;
771
+ pressOpacity: number | undefined;
772
+ android_ripple: PressableAndroidRippleConfig | undefined;
773
+ tabStyle: StyleProp<ViewStyle>;
774
+ defaultTabWidth: number | undefined;
775
+ isWidthSet: boolean;
776
+ gap: number;
777
+ onMeasureTabWidth: (key: string, width: number) => void;
778
+ onMeasureLabelWidth: (key: string, width: number) => void;
779
+ onTabPress: ((scene: Scene<T> & Event) => void) | undefined;
780
+ onTabLongPress: ((scene: Scene<T>) => void) | undefined;
781
+ jumpTo: (key: string) => void;
782
+ renderTabBarItem:
783
+ | ((props: TabBarItemProps<T> & { key: string }) => React.ReactElement)
784
+ | undefined;
785
+ };
786
+
787
+ function TabBarItemWrapper<T extends Route>({
788
+ route,
789
+ index,
790
+ option,
791
+ position,
792
+ navigationState,
793
+ variant,
794
+ activeColor,
795
+ inactiveColor,
796
+ pressColor,
797
+ pressOpacity,
798
+ android_ripple,
799
+ tabStyle,
800
+ defaultTabWidth,
801
+ isWidthSet,
802
+ gap,
803
+ onMeasureTabWidth,
804
+ onMeasureLabelWidth,
805
+ onTabPress,
806
+ onTabLongPress,
807
+ jumpTo,
808
+ renderTabBarItem,
809
+ }: TabBarItemWrapperProps<T>) {
810
+ const {
811
+ testID = getTestIdDefault({ route }),
812
+ labelText = getLabelTextDefault({ route }),
813
+ accessible = getAccessibleDefault({ route }),
814
+ accessibilityLabel = getAccessibilityLabelDefault({ route }),
815
+ ...rest
816
+ } = option ?? {};
817
+
818
+ const onMeasureLayout = React.useCallback(
819
+ ({ width }: { width: number }) => onMeasureTabWidth(route.key, width),
820
+ [route.key, onMeasureTabWidth]
821
+ );
822
+
823
+ const onMeasureLabelLayout = React.useCallback(
824
+ ({ width }: { width: number }) => onMeasureLabelWidth(route.key, width),
825
+ [route.key, onMeasureLabelWidth]
826
+ );
827
+
828
+ const onPress = React.useCallback(() => {
829
+ const event: Scene<T> & Event = {
830
+ route,
831
+ defaultPrevented: false,
832
+ preventDefault: () => {
833
+ event.defaultPrevented = true;
834
+ },
835
+ };
836
+
837
+ onTabPress?.(event);
838
+
839
+ if (event.defaultPrevented) {
840
+ return;
841
+ }
842
+
843
+ jumpTo(route.key);
844
+ }, [route, onTabPress, jumpTo]);
845
+
846
+ const onLongPress = React.useCallback(
847
+ () => onTabLongPress?.({ route }),
848
+ [route, onTabLongPress]
849
+ );
850
+
851
+ const style = React.useMemo(
852
+ () => [
853
+ tabStyle,
854
+ isWidthSet
855
+ ? null
856
+ : defaultTabWidth !== undefined
857
+ ? { width: defaultTabWidth }
858
+ : { minWidth: TAB_MIN_WIDTH },
859
+ ],
860
+ [tabStyle, isWidthSet, defaultTabWidth]
861
+ );
862
+
863
+ const props = {
864
+ ...rest,
865
+ position,
866
+ route,
867
+ navigationState,
868
+ testID,
869
+ labelText,
870
+ accessible,
871
+ accessibilityLabel,
872
+ variant,
873
+ activeColor,
874
+ inactiveColor,
875
+ pressColor,
876
+ pressOpacity,
877
+ onMeasureLayout,
878
+ onMeasureLabelLayout,
879
+ onPress,
880
+ onLongPress,
881
+ style,
882
+ android_ripple,
883
+ } satisfies TabBarItemProps<T>;
884
+
885
+ return (
886
+ <>
887
+ {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
888
+ {renderTabBarItem ? (
889
+ renderTabBarItem({ key: route.key, ...props })
890
+ ) : (
891
+ <TabBarItem key={route.key} {...props} />
892
+ )}
893
+ </>
894
+ );
895
+ }
896
+
897
+ const MemoizedTabBarItemWrapper = React.memo(
898
+ TabBarItemWrapper
899
+ ) as typeof TabBarItemWrapper;
900
+
693
901
  const styles = StyleSheet.create({
694
902
  scroll: {
695
903
  overflow: Platform.select({ default: 'scroll', web: undefined }),
696
904
  },
697
905
  tabBar: {
698
906
  zIndex: 1,
699
- backgroundColor: '#fff',
700
- elevation: 4,
701
- ...Platform.select({
702
- default: {
703
- shadowColor: 'black',
704
- shadowOpacity: 0.1,
705
- shadowRadius: StyleSheet.hairlineWidth,
706
- shadowOffset: {
707
- height: StyleSheet.hairlineWidth,
708
- width: 0,
709
- },
710
- },
711
- web: {
712
- boxShadow: '0 1px 1px rgba(0, 0, 0, 0.1)',
713
- },
714
- }),
907
+ backgroundColor: TAB_BAR_BACKGROUND_COLOR,
908
+ borderBottomColor: TAB_BAR_BORDER_COLOR,
909
+ borderBottomWidth: 1,
715
910
  },
716
911
  tabContent: {
717
912
  flexGrow: 1,
@@ -726,4 +921,14 @@ const styles = StyleSheet.create({
726
921
  bottom: 0,
727
922
  pointerEvents: 'none',
728
923
  },
924
+ primaryIndicator: {
925
+ height: 3,
926
+ borderTopLeftRadius: 3,
927
+ borderTopRightRadius: 3,
928
+ },
929
+ secondaryIndicator: {
930
+ height: 2,
931
+ borderTopLeftRadius: 0,
932
+ borderTopRightRadius: 0,
933
+ },
729
934
  });