@react-navigation/bottom-tabs 8.0.0-alpha.5 → 8.0.0-alpha.50

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 (84) hide show
  1. package/lib/module/index.js +1 -1
  2. package/lib/module/index.js.map +1 -1
  3. package/lib/module/navigators/createBottomTabNavigator.js +4 -25
  4. package/lib/module/navigators/createBottomTabNavigator.js.map +1 -1
  5. package/lib/module/utils/useBottomTabAnimation.js +1 -1
  6. package/lib/module/utils/useBottomTabAnimation.js.map +1 -1
  7. package/lib/module/utils/useBottomTabBarHeight.js +1 -1
  8. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -1
  9. package/lib/module/utils/useIsKeyboardShown.js.map +1 -1
  10. package/lib/module/views/BottomTabBar.js +40 -29
  11. package/lib/module/views/BottomTabBar.js.map +1 -1
  12. package/lib/module/views/BottomTabItem.js +3 -5
  13. package/lib/module/views/BottomTabItem.js.map +1 -1
  14. package/lib/module/views/{BottomTabViewCommon.js → BottomTabView.js} +1 -1
  15. package/lib/module/views/BottomTabView.js.map +1 -0
  16. package/lib/module/views/BottomTabViewCustom.js +97 -81
  17. package/lib/module/views/BottomTabViewCustom.js.map +1 -1
  18. package/lib/module/views/BottomTabViewNativeImpl.js +339 -121
  19. package/lib/module/views/BottomTabViewNativeImpl.js.map +1 -1
  20. package/lib/module/views/Deferred.js +41 -0
  21. package/lib/module/views/Deferred.js.map +1 -0
  22. package/lib/module/views/ScreenContent.js.map +1 -1
  23. package/lib/module/views/TabBarIcon.js +11 -48
  24. package/lib/module/views/TabBarIcon.js.map +1 -1
  25. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +1 -1
  26. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -1
  27. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +1 -1
  28. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -1
  29. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +1 -1
  30. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -1
  31. package/lib/typescript/src/index.d.ts +11 -11
  32. package/lib/typescript/src/index.d.ts.map +1 -1
  33. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +9 -15
  34. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  35. package/lib/typescript/src/types.d.ts +66 -104
  36. package/lib/typescript/src/types.d.ts.map +1 -1
  37. package/lib/typescript/src/utils/BottomTabAnimationContext.d.ts +1 -1
  38. package/lib/typescript/src/utils/BottomTabAnimationContext.d.ts.map +1 -1
  39. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +1 -1
  40. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -1
  41. package/lib/typescript/src/utils/useBottomTabAnimation.d.ts +1 -1
  42. package/lib/typescript/src/utils/useBottomTabAnimation.d.ts.map +1 -1
  43. package/lib/typescript/src/utils/useTabBarPosition.d.ts +1 -1
  44. package/lib/typescript/src/utils/useTabBarPosition.d.ts.map +1 -1
  45. package/lib/typescript/src/views/BottomTabBar.d.ts +3 -2
  46. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  47. package/lib/typescript/src/views/BottomTabItem.d.ts +20 -20
  48. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  49. package/lib/typescript/src/views/{BottomTabViewCommon.d.ts → BottomTabView.d.ts} +3 -3
  50. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -0
  51. package/lib/typescript/src/views/BottomTabViewCustom.d.ts +3 -2
  52. package/lib/typescript/src/views/BottomTabViewCustom.d.ts.map +1 -1
  53. package/lib/typescript/src/views/BottomTabViewNative.android.d.ts +1 -1
  54. package/lib/typescript/src/views/BottomTabViewNative.android.d.ts.map +1 -1
  55. package/lib/typescript/src/views/BottomTabViewNative.d.ts +1 -1
  56. package/lib/typescript/src/views/BottomTabViewNative.d.ts.map +1 -1
  57. package/lib/typescript/src/views/BottomTabViewNative.ios.d.ts +1 -1
  58. package/lib/typescript/src/views/BottomTabViewNative.ios.d.ts.map +1 -1
  59. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts +3 -2
  60. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts.map +1 -1
  61. package/lib/typescript/src/views/Deferred.d.ts +31 -0
  62. package/lib/typescript/src/views/Deferred.d.ts.map +1 -0
  63. package/lib/typescript/src/views/ScreenContent.d.ts +4 -4
  64. package/lib/typescript/src/views/ScreenContent.d.ts.map +1 -1
  65. package/lib/typescript/src/views/TabBarIcon.d.ts +8 -8
  66. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  67. package/package.json +22 -22
  68. package/src/index.tsx +2 -2
  69. package/src/navigators/createBottomTabNavigator.tsx +10 -75
  70. package/src/types.tsx +66 -112
  71. package/src/utils/useAnimatedHashMap.tsx +1 -1
  72. package/src/utils/useBottomTabAnimation.tsx +1 -1
  73. package/src/utils/useBottomTabBarHeight.tsx +1 -1
  74. package/src/utils/useIsKeyboardShown.tsx +2 -2
  75. package/src/views/BottomTabBar.tsx +68 -25
  76. package/src/views/BottomTabItem.tsx +19 -21
  77. package/src/views/BottomTabViewCustom.tsx +172 -143
  78. package/src/views/BottomTabViewNativeImpl.tsx +480 -161
  79. package/src/views/Deferred.tsx +57 -0
  80. package/src/views/ScreenContent.tsx +1 -2
  81. package/src/views/TabBarIcon.tsx +13 -56
  82. package/lib/module/views/BottomTabViewCommon.js.map +0 -1
  83. package/lib/typescript/src/views/BottomTabViewCommon.d.ts.map +0 -1
  84. /package/src/views/{BottomTabViewCommon.tsx → BottomTabView.tsx} +0 -0
