react-native-tab-view 5.0.0-alpha.8 → 5.0.0-alpha.9

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 (43) hide show
  1. package/lib/module/PagerViewAdapter.native.js +29 -13
  2. package/lib/module/PagerViewAdapter.native.js.map +1 -1
  3. package/lib/module/PlatformPressable.js +1 -1
  4. package/lib/module/ScrollViewAdapter.js +46 -18
  5. package/lib/module/ScrollViewAdapter.js.map +1 -1
  6. package/lib/module/TabBar.js +260 -148
  7. package/lib/module/TabBar.js.map +1 -1
  8. package/lib/module/TabBarIndicator.js +282 -168
  9. package/lib/module/TabBarIndicator.js.map +1 -1
  10. package/lib/module/TabBarItem.js +94 -44
  11. package/lib/module/TabBarItem.js.map +1 -1
  12. package/lib/module/TabBarItemLabel.js +3 -2
  13. package/lib/module/TabBarItemLabel.js.map +1 -1
  14. package/lib/module/constants.js +10 -0
  15. package/lib/module/constants.js.map +1 -0
  16. package/lib/module/useLayoutWidths.js +46 -0
  17. package/lib/module/useLayoutWidths.js.map +1 -0
  18. package/lib/typescript/src/PagerViewAdapter.native.d.ts +1 -1
  19. package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -1
  20. package/lib/typescript/src/ScrollViewAdapter.d.ts +1 -2
  21. package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -1
  22. package/lib/typescript/src/TabBar.d.ts +2 -1
  23. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  24. package/lib/typescript/src/TabBarIndicator.d.ts +4 -7
  25. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  26. package/lib/typescript/src/TabBarItem.d.ts +10 -4
  27. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  28. package/lib/typescript/src/TabBarItemLabel.d.ts +4 -3
  29. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  30. package/lib/typescript/src/constants.d.ts +8 -0
  31. package/lib/typescript/src/constants.d.ts.map +1 -0
  32. package/lib/typescript/src/useLayoutWidths.d.ts +2 -0
  33. package/lib/typescript/src/useLayoutWidths.d.ts.map +1 -0
  34. package/package.json +2 -2
  35. package/src/PagerViewAdapter.native.tsx +36 -18
  36. package/src/PlatformPressable.tsx +1 -1
  37. package/src/ScrollViewAdapter.tsx +81 -21
  38. package/src/TabBar.tsx +386 -181
  39. package/src/TabBarIndicator.tsx +323 -248
  40. package/src/TabBarItem.tsx +102 -41
  41. package/src/TabBarItemLabel.tsx +5 -4
  42. package/src/constants.tsx +8 -0
  43. package/src/useLayoutWidths.tsx +51 -0
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,
@@ -15,6 +14,12 @@ import {
15
14
  type ViewStyle,
16
15
  } from 'react-native';
