@react-navigation/bottom-tabs 8.0.0-alpha.2 → 8.0.0-alpha.20

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 (44) hide show
  1. package/lib/module/navigators/createBottomTabNavigator.js +1 -18
  2. package/lib/module/navigators/createBottomTabNavigator.js.map +1 -1
  3. package/lib/module/utils/useBottomTabAnimation.js +1 -1
  4. package/lib/module/utils/useBottomTabAnimation.js.map +1 -1
  5. package/lib/module/utils/useBottomTabBarHeight.js +1 -1
  6. package/lib/module/utils/useBottomTabBarHeight.js.map +1 -1
  7. package/lib/module/views/BottomTabBar.js +13 -10
  8. package/lib/module/views/BottomTabBar.js.map +1 -1
  9. package/lib/module/views/BottomTabItem.js +1 -1
  10. package/lib/module/views/BottomTabItem.js.map +1 -1
  11. package/lib/module/views/BottomTabViewCustom.js +64 -66
  12. package/lib/module/views/BottomTabViewCustom.js.map +1 -1
  13. package/lib/module/views/BottomTabViewNativeImpl.js +117 -29
  14. package/lib/module/views/BottomTabViewNativeImpl.js.map +1 -1
  15. package/lib/module/views/ScreenContent.js.map +1 -1
  16. package/lib/module/views/TabBarIcon.js +5 -14
  17. package/lib/module/views/TabBarIcon.js.map +1 -1
  18. package/lib/typescript/src/index.d.ts +1 -1
  19. package/lib/typescript/src/index.d.ts.map +1 -1
  20. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  21. package/lib/typescript/src/types.d.ts +63 -100
  22. package/lib/typescript/src/types.d.ts.map +1 -1
  23. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  24. package/lib/typescript/src/views/BottomTabItem.d.ts +16 -16
  25. package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
  26. package/lib/typescript/src/views/BottomTabViewCustom.d.ts +1 -1
  27. package/lib/typescript/src/views/BottomTabViewCustom.d.ts.map +1 -1
  28. package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts.map +1 -1
  29. package/lib/typescript/src/views/ScreenContent.d.ts +2 -2
  30. package/lib/typescript/src/views/ScreenContent.d.ts.map +1 -1
  31. package/lib/typescript/src/views/TabBarIcon.d.ts +6 -6
  32. package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
  33. package/package.json +16 -17
  34. package/src/index.tsx +0 -1
  35. package/src/navigators/createBottomTabNavigator.tsx +0 -28
  36. package/src/types.tsx +65 -109
  37. package/src/utils/useBottomTabAnimation.tsx +1 -1
  38. package/src/utils/useBottomTabBarHeight.tsx +1 -1
  39. package/src/views/BottomTabBar.tsx +19 -11
  40. package/src/views/BottomTabItem.tsx +17 -19
  41. package/src/views/BottomTabViewCustom.tsx +119 -117
  42. package/src/views/BottomTabViewNativeImpl.tsx +192 -48
  43. package/src/views/ScreenContent.tsx +1 -2
  44. package/src/views/TabBarIcon.tsx +17 -24
@@ -1,6 +1,6 @@
1
1
  import {
2
+ ActivityView,
2
3
  Container,
3
- Lazy,
4
4
  SafeAreaProviderCompat,
5
5
  } from '@react-navigation/elements/internal';
