@react-navigation/bottom-tabs 8.0.0-alpha.2 → 8.0.0-alpha.21
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/navigators/createBottomTabNavigator.js +1 -18
- package/lib/module/navigators/createBottomTabNavigator.js.map +1 -1
- package/lib/module/utils/useBottomTabAnimation.js +1 -1
- package/lib/module/utils/useBottomTabAnimation.js.map +1 -1
- package/lib/module/utils/useBottomTabBarHeight.js +1 -1
- package/lib/module/utils/useBottomTabBarHeight.js.map +1 -1
- package/lib/module/views/BottomTabBar.js +13 -10
- package/lib/module/views/BottomTabBar.js.map +1 -1
- package/lib/module/views/BottomTabItem.js +1 -1
- package/lib/module/views/BottomTabItem.js.map +1 -1
- package/lib/module/views/BottomTabViewCustom.js +64 -66
- package/lib/module/views/BottomTabViewCustom.js.map +1 -1
- package/lib/module/views/BottomTabViewNativeImpl.js +117 -29
- package/lib/module/views/BottomTabViewNativeImpl.js.map +1 -1
- package/lib/module/views/ScreenContent.js.map +1 -1
- package/lib/module/views/TabBarIcon.js +5 -14
- package/lib/module/views/TabBarIcon.js.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +63 -100
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabItem.d.ts +16 -16
- package/lib/typescript/src/views/BottomTabItem.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabViewCustom.d.ts +1 -1
- package/lib/typescript/src/views/BottomTabViewCustom.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabViewNativeImpl.d.ts.map +1 -1
- package/lib/typescript/src/views/ScreenContent.d.ts +2 -2
- package/lib/typescript/src/views/ScreenContent.d.ts.map +1 -1
- package/lib/typescript/src/views/TabBarIcon.d.ts +6 -6
- package/lib/typescript/src/views/TabBarIcon.d.ts.map +1 -1
- package/package.json +16 -17
- package/src/index.tsx +0 -1
- package/src/navigators/createBottomTabNavigator.tsx +0 -28
- package/src/types.tsx +65 -109
- package/src/utils/useBottomTabAnimation.tsx +1 -1
- package/src/utils/useBottomTabBarHeight.tsx +1 -1
- package/src/views/BottomTabBar.tsx +19 -11
- package/src/views/BottomTabItem.tsx +17 -19
- package/src/views/BottomTabViewCustom.tsx +119 -117
- package/src/views/BottomTabViewNativeImpl.tsx +192 -48
- package/src/views/ScreenContent.tsx +1 -2
- 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
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
transitionSpec
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
<
|
|
321
|
+
<ActivityView
|
|
313
322
|
key={route.key}
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
</
|
|
328
|
+
</ActivityView>
|
|
327
329
|
);
|
|
328
330
|
})}
|
|
329
|
-
</
|
|
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
|
}) {
|