17
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';
18
23
  import {
19
24
  type Props as IndicatorProps,
20
25
  TabBarIndicator,
@@ -31,10 +36,12 @@ import type {
31
36
  TabDescriptor,
32
37
  } from './types';
33
38
  import { useAnimatedValue } from './useAnimatedValue';
39
+ import { useLayoutWidths } from './useLayoutWidths';
34
40
  import { useMeasureLayout } from './useMeasureLayout';
35
41
 
36
42
  export type Props<T extends Route> = SceneRendererProps &
37
43
  EventEmitterProps & {
44
+ variant?: 'primary' | 'secondary' | undefined;
38
45
  navigationState: NavigationState<T>;
39
46
  scrollEnabled?: boolean | undefined;
40
47
  bounces?: boolean | undefined;
@@ -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
  };
@@ -155,7 +160,7 @@ const getComputedTabWidth = ({
155
160
  }
156
161
 
157
162
  if (scrollEnabled) {
158
- return (layoutWidth / 5) * 2;
163
+ return tabWidths[routes[index].key] || TAB_MIN_WIDTH;
159
164
  }
160
165
 
161
166
  const gapTotalWidth = (gap ?? 0) * (routes.length - 1);
@@ -166,6 +171,25 @@ const getComputedTabWidth = ({
166
171
  return (layoutWidth - gapTotalWidth - paddingTotalWidth) / routes.length;
167
172
  };
168
173
 
174
+ const calculateSize = (
175
+ value: ViewStyle['width'] | undefined,
176
+ referenceWidth: number
177
+ ): number | undefined => {
178
+ if (typeof value === 'number') {
179
+ return value;
180
+ }
181
+
182
+ if (typeof value === 'string' && value.endsWith('%')) {
183
+ const parsed = parseFloat(value);
184
+
185
+ if (Number.isFinite(parsed)) {
186
+ return referenceWidth * (parsed / 100);
187
+ }
188
+ }
189
+
190
+ return undefined;
191
+ };
192
+
169
193
  const getMaxScrollDistance = (tabBarWidth: number, layoutWidth: number) =>
170
194
  tabBarWidth - layoutWidth;
171
195
 
@@ -173,13 +197,15 @@ const getTranslateX = (
173
197
  scrollAmount: Animated.Value,
174
198
  maxScrollDistance: number,
175
199
  direction: LocaleDirection
176
- ) =>
177
- Animated.multiply(
200
+ ) => {
201
+ const amount =
202
+ // Android reports scroll from the opposite side in RTL
178
203
  Platform.OS === 'android' && direction === 'rtl'
179
204
  ? Animated.add(maxScrollDistance, Animated.multiply(scrollAmount, -1))
180
- : scrollAmount,
181
- direction === 'rtl' ? 1 : -1
182
- );
205
+ : scrollAmount;
206
+
207
+ return Animated.multiply(amount, direction === 'rtl' ? 1 : -1);
208
+ };
183
209
 
184
210
  const getTabBarWidth = <T extends Route>({
185
211
  routes,
@@ -228,11 +254,9 @@ const normalizeScrollValue = <T extends Route>({
228
254
  flattenedTabWidth,
229
255
  flattenedPaddingStart,
230
256
  flattenedPaddingEnd,
231
- direction,
232
257
  }: CalculationOptions & {
233
258
  routes: T[];
234
259
  value: number;
235
- direction: LocaleDirection;
236
260
  }) => {
237
261
  const tabBarWidth = getTabBarWidth({
238
262
  layoutWidth,
@@ -247,12 +271,6 @@ const normalizeScrollValue = <T extends Route>({
247
271
  const maxDistance = getMaxScrollDistance(tabBarWidth, layoutWidth);
248
272
  const scrollValue = Math.max(Math.min(value, maxDistance), 0);
249
273
 
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
274
  return scrollValue;
257
275
  };
258
276
 
@@ -311,7 +329,6 @@ const getScrollAmount = <T extends Route>({
311
329
  flattenedTabWidth,
312
330
  flattenedPaddingStart,
313
331
  flattenedPaddingEnd,
314
- direction,
315
332
  });
316
333
  };
317
334
  const getLabelTextDefault = ({ route }: Scene<Route>) => route.title;
@@ -334,9 +351,10 @@ const getTestIdDefault = ({ route }: Scene<Route>) => route.testID;
334
351
 
335
352
  // How many items measurements should we update per batch.
336
353
  // Defaults to 10, since that's whats FlatList is using in initialNumToRender.
337
- const MEASURE_PER_BATCH = 10;
354
+ const RENDER_PER_BATCH = 10;
338
355
 
339
356
  export function TabBar<T extends Route>({
357
+ variant = 'primary',
340
358
  renderIndicator = renderIndicatorDefault,
341
359
  gap = 0,
342
360
  scrollEnabled,
@@ -364,16 +382,40 @@ export function TabBar<T extends Route>({
364
382
  const containerRef = React.useRef<View>(null);
365
383
  const [layout, onLayout] = useMeasureLayout(containerRef);
366
384
 
367
- const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
385
+ // Prioritize measuring tabs upto focused item
386
+ // Since we need those measurements for calculation
387
+ const priorityKeysForLayout = navigationState.routes
388
+ .slice(0, navigationState.index + 1)
389
+ .map((r) => r.key);
390
+
391
+ const [tabWidths, onMeasureTabWidth] = useLayoutWidths(priorityKeysForLayout);
392
+ const [labelWidths, onMeasureLabelWidth] = useLayoutWidths(
393
+ priorityKeysForLayout
394
+ );
395
+
368
396
  const flatListRef = React.useRef<FlatList | null>(null);
369
397
  const isFirst = React.useRef(true);
370
- const scrollAmount = useAnimatedValue(0);
398
+
371
399
  const { routes } = navigationState;
400
+
372
401
  const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
373
- const isWidthDynamic = flattenedTabWidth === 'auto';
402
+ const isWidthAuto = flattenedTabWidth === 'auto';
403
+ const isWidthDynamic =
404
+ isWidthAuto || (scrollEnabled && flattenedTabWidth == null);
405
+
374
406
  const flattenedPaddingEnd = getFlattenedPaddingEnd(contentContainerStyle);
375
407
  const flattenedPaddingStart = getFlattenedPaddingStart(contentContainerStyle);
376
408
 
409
+ const paddingEnd = convertPaddingPercentToSize(
410
+ flattenedPaddingEnd,
411
+ layout.width
412
+ );
413
+
414
+ const paddingStart = convertPaddingPercentToSize(
415
+ flattenedPaddingStart,
416
+ layout.width
417
+ );
418
+
377
419
  const scrollOffset = getScrollAmount({
378
420
  layoutWidth: layout.width,
379
421
  routes,
@@ -399,7 +441,7 @@ export function TabBar<T extends Route>({
399
441
  return;
400
442
  }
401
443
 
402
- if (isWidthDynamic && !hasMeasuredTabWidths) {
444
+ if (isWidthAuto && !hasMeasuredTabWidths) {
403
445
  return;
404
446
  }
405
447
 
@@ -409,7 +451,7 @@ export function TabBar<T extends Route>({
409
451
  animated: true,
410
452
  });
411
453
  }
412
- }, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
454
+ }, [hasMeasuredTabWidths, isWidthAuto, scrollEnabled, scrollOffset]);
413
455
 
414
456
  const tabBarWidth = getTabBarWidth({
415
457
  layoutWidth: layout.width,
@@ -422,141 +464,86 @@ export function TabBar<T extends Route>({
422
464
  flattenedPaddingEnd,
423
465
  });
424
466
 
425
- const separatorsWidth = Math.max(0, routes.length - 1) * gap;
426
- const paddingsWidth = Math.max(
427
- 0,
428
- convertPaddingPercentToSize(flattenedPaddingStart, layout.width) +
429
- convertPaddingPercentToSize(flattenedPaddingEnd, layout.width)
430
- );
467
+ const maxScrollDistance = getMaxScrollDistance(tabBarWidth, layout.width);
468
+ const scrollAmount = useAnimatedValue(0);
469
+
470
+ React.useLayoutEffect(() => {
471
+ scrollAmount.setValue(
472
+ Platform.OS === 'android' && direction === 'rtl' ? maxScrollDistance : 0
473
+ );
474
+ }, [direction, maxScrollDistance, scrollAmount]);
431
475
 
432
476
  const translateX = React.useMemo(
433
- () =>
434
- getTranslateX(
435
- scrollAmount,
436
- getMaxScrollDistance(tabBarWidth, layout.width),
437
- direction
438
- ),
439
- [direction, layout.width, scrollAmount, tabBarWidth]
477
+ () => getTranslateX(scrollAmount, maxScrollDistance, direction),
478
+ [direction, maxScrollDistance, scrollAmount]
440
479
  );
441
480
 
442
- const measuredTabWidths = React.useRef<Record<string, number>>({});
443
- const animationFrameHandle =
444
- React.useRef<ReturnType<typeof requestAnimationFrame>>(null);
481
+ const flattenedTabStyle = StyleSheet.flatten(tabStyle);
482
+ const isTabWidthSet = flattenedTabStyle?.width !== undefined;
445
483
 
446
- const renderItem = React.useCallback(
447
- ({ item: route, index }: ListRenderItemInfo<T>) => {
448
- const {
449
- testID = getTestIdDefault({ route }),
450
- labelText = getLabelTextDefault({ route }),
451
- accessible = getAccessibleDefault({ route }),
452
- accessibilityLabel = getAccessibilityLabelDefault({ route }),
453
- ...rest
454
- } = options?.[route.key] ?? {};
455
-
456
- const onLayout = isWidthDynamic
457
- ? (e: LayoutChangeEvent) => {
458
- measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
459
-
460
- if (animationFrameHandle.current != null) {
461
- cancelAnimationFrame(animationFrameHandle.current);
462
- }
463
-
464
- animationFrameHandle.current = requestAnimationFrame(() => {
465
- setTabWidths({ ...measuredTabWidths.current });
466
- });
467
- }
468
- : undefined;
469
-
470
- const onPress = () => {
471
- const event: Scene<T> & Event = {
472
- route,
473
- defaultPrevented: false,
474
- preventDefault: () => {
475
- event.defaultPrevented = true;
476
- },
477
- };
478
-
479
- onTabPress?.(event);
480
-
481
- if (event.defaultPrevented) {
482
- return;
483
- }
484
+ // Calculate the default width for tab for FlatList to work.
485
+ const defaultTabWidth = !isWidthDynamic
486
+ ? getComputedTabWidth({
487
+ // When `isWidthDynamic` is false, every tab gets the same width and
488
+ // `getComputedTabWidth` ignores `index`, so we compute it once with index 0.
489
+ index: 0,
490
+ layoutWidth: layout.width,
491
+ routes,
492
+ scrollEnabled,
493
+ tabWidths,
494
+ flattenedTabWidth,
495
+ flattenedPaddingStart,
496
+ flattenedPaddingEnd,
497
+ gap,
498
+ })
499
+ : undefined;
484
500
 
485
- jumpTo(route.key);
486
- };
487
-
488
- const onLongPress = () => onTabLongPress?.({ route });
489
-
490
- // Calculate the default width for tab for FlatList to work
491
- const defaultTabWidth = !isWidthDynamic
492
- ? getComputedTabWidth({
493
- index,
494
- layoutWidth: layout.width,
495
- routes,
496
- scrollEnabled,
497
- tabWidths,
498
- flattenedTabWidth: getFlattenedTabWidth(tabStyle),
499
- flattenedPaddingStart: getFlattenedPaddingStart(
500
- contentContainerStyle
501
- ),
502
- flattenedPaddingEnd: getFlattenedPaddingEnd(contentContainerStyle),
503
- gap,
504
- })
505
- : undefined;
506
-
507
- const props = {
508
- ...rest,
509
- position,
510
- route,
511
- navigationState,
512
- testID,
513
- labelText,
514
- accessible,
515
- accessibilityLabel,
516
- activeColor,
517
- inactiveColor,
518
- pressColor,
519
- pressOpacity,
520
- onLayout,
521
- onPress,
522
- onLongPress,
523
- style: tabStyle,
524
- defaultTabWidth,
525
- android_ripple,
526
- } satisfies TabBarItemProps<T>;
527
-
528
- return (
529
- <>
530
- {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
531
- {renderTabBarItem ? (
532
- renderTabBarItem({ key: route.key, ...props })
533
- ) : (
534
- <TabBarItem key={route.key} {...props} />
535
- )}
536
- </>
537
- );
538
- },
501
+ const renderItem = React.useCallback(
502
+ ({ item: route, index }: ListRenderItemInfo<T>) => (
503
+ <MemoizedTabBarItemWrapper
504
+ route={route}
505
+ index={index}
506
+ option={options?.[route.key]}
507
+ position={position}
508
+ navigationState={navigationState}
509
+ variant={variant}
510
+ activeColor={activeColor}
511
+ inactiveColor={inactiveColor}
512
+ pressColor={pressColor}
513
+ pressOpacity={pressOpacity}
514
+ android_ripple={android_ripple}
515
+ tabStyle={tabStyle}
516
+ defaultTabWidth={defaultTabWidth}
517
+ isWidthSet={isTabWidthSet}
518
+ gap={gap}
519
+ onMeasureTabWidth={onMeasureTabWidth}
520
+ onMeasureLabelWidth={onMeasureLabelWidth}
521
+ onTabPress={onTabPress}
522
+ onTabLongPress={onTabLongPress}
523
+ jumpTo={jumpTo}
524
+ renderTabBarItem={renderTabBarItem}
525
+ />
526
+ ),
539
527
  [
528
+ options,
540
529
  position,
541
530
  navigationState,
542
- options,
531
+ variant,
543
532
  activeColor,
544
533
  inactiveColor,
545
534
  pressColor,
546
535
  pressOpacity,
547
- isWidthDynamic,
536
+ android_ripple,
548
537
  tabStyle,
549
- layout,
550
- routes,
551
- scrollEnabled,
552
- tabWidths,
553
- contentContainerStyle,
538
+ defaultTabWidth,
539
+ isTabWidthSet,
554
540
  gap,
555
- android_ripple,
556
- renderTabBarItem,
541
+ onMeasureTabWidth,
542
+ onMeasureLabelWidth,
557
543
  onTabPress,
558
- jumpTo,
559
544
  onTabLongPress,
545
+ jumpTo,
546
+ renderTabBarItem,
560
547
  ]
561
548
  );
562
549
 
@@ -581,16 +568,112 @@ export function TabBar<T extends Route>({
581
568
  },
582
569
  },
583
570
  ],
584
- { useNativeDriver }
571
+ { useNativeDriver: Platform.OS !== 'web' }
585
572
  ),
586
573
  [scrollAmount]
587
574
  );
588
575
 
576
+ const flattenedIndicatorStyle = StyleSheet.flatten(indicatorStyle);
577
+ const defaultIndicatorStyle =
578
+ variant === 'primary' ? styles.primaryIndicator : styles.secondaryIndicator;
579
+
580
+ const tabWidthByIndex = routes.map((_, i) =>
581
+ getComputedTabWidth({
582
+ index: i,
583
+ layoutWidth: layout.width,
584
+ routes,
585
+ scrollEnabled,
586
+ tabWidths,
587
+ flattenedTabWidth,
588
+ flattenedPaddingEnd,
589
+ flattenedPaddingStart,
590
+ gap,
591
+ })
592
+ );
593
+
594
+ const customIndicatorWidths = routes.map((_, i) =>
595
+ calculateSize(flattenedIndicatorStyle?.width, tabWidthByIndex[i])
596
+ );
597
+
598
+ const indicatorBaseWidths = routes.map((_, i) => {
599
+ if (customIndicatorWidths[i] != null) {
600
+ return customIndicatorWidths[i];
601
+ }
602
+
603
+ if (variant === 'primary') {
604
+ const labelWidth = labelWidths[routes[i].key];
605
+
606
+ return labelWidth ? Math.max(PRIMARY_INDICATOR_MIN_WIDTH, labelWidth) : 0;
607
+ }
608
+
609
+ return tabWidthByIndex[i];
610
+ });
611
+
612
+ const indicatorMargins = indicatorBaseWidths.map((width) => {
613
+ if (!width) {
614
+ return { left: 0, right: 0 };
615
+ }
616
+
617
+ const marginHorizontal =
618
+ flattenedIndicatorStyle?.marginHorizontal ??
619
+ flattenedIndicatorStyle?.margin;
620
+
621
+ const leftMargin =
622
+ (direction === 'ltr'
623
+ ? flattenedIndicatorStyle?.marginStart
624
+ : flattenedIndicatorStyle?.marginEnd) ??
625
+ flattenedIndicatorStyle?.marginLeft ??
626
+ marginHorizontal;
627
+
628
+ const rightMargin =
629
+ (direction === 'rtl'
630
+ ? flattenedIndicatorStyle?.marginStart
631
+ : flattenedIndicatorStyle?.marginEnd) ??
632
+ flattenedIndicatorStyle?.marginRight ??
633
+ marginHorizontal;
634
+
635
+ return {
636
+ left: calculateSize(leftMargin, width) ?? 0,
637
+ right: calculateSize(rightMargin, width) ?? 0,
638
+ };
639
+ });
640
+
641
+ const indicatorWidths = indicatorBaseWidths.map((width, i) => {
642
+ if (!width) {
643
+ return 0;
644
+ }
645
+
646
+ return Math.max(
647
+ 0,
648
+ width - indicatorMargins[i].left - indicatorMargins[i].right
649
+ );
650
+ });
651
+
652
+ const indicatorOffsets = routes.map((_, i) => {
653
+ const precedingTabsWidth = tabWidthByIndex
654
+ .slice(0, i)
655
+ .reduce((sum, width) => sum + width, 0);
656
+
657
+ const tabStart = precedingTabsWidth + gap * i;
658
+
659
+ const shouldCenterIndicator =
660
+ variant === 'primary' ||
661
+ (customIndicatorWidths[i] != null &&
662
+ (flattenedIndicatorStyle?.margin === 'auto' ||
663
+ flattenedIndicatorStyle?.marginHorizontal === 'auto'));
664
+
665
+ const baseOffset = shouldCenterIndicator
666
+ ? (tabWidthByIndex[i] - indicatorBaseWidths[i]) / 2
667
+ : 0;
668
+
669
+ return tabStart + baseOffset + indicatorMargins[i].left;
670
+ });
671
+
589
672
  return (
590
673
  <Animated.View
591
674
  ref={containerRef}
592
675
  onLayout={onLayout}
593
- style={[styles.tabBar, style]}
676
+ style={[styles.tabBar, { direction }, style]}
594
677
  >
595
678
  <Animated.View
596
679
  style={[
@@ -601,33 +684,18 @@ export function TabBar<T extends Route>({
601
684
  ]}
602
685
  >
603
686
  {renderIndicator({
687
+ variant,
604
688
  position,
605
689
  navigationState,
606
690
  jumpTo,
607
691
  direction,
608
- width: isWidthDynamic
609
- ? 'auto'
610
- : Math.max(
611
- 0,
612
- (tabBarWidth - separatorsWidth - paddingsWidth) / routes.length
613
- ),
692
+ widths: indicatorWidths,
693
+ offsets: indicatorOffsets,
614
694
  style: [
695
+ defaultIndicatorStyle,
615
696
  indicatorStyle,
616
- { start: flattenedPaddingStart, end: flattenedPaddingEnd },
697
+ { start: paddingStart, end: paddingEnd },
617
698
  ],
618
- getTabWidth: (i: number) =>
619
- getComputedTabWidth({
620
- index: i,
621
- layoutWidth: layout.width,
622
- routes,
623
- scrollEnabled,
624
- tabWidths,
625
- flattenedTabWidth,
626
- flattenedPaddingEnd,
627
- flattenedPaddingStart,
628
- gap,
629
- }),
630
- gap,
631
699
  })}
632
700
  </Animated.View>
633
701
  <View style={styles.scroll}>
@@ -639,7 +707,7 @@ export function TabBar<T extends Route>({
639
707
  keyboardShouldPersistTaps="handled"
640
708
  scrollEnabled={scrollEnabled}
641
709
  bounces={bounces}
642
- initialNumToRender={MEASURE_PER_BATCH}
710
+ initialNumToRender={RENDER_PER_BATCH}
643
711
  alwaysBounceHorizontal={false}
644
712
  scrollsToTop={false}
645
713
  showsHorizontalScrollIndicator={false}
@@ -658,28 +726,155 @@ export function TabBar<T extends Route>({
658
726
  );
659
727
  }
660
728
 
729
+ type TabBarItemWrapperProps<T extends Route> = {
730
+ route: T;
731
+ index: number;
732
+ option: TabDescriptor<T> | undefined;
733
+ position: Animated.AnimatedInterpolation<number>;
734
+ navigationState: NavigationState<T>;
735
+ variant: 'primary' | 'secondary';
736
+ activeColor: ColorValue | undefined;
737
+ inactiveColor: ColorValue | undefined;
738
+ pressColor: ColorValue | undefined;
739
+ pressOpacity: number | undefined;
740
+ android_ripple: PressableAndroidRippleConfig | undefined;
741
+ tabStyle: StyleProp<ViewStyle>;
742
+ defaultTabWidth: number | undefined;
743
+ isWidthSet: boolean;
744
+ gap: number;
745
+ onMeasureTabWidth: (key: string, width: number) => void;
746
+ onMeasureLabelWidth: (key: string, width: number) => void;
747
+ onTabPress: ((scene: Scene<T> & Event) => void) | undefined;
748
+ onTabLongPress: ((scene: Scene<T>) => void) | undefined;
749
+ jumpTo: (key: string) => void;
750
+ renderTabBarItem:
751
+ | ((props: TabBarItemProps<T> & { key: string }) => React.ReactElement)
752
+ | undefined;
753
+ };
754
+
755
+ function TabBarItemWrapper<T extends Route>({
756
+ route,
757
+ index,
758
+ option,
759
+ position,
760
+ navigationState,
761
+ variant,
762
+ activeColor,
763
+ inactiveColor,
764
+ pressColor,
765
+ pressOpacity,
766
+ android_ripple,
767
+ tabStyle,
768
+ defaultTabWidth,
769
+ isWidthSet,
770
+ gap,
771
+ onMeasureTabWidth,
772
+ onMeasureLabelWidth,
773
+ onTabPress,
774
+ onTabLongPress,
775
+ jumpTo,
776
+ renderTabBarItem,
777
+ }: TabBarItemWrapperProps<T>) {
778
+ const {
779
+ testID = getTestIdDefault({ route }),
780
+ labelText = getLabelTextDefault({ route }),
781
+ accessible = getAccessibleDefault({ route }),
782
+ accessibilityLabel = getAccessibilityLabelDefault({ route }),
783
+ ...rest
784
+ } = option ?? {};
785
+
786
+ const onMeasureLayout = React.useCallback(
787
+ ({ width }: { width: number }) => onMeasureTabWidth(route.key, width),
788
+ [route.key, onMeasureTabWidth]
789
+ );
790
+
791
+ const onMeasureLabelLayout = React.useCallback(
792
+ ({ width }: { width: number }) => onMeasureLabelWidth(route.key, width),
793
+ [route.key, onMeasureLabelWidth]
794
+ );
795
+
796
+ const onPress = React.useCallback(() => {
797
+ const event: Scene<T> & Event = {
798
+ route,
799
+ defaultPrevented: false,
800
+ preventDefault: () => {
801
+ event.defaultPrevented = true;
802
+ },
803
+ };
804
+
805
+ onTabPress?.(event);
806
+
807
+ if (event.defaultPrevented) {
808
+ return;
809
+ }
810
+
811
+ jumpTo(route.key);
812
+ }, [route, onTabPress, jumpTo]);
813
+
814
+ const onLongPress = React.useCallback(
815
+ () => onTabLongPress?.({ route }),
816
+ [route, onTabLongPress]
817
+ );
818
+
819
+ const style = React.useMemo(
820
+ () => [
821
+ tabStyle,
822
+ isWidthSet
823
+ ? null
824
+ : defaultTabWidth !== undefined
825
+ ? { width: defaultTabWidth }
826
+ : { minWidth: TAB_MIN_WIDTH },
827
+ ],
828
+ [tabStyle, isWidthSet, defaultTabWidth]
829
+ );
830
+
831
+ const props = {
832
+ ...rest,
833
+ position,
834
+ route,
835
+ navigationState,
836
+ testID,
837
+ labelText,
838
+ accessible,
839
+ accessibilityLabel,
840
+ variant,
841
+ activeColor,
842
+ inactiveColor,
843
+ pressColor,
844
+ pressOpacity,
845
+ onMeasureLayout,
846
+ onMeasureLabelLayout,
847
+ onPress,
848
+ onLongPress,
849
+ style,
850
+ android_ripple,
851
+ } satisfies TabBarItemProps<T>;
852
+
853
+ return (
854
+ <>
855
+ {gap > 0 && index > 0 ? <Separator width={gap} /> : null}
856
+ {renderTabBarItem ? (
857
+ renderTabBarItem({ key: route.key, ...props })
858
+ ) : (
859
+ <TabBarItem key={route.key} {...props} />
860
+ )}
861
+ </>
862
+ );
863
+ }
864
+
865
+ const MemoizedTabBarItemWrapper = React.memo(
866
+ TabBarItemWrapper
867
+ ) as typeof TabBarItemWrapper;
868
+
661
869
  const styles = StyleSheet.create({
662
870
  scroll: {
663
871
  overflow: Platform.select({ default: 'scroll', web: undefined }),
664
872
  },
665
873
  tabBar: {
666
874
  zIndex: 1,
667
- backgroundColor: '#fff',
668
- elevation: 4,
669
- ...Platform.select({
670
- default: {
671
- shadowColor: 'black',
672
- shadowOpacity: 0.1,
673
- shadowRadius: StyleSheet.hairlineWidth,
674
- shadowOffset: {
675
- height: StyleSheet.hairlineWidth,
676
- width: 0,
677
- },
678
- },
679
- web: {
680
- boxShadow: '0 1px 1px rgba(0, 0, 0, 0.1)',
681
- },
682
- }),
875
+ backgroundColor: TAB_BAR_BACKGROUND_COLOR,
876
+ borderBottomColor: TAB_BAR_BORDER_COLOR,
877
+ borderBottomWidth: 1,
683
878
  },
684
879
  tabContent: {
685
880
  flexGrow: 1,
@@ -694,4 +889,14 @@ const styles = StyleSheet.create({
694
889
  bottom: 0,
695
890
  pointerEvents: 'none',
696
891
  },
892
+ primaryIndicator: {
893
+ height: 3,
894
+ borderTopLeftRadius: 3,
895
+ borderTopRightRadius: 3,
896
+ },
897
+ secondaryIndicator: {
898
+ height: 2,
899
+ borderTopLeftRadius: 0,
900
+ borderTopRightRadius: 0,
901
+ },
697
902
  });