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.
- package/lib/module/PagerViewAdapter.native.js +29 -13
- package/lib/module/PagerViewAdapter.native.js.map +1 -1
- package/lib/module/PlatformPressable.js +1 -1
- package/lib/module/ScrollViewAdapter.js +46 -18
- package/lib/module/ScrollViewAdapter.js.map +1 -1
- package/lib/module/TabBar.js +260 -148
- package/lib/module/TabBar.js.map +1 -1
- package/lib/module/TabBarIndicator.js +282 -168
- package/lib/module/TabBarIndicator.js.map +1 -1
- package/lib/module/TabBarItem.js +94 -44
- package/lib/module/TabBarItem.js.map +1 -1
- package/lib/module/TabBarItemLabel.js +3 -2
- package/lib/module/TabBarItemLabel.js.map +1 -1
- package/lib/module/constants.js +10 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/useLayoutWidths.js +46 -0
- package/lib/module/useLayoutWidths.js.map +1 -0
- package/lib/typescript/src/PagerViewAdapter.native.d.ts +1 -1
- package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -1
- package/lib/typescript/src/ScrollViewAdapter.d.ts +1 -2
- package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -1
- package/lib/typescript/src/TabBar.d.ts +2 -1
- package/lib/typescript/src/TabBar.d.ts.map +1 -1
- package/lib/typescript/src/TabBarIndicator.d.ts +4 -7
- package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItem.d.ts +10 -4
- package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
- package/lib/typescript/src/TabBarItemLabel.d.ts +4 -3
- package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
- package/lib/typescript/src/constants.d.ts +8 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/useLayoutWidths.d.ts +2 -0
- package/lib/typescript/src/useLayoutWidths.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/PagerViewAdapter.native.tsx +36 -18
- package/src/PlatformPressable.tsx +1 -1
- package/src/ScrollViewAdapter.tsx +81 -21
- package/src/TabBar.tsx +386 -181
- package/src/TabBarIndicator.tsx +323 -248
- package/src/TabBarItem.tsx +102 -41
- package/src/TabBarItemLabel.tsx +5 -4
- package/src/constants.tsx +8 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
398
|
+
|
|
371
399
|
const { routes } = navigationState;
|
|
400
|
+
|
|
372
401
|
const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
|
|
373
|
-
const
|
|
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 (
|
|
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,
|
|
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
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
|
443
|
-
const
|
|
444
|
-
React.useRef<ReturnType<typeof requestAnimationFrame>>(null);
|
|
481
|
+
const flattenedTabStyle = StyleSheet.flatten(tabStyle);
|
|
482
|
+
const isTabWidthSet = flattenedTabStyle?.width !== undefined;
|
|
445
483
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
531
|
+
variant,
|
|
543
532
|
activeColor,
|
|
544
533
|
inactiveColor,
|
|
545
534
|
pressColor,
|
|
546
535
|
pressOpacity,
|
|
547
|
-
|
|
536
|
+
android_ripple,
|
|
548
537
|
tabStyle,
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
scrollEnabled,
|
|
552
|
-
tabWidths,
|
|
553
|
-
contentContainerStyle,
|
|
538
|
+
defaultTabWidth,
|
|
539
|
+
isTabWidthSet,
|
|
554
540
|
gap,
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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:
|
|
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={
|
|
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:
|
|
668
|
-
|
|
669
|
-
|
|
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
|
});
|