@@ -1,10 +1,9 @@
1
1
  import {
2
+ ActivityView,
2
3
  Container,
3
- Lazy,
4
4
  SafeAreaProviderCompat,
5
5
  } from '@react-navigation/elements/internal';
6
6
  import {
7
- type NavigationAction,
8
7
  type ParamListBase,
9
8
  StackActions,
10
9
  type TabNavigationState,
@@ -17,7 +16,6 @@ import {
17
16
  StyleSheet,
18
17
  type ViewStyle,
19
18
  } from 'react-native';
20
- import { Screen, ScreenContainer } from 'react-native-screens';
21
19
 
22
20
  import {
23
21
  FadeTransition,
@@ -37,6 +35,7 @@ import { BottomTabBarHeightContext } from '../utils/BottomTabBarHeightContext';
37
35
  import { useAnimatedHashMap } from '../utils/useAnimatedHashMap';
38
36
  import { useTabBarPosition } from '../utils/useTabBarPosition';
39
37
  import { BottomTabBar, getTabBarHeight } from './BottomTabBar';
38
+ import { Deferred } from './Deferred';
40
39
  import { ScreenContent } from './ScreenContent';
41
40
 
42
41
  type Props = BottomTabNavigationConfig & {
@@ -45,11 +44,6 @@ type Props = BottomTabNavigationConfig & {
45
44
  descriptors: BottomTabDescriptorMap;
46
45
  };
47
46
 
48
- const EPSILON = 1e-5;
49
- const STATE_INACTIVE = 0;
50
- const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
51
- const STATE_ON_TOP = 2;
52
-
53
47
  const NAMED_TRANSITIONS_PRESETS = {
54
48
  fade: FadeTransition,
55
49
  shift: ShiftTransition,
@@ -83,36 +77,48 @@ export function BottomTabViewCustom({
83
77
  state,
84
78
  navigation,
85
79
  descriptors,
86
- detachInactiveScreens = Platform.OS === 'web' ||
87
- Platform.OS === 'android' ||
88
- Platform.OS === 'ios',
89
80
  }: Props) {
90
81
  const { routes } = state;
91
- const focusedRouteKey = routes[state.index].key;
82
+ const focusedRoute = routes[state.index];
83
+
84
+ if (focusedRoute == null) {
85
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
86
+ }
87
+
88
+ const focusedRouteKey = focusedRoute.key;
89
+ const focusedOptions = descriptors[focusedRouteKey]?.options;
90
+
91
+ const [loaded, setLoaded] = React.useState([focusedRouteKey]);
92
+
93
+ if (!loaded.includes(focusedRouteKey)) {
94
+ setLoaded([...loaded, focusedRouteKey]);
95
+ }
96
+
97
+ const [lastUpdate, setLastUpdate] = React.useState<{
98
+ current: string;
99
+ previous?: string;
100
+ animating: boolean;
101
+ }>({
102
+ current: focusedRouteKey,
103
+ animating: false,
104
+ });
105
+
106
+ if (lastUpdate.current !== focusedRouteKey) {
107
+ setLastUpdate({
108
+ current: focusedRouteKey,
109
+ previous: lastUpdate.current,
110
+ animating: true,
111
+ });
112
+ }
92
113
 
93
- const previousRouteKeyRef = React.useRef(focusedRouteKey);
94
114
  const tabAnims = useAnimatedHashMap(state);
95
115
 
116
+ const previousRouteKeyRef = React.useRef(focusedRouteKey);
117
+
96
118
  React.useEffect(() => {
97
119
  const previousRouteKey = previousRouteKeyRef.current;
98
120
 
99
- let popToTopAction: NavigationAction | undefined;
100
-
101
- if (
102
- previousRouteKey !== focusedRouteKey &&
103
- descriptors[previousRouteKey]?.options.popToTopOnBlur
104
- ) {
105
- const prevRoute = state.routes.find(
106
- (route) => route.key === previousRouteKey
107
- );
108
-
109
- if (prevRoute?.state?.type === 'stack' && prevRoute.state.key) {
110
- popToTopAction = {
111
- ...StackActions.popToTop(),
112
- target: prevRoute.state.key,
113
- };
114
- }
115
- }
121
+ let timer: ReturnType<typeof setTimeout> | undefined;
116
122
 
117
123
  const animateToIndex = () => {
118
124
  if (previousRouteKey !== focusedRouteKey) {
@@ -122,42 +128,71 @@ export function BottomTabViewCustom({
122
128
  });
123
129
  }
124
130
 
125
- Animated.parallel(
126
- state.routes
127
- .map((route, index) => {
128
- const { options } = descriptors[route.key];
129
- const {
130
- animation = 'none',
131
- transitionSpec = NAMED_TRANSITIONS_PRESETS[animation]
132
- .transitionSpec,
133
- } = options;
134
-
135
- let spec = transitionSpec;
136
-
137
- if (
138
- route.key !== previousRouteKey &&
139
- route.key !== focusedRouteKey
140
- ) {
141
- // Don't animate if the screen is not previous one or new one
142
- // This will avoid flicker for screens not involved in the transition
143
- spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
144
- }
145
-
146
- spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
147
-
148
- const toValue =
149
- index === state.index ? 0 : index >= state.index ? 1 : -1;
150
-
151
- return Animated[spec.animation](tabAnims[route.key], {
152
- ...spec.config,
153
- toValue,
154
- useNativeDriver,
131
+ const animations = state.routes.map((route, index) => {
132
+ const descriptor = descriptors[route.key];
133
+
134
+ if (descriptor == null) {
135
+ throw new Error(
136
+ `Couldn't find a descriptor for route '${route.key}'.`
137
+ );
138
+ }
139
+
140
+ const { options } = descriptor;
141
+ const {
142
+ animation = 'none',
143
+ transitionSpec = NAMED_TRANSITIONS_PRESETS[animation].transitionSpec,
144
+ } = options;
145
+
146
+ let spec = transitionSpec;
147
+
148
+ if (route.key !== previousRouteKey && route.key !== focusedRouteKey) {
149
+ // Don't animate if the screen is not previous one or new one
150
+ // This will avoid flicker for screens not involved in the transition
151
+ spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
152
+ }
153
+
154
+ spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
155
+
156
+ const toValue =
157
+ index === state.index ? 0 : index >= state.index ? 1 : -1;
158
+
159
+ const tabAnimation = tabAnims[route.key];
160
+
161
+ if (tabAnimation == null) {
162
+ throw new Error(
163
+ `Couldn't find an animation for route '${route.key}'.`
164
+ );
165
+ }
166
+
167
+ return Animated[spec.animation](tabAnimation, {
168
+ ...spec.config,
169
+ toValue,
170
+ useNativeDriver,
171
+ });
172
+ });
173
+
174
+ Animated.parallel(animations).start(({ finished }) => {
175
+ if (
176
+ previousRouteKey &&
177
+ previousRouteKey !== focusedRouteKey &&
178
+ descriptors[previousRouteKey]?.options.popToTopOnBlur
179
+ ) {
180
+ const currentState = navigation.getState();
181
+ const prevRoute = currentState.routes.find(
182
+ (route) => route.key === previousRouteKey
183
+ );
184
+
185
+ if (
186
+ prevRoute?.state?.type === 'stack' &&
187
+ prevRoute.state.key &&
188
+ ((prevRoute.state.index ?? prevRoute.state.routes.length - 1) > 0 ||
189
+ prevRoute.state.routes[0]?.history?.length)
190
+ ) {
191
+ navigation.dispatch({
192
+ ...StackActions.popToTop(),
193
+ target: prevRoute.state.key,
155
194
  });
156
- })
157
- .filter(Boolean) as Animated.CompositeAnimation[]
158
- ).start(({ finished }) => {
159
- if (finished && popToTopAction) {
160
- navigation.dispatch(popToTopAction);
195
+ }
161
196
  }
162
197
 
163
198
  if (previousRouteKey !== focusedRouteKey) {
@@ -166,12 +201,26 @@ export function BottomTabViewCustom({
166
201
  target: focusedRouteKey,
167
202
  });
168
203
  }
204
+
205
+ if (finished && animations.length) {
206
+ // Delay clearing `animating` state
207
+ // This will give time for `popToTop` to get handled before pause
208
+ timer = setTimeout(() => {
209
+ setLastUpdate((update) =>
210
+ update.animating ? { ...update, animating: false } : update
211
+ );
212
+ }, 32);
213
+ }
169
214
  });
170
215
  };
171
216
 
172
217
  animateToIndex();
173
218
 
174
219
  previousRouteKeyRef.current = focusedRouteKey;
220
+
221
+ return () => {
222
+ clearTimeout(timer);
223
+ };
175
224
  }, [
176
225
  descriptors,
177
226
  focusedRouteKey,
@@ -187,7 +236,7 @@ export function BottomTabViewCustom({
187
236
  descriptors,
188
237
  dimensions: SafeAreaProviderCompat.initialMetrics.frame,
189
238
  insets: SafeAreaProviderCompat.initialMetrics.insets,
190
- style: descriptors[focusedRouteKey].options.tabBarStyle,
239
+ style: focusedOptions?.tabBarStyle,
191
240
  })
192
241
  );
193
242
 
@@ -204,14 +253,7 @@ export function BottomTabViewCustom({
204
253
  </BottomTabBarHeightCallbackContext.Provider>
205
254
  );
206
255
 
207
- // If there is no animation, we only have 2 states: visible and invisible
208
- const hasTwoStates = !routes.some((route) =>
209
- hasAnimation(descriptors[route.key].options)
210
- );
211
-
212
- const tabBarPosition = useTabBarPosition(
213
- descriptors[focusedRouteKey].options
214
- );
256
+ const tabBarPosition = useTabBarPosition(focusedOptions ?? {});
215
257
 
216
258
  return (
217
259
  <SafeAreaProviderCompat
@@ -225,19 +267,21 @@ export function BottomTabViewCustom({
225
267
  {tabBarPosition === 'top' || tabBarPosition === 'left'
226
268
  ? tabBarElement
227
269
  : null}
228
- <ScreenContainer
229
- key="screens"
230
- enabled={detachInactiveScreens}
231
- hasTwoStates={hasTwoStates}
232
- style={styles.screens}
233
- >
270
+ <Container key="screens" style={styles.screens}>
234
271
  {routes.map((route, index) => {
235
272
  const descriptor = descriptors[route.key];
236
273
 
274
+ if (descriptor == null) {
275
+ throw new Error(
276
+ `Couldn't find a descriptor for route '${route.key}'.`
277
+ );
278
+ }
279
+
237
280
  const { navigation, options, render } = descriptor;
238
281
 
239
282
  const {
240
283
  lazy = true,
284
+ inactiveBehavior = 'pause',
241
285
  animation = 'none',
242
286
  sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation]
243
287
  .sceneStyleInterpolator,
@@ -248,85 +292,70 @@ export function BottomTabViewCustom({
248
292
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
249
293
 
250
294
  const animationEnabled = hasAnimation(descriptor.options);
295
+ const tabAnimation = tabAnims[route.key];
296
+
297
+ if (tabAnimation == null) {
298
+ throw new Error(
299
+ `Couldn't find an animation for route '${route.key}'.`
300
+ );
301
+ }
251
302
 
252
303
  const content = (
253
304
  <AnimatedScreenContent
254
- progress={tabAnims[route.key]}
305
+ key={route.key}
306
+ progress={tabAnimation}
255
307
  animationEnabled={animationEnabled}
256
308
  sceneStyleInterpolator={sceneStyleInterpolator}
257
- style={customSceneStyle}
309
+ style={[StyleSheet.absoluteFill, customSceneStyle]}
258
310
  >
259
- <Lazy enabled={lazy} visible={isFocused || isPreloaded}>
260
- <ScreenContent
261
- isFocused={isFocused}
262
- route={route}
263
- navigation={navigation}
264
- options={options}
311
+ <ScreenContent
312
+ isFocused={isFocused}
313
+ route={route}
314
+ navigation={navigation}
315
+ options={options}
316
+ >
317
+ <BottomTabBarHeightContext.Provider
318
+ value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
265
319
  >
266
- <BottomTabBarHeightContext.Provider
267
- value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
268
- >
269
- {render()}
270
- </BottomTabBarHeightContext.Provider>
271
- </ScreenContent>
272
- </Lazy>
320
+ {render()}
321
+ </BottomTabBarHeightContext.Provider>
322
+ </ScreenContent>
273
323
  </AnimatedScreenContent>
274
324
  );
275
325
 
276
- if (Platform.OS === 'web') {
277
- /**
278
- * Don't use react-native-screens on web:
279
- * - It applies display: none as fallback, which triggers `onLayout` events
280
- * - We still need to hide the view when screens is not enabled
281
- * - We can use `inert` to handle a11y better for unfocused screens
282
- */
283
- return (
284
- <Container
285
- key={route.key}
286
- inert={!isFocused}
287
- style={{
288
- ...StyleSheet.absoluteFillObject,
289
- visibility: isFocused ? 'visible' : 'hidden',
290
- }}
291
- >
292
- {content}
293
- </Container>
294
- );
295
- }
326
+ const isAnimatingRoute =
327
+ lastUpdate.animating &&
328
+ (lastUpdate.previous === route.key ||
329
+ lastUpdate.current === route.key);
296
330
 
297
- const activityState = isFocused
298
- ? STATE_ON_TOP // the screen is on top after the transition
299
- : animationEnabled // is animation is not enabled, immediately move to inactive state
300
- ? tabAnims[route.key].interpolate({
301
- inputRange: [0, 1 - EPSILON, 1],
302
- outputRange: [
303
- STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
304
- STATE_TRANSITIONING_OR_BELOW_TOP,
305
- STATE_INACTIVE, // the screen is detached after transition
306
- ],
307
- extrapolate: 'extend',
308
- })
309
- : STATE_INACTIVE;
331
+ // For preloaded screens and if lazy is false,
332
+ // Keep them active so that the effects can run
333
+ const isActive =
334
+ inactiveBehavior === 'none' ||
335
+ isAnimatingRoute ||
336
+ isPreloaded ||
337
+ (lazy === false && !loaded.includes(route.key));
310
338
 
311
339
  return (
312
- <Screen
340
+ <Deferred
313
341
  key={route.key}
314
- style={[
315
- StyleSheet.absoluteFill,
316
- {
317
- zIndex: isFocused ? 0 : -1,
318
- pointerEvents: isFocused ? 'auto' : 'none',
319
- },
320
- ]}
321
- activityState={activityState}
322
- enabled={detachInactiveScreens}
323
- shouldFreeze={activityState === STATE_INACTIVE && !isPreloaded}
342
+ lazy={lazy}
343
+ visible={isFocused || isPreloaded || loaded.includes(route.key)}
324
344
  >
325
- {content}
326
- </Screen>
345
+ <ActivityView
346
+ mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
347
+ visible={isFocused || isAnimatingRoute}
348
+ style={{
349
+ ...StyleSheet.absoluteFill,
350
+ zIndex: isFocused ? 0 : -1,
351
+ }}
352
+ >
353
+ {content}
354
+ </ActivityView>
355
+ </Deferred>
327
356
  );
328
357
  })}
329
- </ScreenContainer>
358
+ </Container>
330
359
  {tabBarPosition === 'bottom' || tabBarPosition === 'right'
331
360
  ? tabBarElement
332
361
  : null}
@@ -343,7 +372,7 @@ function AnimatedScreenContent({
343
372
  }: {
344
373
  progress: Animated.Value;
345
374
  animationEnabled: boolean;
346
- sceneStyleInterpolator?: BottomTabSceneStyleInterpolator;
375
+ sceneStyleInterpolator?: BottomTabSceneStyleInterpolator | undefined;
347
376
  children: React.ReactNode;
348
377
  style: StyleProp<ViewStyle>;
349
378
  }) {