@react-navigation/bottom-tabs 8.0.0-alpha.27 → 8.0.0-alpha.29

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.
@@ -18,14 +18,21 @@ import * as React from 'react';
18
18
  import {
19
19
  Animated,
20
20
  type ColorValue,
21
+ type NativeSyntheticEvent,
21
22
  Platform,
22
23
  PlatformColor,
23
24
  StyleSheet,
24
25
  } from 'react-native';
25
26
  import {
26
- type PlatformIcon,
27
+ type PlatformIconAndroid,
28
+ type PlatformIconIOS,
29
+ type PlatformIconShared,
27
30
  Tabs,
28
- type TabsScreenItemStateAppearance,
31
+ type TabsBottomAccessoryEnvironment,
32
+ type TabSelectedEvent,
33
+ type TabSelectionRejectedEvent,
34
+ type TabsScreenItemStateAppearanceAndroid,
35
+ type TabsScreenItemStateAppearanceIOS,
29
36
  } from 'react-native-screens';
30
37
 
31
38
  import type {
@@ -44,6 +51,37 @@ type Props = BottomTabNavigationConfig & {
44
51
  descriptors: BottomTabDescriptorMap;
45
52
  };
46
53
 
54
+ type TabSelectionPreventedEvent = {
55
+ selectedScreenKey: string;
56
+ provenance: number;
57
+ preventedScreenKey: string;
58
+ };
59
+
60
+ type PlatformIcon = {
61
+ ios?: PlatformIconIOS;
62
+ android?: PlatformIconAndroid;
63
+ shared?: PlatformIconShared;
64
+ };
65
+
66
+ type ConfirmedState = {
67
+ routeKey: string;
68
+ provenance: number;
69
+ };
70
+
71
+ type NativeState = {
72
+ lastTransition: { from: string; to: string } | null;
73
+ confirmed: ConfirmedState;
74
+ };
75
+
76
+ type NativeAction =
77
+ | { type: 'CLEAR_TRANSITION'; to: string }
78
+ | {
79
+ type: 'TRACK_TRANSITION';
80
+ confirmed: ConfirmedState;
81
+ lastTransition: NonNullable<NativeState['lastTransition']>;
82
+ }
83
+ | { type: 'CONFIRM_STATE'; confirmed: ConfirmedState };
84
+
47
85
  const ICON_SIZE = Platform.select({
48
86
  ios: 25,
49
87
  default: 24,
@@ -53,6 +91,32 @@ const meta = {
53
91
  type: 'native-tabs',
54
92
  };
55
93
 
94
+ function reducer(state: NativeState, action: NativeAction): NativeState {
95
+ switch (action.type) {
96
+ case 'TRACK_TRANSITION':
97
+ return {
98
+ ...state,
99
+ lastTransition: action.lastTransition,
100
+ confirmed: action.confirmed,
101
+ };
102
+ case 'CLEAR_TRANSITION':
103
+ return state.lastTransition?.to === action.to
104
+ ? {
105
+ ...state,
106
+ lastTransition: null,
107
+ }
108
+ : state;
109
+ case 'CONFIRM_STATE':
110
+ return state.confirmed.routeKey === action.confirmed.routeKey &&
111
+ state.confirmed.provenance === action.confirmed.provenance
112
+ ? state
113
+ : {
114
+ ...state,
115
+ confirmed: action.confirmed,
116
+ };
117
+ }
118
+ }
119
+
56
120
  export function BottomTabViewNative({
57
121
  state,
58
122
  navigation,
@@ -69,10 +133,13 @@ export function BottomTabViewNative({
69
133
  setLoaded([...loaded, focusedRouteKey]);
70
134
  }
71
135
 
72
- const [pendingNavigation, setPendingNavigation] = React.useState<{
73
- from: string;
74
- to: string;
75
- } | null>(null);
136
+ const [nativeState, dispatch] = React.useReducer(reducer, {
137
+ lastTransition: null,
138
+ confirmed: {
139
+ routeKey: focusedRouteKey,
140
+ provenance: 0,
141
+ },
142
+ });
76
143
 
77
144
  const previousRouteKeyRef = React.useRef(focusedRouteKey);
78
145
 
@@ -99,21 +166,142 @@ export function BottomTabViewNative({
99
166
 
100
167
  previousRouteKeyRef.current = focusedRouteKey;
101
168
 
102
- // Delay clearing `isAnimating`
103
- // This will give time for `popToAction` to get handled before pause
169
+ // We dispatch `popToTop` for unfocused tabs when `popToTopOnBlur` is true
170
+ // So we delay clearing `lastTransition` to keep the screen active for longer
171
+ // This gives time for the action to be handled before the screen is paused,
104
172
  const timer = setTimeout(() => {
105
- setPendingNavigation((pending) => {
106
- if (pending?.to === focusedRouteKey) {
107
- return null;
108
- }
109
-
110
- return pending;
173
+ dispatch({
174
+ type: 'CLEAR_TRANSITION',
175
+ to: focusedRouteKey,
111
176
  });
112
177
  }, 32);
113
178
 
114
179
  return () => clearTimeout(timer);
115
180
  }, [descriptors, focusedRouteKey, navigation, state.index, state.routes]);
116
181
 
182
+ const navigate = (
183
+ route: (typeof state.routes)[number],
184
+ confirmed: ConfirmedState
185
+ ) => {
186
+ dispatch({
187
+ type: 'TRACK_TRANSITION',
188
+ confirmed,
189
+ lastTransition: {
190
+ from: previousRouteKeyRef.current,
191
+ to: route.key,
192
+ },
193
+ });
194
+
195
+ navigation.dispatch({
196
+ ...CommonActions.navigate(route.name, route.params),
197
+ target: state.key,
198
+ });
199
+ };
200
+
201
+ // Native tabs are the source of truth for the selected tab.
202
+ // JS sends a requested tab with the native provenance it was based on.
203
+ // Native replies with the selected tab and its new provenance.
204
+ const onTabSelected = (event: NativeSyntheticEvent<TabSelectedEvent>) => {
205
+ const { selectedScreenKey, provenance, actionOrigin } = event.nativeEvent;
206
+
207
+ const confirmed = {
208
+ routeKey: selectedScreenKey,
209
+ provenance,
210
+ };
211
+
212
+ const route = state.routes.find((route) => route.key === selectedScreenKey);
213
+
214
+ if (!route) {
215
+ console.error(
216
+ `Received 'tabSelected' for route that doesn't exist: ${selectedScreenKey}`
217
+ );
218
+
219
+ return;
220
+ }
221
+
222
+ if (actionOrigin === 'user') {
223
+ const event = navigation.emit({
224
+ type: 'tabPress',
225
+ target: route.key,
226
+ canPreventDefault: true,
227
+ });
228
+
229
+ if (event.defaultPrevented) {
230
+ throw new Error(
231
+ "Preventing default for 'tabPress' is not supported with native tab bar. Use the 'tabBarSelectionEnabled: false' option instead."
232
+ );
233
+ }
234
+ }
235
+
236
+ if (actionOrigin === 'programmatic-js' || focusedRouteKey === route.key) {
237
+ dispatch({
238
+ type: 'CONFIRM_STATE',
239
+ confirmed,
240
+ });
241
+
242
+ return;
243
+ }
244
+
245
+ navigate(route, confirmed);
246
+ };
247
+
248
+ // If native rejects a JS request, keep native as the source of truth
249
+ // and move JS back to the tab that native says is selected.
250
+ const onTabSelectionRejected = (
251
+ event: NativeSyntheticEvent<TabSelectionRejectedEvent>
252
+ ) => {
253
+ const { selectedScreenKey, provenance } = event.nativeEvent;
254
+
255
+ const confirmed = {
256
+ routeKey: selectedScreenKey,
257
+ provenance,
258
+ };
259
+
260
+ const route = state.routes.find((route) => route.key === selectedScreenKey);
261
+
262
+ if (!route) {
263
+ console.error(
264
+ `Received 'tabSelectionRejected' for route that doesn't exist: ${selectedScreenKey}`
265
+ );
266
+
267
+ return;
268
+ }
269
+
270
+ if (focusedRouteKey === route.key) {
271
+ dispatch({
272
+ type: 'CONFIRM_STATE',
273
+ confirmed,
274
+ });
275
+
276
+ return;
277
+ }
278
+
279
+ navigate(route, confirmed);
280
+ };
281
+
282
+ const onTabSelectionPrevented = (
283
+ event: NativeSyntheticEvent<TabSelectionPreventedEvent>
284
+ ) => {
285
+ const { selectedScreenKey, provenance, preventedScreenKey } =
286
+ event.nativeEvent;
287
+
288
+ const confirmed = {
289
+ routeKey: selectedScreenKey,
290
+ provenance,
291
+ };
292
+
293
+ dispatch({
294
+ type: 'CONFIRM_STATE',
295
+ confirmed,
296
+ });
297
+
298
+ navigation.emit({
299
+ type: 'tabPress',
300
+ target: preventedScreenKey,
301
+ canPreventDefault: true,
302
+ });
303
+ };
304
+
117
305
  const currentOptions = descriptors[state.routes[state.index].key]?.options;
118
306
 
119
307
  const {
@@ -241,68 +429,24 @@ export function BottomTabViewNative({
241
429
  ? tabBarElement
242
430
  : null}
243
431
  <Tabs.Host
432
+ navStateRequest={{
433
+ selectedScreenKey: focusedRouteKey,
434
+ baseProvenance: nativeState.confirmed.provenance,
435
+ }}
436
+ rejectStaleNavStateUpdates
437
+ onTabSelected={onTabSelected}
438
+ onTabSelectionRejected={onTabSelectionRejected}
439
+ onTabSelectionPrevented={onTabSelectionPrevented}
244
440
  tabBarHidden={hasCustomTabBar || shouldHideTabBar}
245
- bottomAccessory={
246
- bottomAccessory
247
- ? (environment) => bottomAccessory({ placement: environment })
248
- : undefined
249
- }
250
- tabBarItemLabelVisibilityMode={
251
- currentOptions?.tabBarLabelVisibilityMode
252
- }
253
- tabBarControllerMode={tabBarControllerMode}
254
- tabBarMinimizeBehavior={tabBarMinimizeBehavior}
255
- tabBarTintColor={activeTintColor}
256
- tabBarItemIconColor={inactiveTintColor}
257
- tabBarItemIconColorActive={activeTintColor}
258
- tabBarItemTitleFontColor={inactiveTintColor ?? fontColor}
259
- tabBarItemTitleFontColorActive={activeTintColor}
260
- tabBarItemTitleFontFamily={fontFamily}
261
- tabBarItemTitleFontWeight={fontWeight}
262
- tabBarItemTitleFontSize={fontSize}
263
- tabBarItemTitleFontSizeActive={fontSize}
264
- tabBarItemTitleFontStyle={fontStyle}
265
- tabBarBackgroundColor={backgroundColor}
266
- tabBarItemActiveIndicatorColor={activeIndicatorColor}
267
- tabBarItemActiveIndicatorEnabled={
268
- currentOptions?.tabBarActiveIndicatorEnabled
269
- }
270
- tabBarItemRippleColor={currentOptions?.tabBarRippleColor}
271
- experimentalControlNavigationStateInJS={false}
272
- onNativeFocusChange={(e) => {
273
- const route = state.routes.find(
274
- (route) => route.key === e.nativeEvent.tabKey
275
- );
276
-
277
- if (route) {
278
- const event = navigation.emit({
279
- type: 'tabPress',
280
- target: route.key,
281
- canPreventDefault: true,
282
- });
283
-
284
- if (event.defaultPrevented) {
285
- throw new Error(
286
- "Preventing default for 'tabPress' is not supported with native tab bar."
287
- );
288
- }
289
-
290
- const isFocused =
291
- state.index ===
292
- state.routes.findIndex((r) => r.key === route.key);
293
-
294
- if (!isFocused) {
295
- setPendingNavigation({
296
- from: previousRouteKeyRef.current,
297
- to: route.key,
298
- });
299
-
300
- navigation.dispatch({
301
- ...CommonActions.navigate(route.name, route.params),
302
- target: state.key,
303
- });
304
- }
305
- }
441
+ colorScheme={dark ? 'dark' : 'light'}
442
+ ios={{
443
+ bottomAccessory: bottomAccessory
444
+ ? (environment: TabsBottomAccessoryEnvironment) =>
445
+ bottomAccessory({ placement: environment })
446
+ : undefined,
447
+ tabBarControllerMode,
448
+ tabBarMinimizeBehavior,
449
+ tabBarTintColor: activeTintColor,
306
450
  }}
307
451
  >
308
452
  {state.routes.map((route, index) => {
@@ -315,6 +459,7 @@ export function BottomTabViewNative({
315
459
  lazy = true,
316
460
  inactiveBehavior = 'pause',
317
461
  tabBarLabel,
462
+ tabBarSelectionEnabled,
318
463
  tabBarBadgeStyle,
319
464
  tabBarIcon,
320
465
  tabBarBadge,
@@ -324,7 +469,6 @@ export function BottomTabViewNative({
324
469
  tabBarAccessibilityLabel,
325
470
  tabBarButtonTestID,
326
471
  sceneStyle,
327
- scrollEdgeEffects,
328
472
  overrideScrollViewContentInsetAdjustmentBehavior,
329
473
  } = options;
330
474
 
@@ -345,7 +489,7 @@ export function BottomTabViewNative({
345
489
  const badgeTextColor =
346
490
  tabBarBadgeStyle?.color ?? Color.foreground(badgeBackgroundColor);
347
491
 
348
- const tabItemAppearance: TabsScreenItemStateAppearance = {
492
+ const tabItemAppearance: TabsScreenItemStateAppearanceIOS = {
349
493
  tabBarItemTitleFontFamily: fontFamily,
350
494
  tabBarItemTitleFontSize: fontSize,
351
495
  tabBarItemTitleFontWeight: fontWeight,
@@ -355,6 +499,18 @@ export function BottomTabViewNative({
355
499
  tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
356
500
  };
357
501
 
502
+ const normalTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
503
+ {
504
+ tabBarItemTitleFontColor: inactiveTintColor ?? fontColor,
505
+ tabBarItemIconColor: inactiveTintColor,
506
+ };
507
+
508
+ const selectedTabItemAppearance: TabsScreenItemStateAppearanceAndroid =
509
+ {
510
+ tabBarItemTitleFontColor: activeTintColor,
511
+ tabBarItemIconColor: activeTintColor,
512
+ };
513
+
358
514
  const getIcon = (selected: boolean) => {
359
515
  if (typeof tabBarIcon === 'function') {
360
516
  const result = tabBarIcon({
@@ -393,8 +549,8 @@ export function BottomTabViewNative({
393
549
  const isActive =
394
550
  inactiveBehavior === 'none' ||
395
551
  isPreloaded ||
396
- pendingNavigation?.from === route.key ||
397
- pendingNavigation?.to === route.key ||
552
+ nativeState.lastTransition?.from === route.key ||
553
+ nativeState.lastTransition?.to === route.key ||
398
554
  (lazy === false && !loaded.includes(route.key));
399
555
 
400
556
  return (
@@ -402,73 +558,75 @@ export function BottomTabViewNative({
402
558
  onWillAppear={() => onTransitionStart({ route })}
403
559
  onDidAppear={() => onTransitionEnd({ route })}
404
560
  key={route.key}
405
- tabKey={route.key}
406
- icon={icon}
407
- selectedIcon={selectedIcon?.ios ?? selectedIcon?.shared}
408
- tabBarItemBadgeBackgroundColor={badgeBackgroundColor}
409
- tabBarItemBadgeTextColor={badgeTextColor}
561
+ screenKey={route.key}
410
562
  tabBarItemAccessibilityLabel={tabBarAccessibilityLabel}
411
563
  tabBarItemTestID={tabBarButtonTestID}
564
+ preventNativeSelection={tabBarSelectionEnabled === false}
412
565
  badgeValue={tabBarBadge?.toString()}
413
- systemItem={tabBarSystemItem}
414
- isFocused={isFocused}
415
566
  title={tabTitle}
416
- scrollEdgeEffects={{
417
- top:
418
- scrollEdgeEffects?.top === 'auto'
419
- ? 'automatic'
420
- : scrollEdgeEffects?.top,
421
- bottom:
422
- scrollEdgeEffects?.bottom === 'auto'
423
- ? 'automatic'
424
- : scrollEdgeEffects?.bottom,
425
- left:
426
- scrollEdgeEffects?.left === 'auto'
427
- ? 'automatic'
428
- : scrollEdgeEffects?.left,
429
- right:
430
- scrollEdgeEffects?.right === 'auto'
431
- ? 'automatic'
432
- : scrollEdgeEffects?.right,
433
- }}
434
- scrollEdgeAppearance={{
435
- tabBarBackgroundColor,
436
- tabBarShadowColor,
437
- tabBarBlurEffect,
438
- stacked: {
439
- normal: tabItemAppearance,
440
- },
441
- inline: {
442
- normal: tabItemAppearance,
443
- },
444
- compactInline: {
445
- normal: tabItemAppearance,
446
- },
447
- }}
448
- standardAppearance={{
449
- tabBarBackgroundColor,
450
- tabBarShadowColor,
451
- tabBarBlurEffect,
452
- stacked: {
453
- normal: tabItemAppearance,
454
- },
455
- inline: {
456
- normal: tabItemAppearance,
457
- },
458
- compactInline: {
459
- normal: tabItemAppearance,
460
- },
461
- }}
462
567
  specialEffects={{
463
568
  repeatedTabSelection: {
464
569
  popToRoot: true,
465
570
  scrollToTop: true,
466
571
  },
467
572
  }}
468
- overrideScrollViewContentInsetAdjustmentBehavior={
469
- overrideScrollViewContentInsetAdjustmentBehavior
470
- }
471
- experimental_userInterfaceStyle={dark ? 'dark' : 'light'}
573
+ android={{
574
+ icon: icon?.android ?? icon?.shared,
575
+ selectedIcon: selectedIcon?.android ?? selectedIcon?.shared,
576
+ standardAppearance: {
577
+ tabBarBackgroundColor:
578
+ tabBarBackgroundColor ?? backgroundColor,
579
+ tabBarItemRippleColor: currentOptions?.tabBarRippleColor,
580
+ tabBarItemLabelVisibilityMode:
581
+ currentOptions?.tabBarLabelVisibilityMode,
582
+ normal: normalTabItemAppearance,
583
+ selected: selectedTabItemAppearance,
584
+ tabBarItemActiveIndicatorColor: activeIndicatorColor,
585
+ tabBarItemActiveIndicatorEnabled:
586
+ currentOptions?.tabBarActiveIndicatorEnabled,
587
+ tabBarItemTitleFontFamily: fontFamily,
588
+ tabBarItemTitleFontWeight: fontWeight,
589
+ tabBarItemTitleSmallLabelFontSize: fontSize,
590
+ tabBarItemTitleLargeLabelFontSize: fontSize,
591
+ tabBarItemTitleFontStyle: fontStyle,
592
+ tabBarItemBadgeBackgroundColor: badgeBackgroundColor,
593
+ tabBarItemBadgeTextColor: badgeTextColor,
594
+ },
595
+ }}
596
+ ios={{
597
+ icon: icon?.ios ?? icon?.shared,
598
+ selectedIcon: selectedIcon?.ios ?? selectedIcon?.shared,
599
+ systemItem: tabBarSystemItem,
600
+ scrollEdgeAppearance: {
601
+ tabBarBackgroundColor,
602
+ tabBarShadowColor,
603
+ tabBarBlurEffect,
604
+ stacked: {
605
+ normal: tabItemAppearance,
606
+ },
607
+ inline: {
608
+ normal: tabItemAppearance,
609
+ },
610
+ compactInline: {
611
+ normal: tabItemAppearance,
612
+ },
613
+ },
614
+ standardAppearance: {
615
+ tabBarBackgroundColor,
616
+ tabBarShadowColor,
617
+ tabBarBlurEffect,
618
+ stacked: {
619
+ normal: tabItemAppearance,
620
+ },
621
+ inline: {
622
+ normal: tabItemAppearance,
623
+ },
624
+ compactInline: {
625
+ normal: tabItemAppearance,
626
+ },
627
+ },
628
+ overrideScrollViewContentInsetAdjustmentBehavior,
629
+ }}
472
630
  >
473
631
  {lazy &&
474
632
  !loaded.includes(route.key) &&
@@ -1 +0,0 @@
1
- {"version":3,"names":["BottomTabViewCustom","BottomTabViewNative","jsx","_jsx","BottomTabView","props","implementation"],"sourceRoot":"../../../src","sources":["views/BottomTabViewCommon.tsx"],"mappings":";;AAUA,SAASA,mBAAmB,QAAQ,0BAAuB;AAC3D,SAASC,mBAAmB,QAAQ,uBAAuB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAQ5D,OAAO,SAASC,aAAaA,CAACC,KAAY,EAAE;EAC1C,IAAIA,KAAK,CAACC,cAAc,KAAK,QAAQ,EAAE;IACrC,oBAAOH,IAAA,CAACH,mBAAmB;MAAA,GAAKK;IAAK,CAAG,CAAC;EAC3C;EAEA,oBAAOF,IAAA,CAACF,mBAAmB;IAAA,GAAKI;EAAK,CAAG,CAAC;AAC3C","ignoreList":[]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"BottomTabViewCommon.d.ts","sourceRoot":"","sources":["../../../../src/views/BottomTabViewCommon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,sBAAsB,EACtB,yBAAyB,EACzB,0BAA0B,EAC3B,MAAM,UAAU,CAAC;AAIlB,KAAK,KAAK,GAAG,yBAAyB,GAAG;IACvC,KAAK,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACzC,UAAU,EAAE,0BAA0B,CAAC;IACvC,WAAW,EAAE,sBAAsB,CAAC;CACrC,CAAC;AAEF,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,2CAMzC"}