6
6
  import {
@@ -17,7 +17,6 @@ import {
17
17
  StyleSheet,
18
18
  type ViewStyle,
19
19
  } from 'react-native';
20
- import { Screen, ScreenContainer } from 'react-native-screens';
21
20
 
22
21
  import {
23
22
  FadeTransition,
@@ -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,22 +77,43 @@ 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
82
  const focusedRouteKey = routes[state.index].key;
92
83
 
93
- const previousRouteKeyRef = React.useRef(focusedRouteKey);
84
+ const [loaded, setLoaded] = React.useState([focusedRouteKey]);
85
+
86
+ if (!loaded.includes(focusedRouteKey)) {
87
+ setLoaded([...loaded, focusedRouteKey]);
88
+ }
89
+
90
+ const [lastUpdate, setLastUpdate] = React.useState<{
91
+ current: string;
92
+ previous?: string;
93
+ }>({
94
+ current: focusedRouteKey,
95
+ });
96
+
97
+ if (lastUpdate.current !== focusedRouteKey) {
98
+ setLastUpdate({
99
+ current: focusedRouteKey,
100
+ previous: lastUpdate.current,
101
+ });
102
+ }
103
+
94
104
  const tabAnims = useAnimatedHashMap(state);
95
105
 
106
+ const [isAnimating, setIsAnimating] = React.useState(false);
107
+
108
+ const previousRouteKeyRef = React.useRef(focusedRouteKey);
109
+
96
110
  React.useEffect(() => {
97
111
  const previousRouteKey = previousRouteKeyRef.current;
98
112
 
99
113
  let popToTopAction: NavigationAction | undefined;
100
114
 
101
115
  if (
116
+ previousRouteKey &&
102
117
  previousRouteKey !== focusedRouteKey &&
103
118
  descriptors[previousRouteKey]?.options.popToTopOnBlur
104
119
  ) {
@@ -114,6 +129,8 @@ export function BottomTabViewCustom({
114
129
  }
115
130
  }
116
131
 
132
+ let timer: ReturnType<typeof setTimeout>;
133
+
117
134
  const animateToIndex = () => {
118
135
  if (previousRouteKey !== focusedRouteKey) {
119
136
  navigation.emit({
@@ -122,40 +139,42 @@ export function BottomTabViewCustom({
122
139
  });
123
140
  }
124
141
 
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,
155
- });
156
- })
157
- .filter(Boolean) as Animated.CompositeAnimation[]
158
- ).start(({ finished }) => {
142
+ const animations = state.routes
143
+ .map((route, index) => {
144
+ const { options } = descriptors[route.key];
145
+ const {
146
+ animation = 'none',
147
+ transitionSpec = NAMED_TRANSITIONS_PRESETS[animation]
148
+ .transitionSpec,
149
+ } = options;
150
+
151
+ let spec = transitionSpec;
152
+
153
+ if (route.key !== previousRouteKey && route.key !== focusedRouteKey) {
154
+ // Don't animate if the screen is not previous one or new one
155
+ // This will avoid flicker for screens not involved in the transition
156
+ spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
157
+ }
158
+
159
+ spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec;
160
+
161
+ const toValue =
162
+ index === state.index ? 0 : index >= state.index ? 1 : -1;
163
+
164
+ return Animated[spec.animation](tabAnims[route.key], {
165
+ ...spec.config,
166
+ toValue,
167
+ useNativeDriver,
168
+ });
169
+ })
170
+ .filter((anim) => anim != null);
171
+
172
+ if (animations.length) {
173
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
174
+ setIsAnimating(true);
175
+ }
176
+
177
+ Animated.parallel(animations).start(({ finished }) => {
159
178
  if (finished && popToTopAction) {
160
179
  navigation.dispatch(popToTopAction);
161
180
  }
@@ -166,12 +185,24 @@ export function BottomTabViewCustom({
166
185
  target: focusedRouteKey,
167
186
  });
168
187
  }
188
+
189
+ if (finished && animations.length) {
190
+ // Delay clearing `isAnimating`
191
+ // This will give time for `popToAction` to get handled before pause
192
+ timer = setTimeout(() => {
193
+ setIsAnimating(false);
194
+ }, 32);
195
+ }
169
196
  });
170
197
  };
171
198
 
172
199
  animateToIndex();
173
200
 
174
201
  previousRouteKeyRef.current = focusedRouteKey;
202
+
203
+ return () => {
204
+ clearTimeout(timer);
205
+ };
175
206
  }, [
176
207
  descriptors,
177
208
  focusedRouteKey,
@@ -204,11 +235,6 @@ export function BottomTabViewCustom({
204
235
  </BottomTabBarHeightCallbackContext.Provider>
205
236
  );
206
237
 
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
238
  const tabBarPosition = useTabBarPosition(
213
239
  descriptors[focusedRouteKey].options
214
240
  );
@@ -225,12 +251,7 @@ export function BottomTabViewCustom({
225
251
  {tabBarPosition === 'top' || tabBarPosition === 'left'
226
252
  ? tabBarElement
227
253
  : null}
228
- <ScreenContainer
229
- key="screens"
230
- enabled={detachInactiveScreens}
231
- hasTwoStates={hasTwoStates}
232
- style={styles.screens}
233
- >
254
+ <Container key="screens" style={styles.screens}>
234
255
  {routes.map((route, index) => {
235
256
  const descriptor = descriptors[route.key];
236
257
 
@@ -238,6 +259,7 @@ export function BottomTabViewCustom({
238
259
 
239
260
  const {
240
261
  lazy = true,
262
+ inactiveBehavior = 'pause',
241
263
  animation = 'none',
242
264
  sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation]
243
265
  .sceneStyleInterpolator,
@@ -247,86 +269,66 @@ export function BottomTabViewCustom({
247
269
  const isFocused = state.index === index;
248
270
  const isPreloaded = state.preloadedRouteKeys.includes(route.key);
249
271
 
272
+ if (
273
+ lazy &&
274
+ !loaded.includes(route.key) &&
275
+ !isFocused &&
276
+ !isPreloaded
277
+ ) {
278
+ // Don't render a lazy screen if we've never navigated to it or it wasn't preloaded
279
+ return null;
280
+ }
281
+
250
282
  const animationEnabled = hasAnimation(descriptor.options);
251
283
 
252
284
  const content = (
253
285
  <AnimatedScreenContent
286
+ key={route.key}
254
287
  progress={tabAnims[route.key]}
255
288
  animationEnabled={animationEnabled}
256
289
  sceneStyleInterpolator={sceneStyleInterpolator}
257
- style={customSceneStyle}
290
+ style={[StyleSheet.absoluteFill, customSceneStyle]}
258
291
  >
259
- <Lazy enabled={lazy} visible={isFocused || isPreloaded}>
260
- <ScreenContent
261
- isFocused={isFocused}
262
- route={route}
263
- navigation={navigation}
264
- options={options}
292
+ <ScreenContent
293
+ isFocused={isFocused}
294
+ route={route}
295
+ navigation={navigation}
296
+ options={options}
297
+ >
298
+ <BottomTabBarHeightContext.Provider
299
+ value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
265
300
  >
266
- <BottomTabBarHeightContext.Provider
267
- value={tabBarPosition === 'bottom' ? tabBarHeight : 0}
268
- >
269
- {render()}
270
- </BottomTabBarHeightContext.Provider>
271
- </ScreenContent>
272
- </Lazy>
301
+ {render()}
302
+ </BottomTabBarHeightContext.Provider>
303
+ </ScreenContent>
273
304
  </AnimatedScreenContent>
274
305
  );
275
306
 
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
- }
307
+ const isAnimatingRoute =
308
+ isAnimating &&
309
+ (lastUpdate.previous === route.key ||
310
+ lastUpdate.current === route.key);
296
311
 
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;
312
+ // For preloaded screens and if lazy is false,
313
+ // Keep them active so that the effects can run
314
+ const isActive =
315
+ inactiveBehavior === 'none' ||
316
+ isAnimatingRoute ||
317
+ isPreloaded ||
318
+ (lazy === false && !loaded.includes(route.key));
310
319
 
311
320
  return (
312
- <Screen
321
+ <ActivityView
313
322
  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}
323
+ mode={isFocused ? 'normal' : isActive ? 'inert' : 'paused'}
324
+ visible={isFocused || isAnimatingRoute}
325
+ style={{ ...StyleSheet.absoluteFill, zIndex: isFocused ? 0 : -1 }}
324
326
  >
325
327
  {content}
326
- </Screen>
328
+ </ActivityView>
327
329
  );
328
330
  })}
329
- </ScreenContainer>
331
+ </Container>
330
332
  {tabBarPosition === 'bottom' || tabBarPosition === 'right'
331
333
  ? tabBarElement
332
334
  : null}
@@ -343,7 +345,7 @@ function AnimatedScreenContent({
343
345
  }: {
344
346
  progress: Animated.Value;
345
347
  animationEnabled: boolean;
346
- sceneStyleInterpolator?: BottomTabSceneStyleInterpolator;
348
+ sceneStyleInterpolator?: BottomTabSceneStyleInterpolator | undefined;
347
349
  children: React.ReactNode;
348
350
  style: StyleProp<ViewStyle>;
349
351
  }) {