@amazon-devices/react-navigation__react-native-tab-view 2.0.1758683737
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/LICENSE +7 -0
- package/README.md +649 -0
- package/SystemBundles/c5889b273a6efc2a3c30241851055c37630fbf3381df86d403967369775f0536.bundle +14497 -0
- package/SystemBundles/c5889b273a6efc2a3c30241851055c37630fbf3381df86d403967369775f0536.map +1 -0
- package/SystemModules/amzn__react-navigation__react-native-tab-view-2.modules.txt +41 -0
- package/SystemModules/amzn__react-navigation__react-native-tab-view-2.private.modules.txt +98 -0
- package/SystemModules/keplerscript-system-bundle-config.json +7 -0
- package/THIRD_PARTY_NOTICES.txt +24 -0
- package/kepler-compatibility.json +8 -0
- package/kepler-transformed-package-info.json +3 -0
- package/lib/commonjs/Pager.android.js +14 -0
- package/lib/commonjs/Pager.android.js.map +1 -0
- package/lib/commonjs/Pager.ios.js +14 -0
- package/lib/commonjs/Pager.ios.js.map +1 -0
- package/lib/commonjs/Pager.js +14 -0
- package/lib/commonjs/Pager.js.map +1 -0
- package/lib/commonjs/PagerViewAdapter.js +132 -0
- package/lib/commonjs/PagerViewAdapter.js.map +1 -0
- package/lib/commonjs/PanResponderAdapter.js +204 -0
- package/lib/commonjs/PanResponderAdapter.js.map +1 -0
- package/lib/commonjs/PlatformPressable.js +43 -0
- package/lib/commonjs/PlatformPressable.js.map +1 -0
- package/lib/commonjs/SceneMap.js +29 -0
- package/lib/commonjs/SceneMap.js.map +1 -0
- package/lib/commonjs/SceneView.js +78 -0
- package/lib/commonjs/SceneView.js.map +1 -0
- package/lib/commonjs/TabBar.js +405 -0
- package/lib/commonjs/TabBar.js.map +1 -0
- package/lib/commonjs/TabBarIndicator.js +110 -0
- package/lib/commonjs/TabBarIndicator.js.map +1 -0
- package/lib/commonjs/TabBarItem.js +227 -0
- package/lib/commonjs/TabBarItem.js.map +1 -0
- package/lib/commonjs/TabView.js +122 -0
- package/lib/commonjs/TabView.js.map +1 -0
- package/lib/commonjs/index.js +42 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useAnimatedValue.js +18 -0
- package/lib/commonjs/useAnimatedValue.js.map +1 -0
- package/lib/module/Pager.android.js +2 -0
- package/lib/module/Pager.android.js.map +1 -0
- package/lib/module/Pager.ios.js +2 -0
- package/lib/module/Pager.ios.js.map +1 -0
- package/lib/module/Pager.js +2 -0
- package/lib/module/Pager.js.map +1 -0
- package/lib/module/PagerViewAdapter.js +123 -0
- package/lib/module/PagerViewAdapter.js.map +1 -0
- package/lib/module/PanResponderAdapter.js +195 -0
- package/lib/module/PanResponderAdapter.js.map +1 -0
- package/lib/module/PlatformPressable.js +35 -0
- package/lib/module/PlatformPressable.js.map +1 -0
- package/lib/module/SceneMap.js +21 -0
- package/lib/module/SceneMap.js.map +1 -0
- package/lib/module/SceneView.js +70 -0
- package/lib/module/SceneView.js.map +1 -0
- package/lib/module/TabBar.js +396 -0
- package/lib/module/TabBar.js.map +1 -0
- package/lib/module/TabBarIndicator.js +101 -0
- package/lib/module/TabBarIndicator.js.map +1 -0
- package/lib/module/TabBarItem.js +218 -0
- package/lib/module/TabBarItem.js.map +1 -0
- package/lib/module/TabView.js +113 -0
- package/lib/module/TabView.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useAnimatedValue.js +10 -0
- package/lib/module/useAnimatedValue.js.map +1 -0
- package/lib/typescript/src/Pager.android.d.ts +2 -0
- package/lib/typescript/src/Pager.android.d.ts.map +1 -0
- package/lib/typescript/src/Pager.d.ts +2 -0
- package/lib/typescript/src/Pager.d.ts.map +1 -0
- package/lib/typescript/src/Pager.ios.d.ts +2 -0
- package/lib/typescript/src/Pager.ios.d.ts.map +1 -0
- package/lib/typescript/src/PagerViewAdapter.d.ts +15 -0
- package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -0
- package/lib/typescript/src/PanResponderAdapter.d.ts +16 -0
- package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -0
- package/lib/typescript/src/PlatformPressable.d.ts +16 -0
- package/lib/typescript/src/PlatformPressable.d.ts.map +1 -0
- package/lib/typescript/src/SceneMap.d.ts +10 -0
- package/lib/typescript/src/SceneMap.d.ts.map +1 -0
- package/lib/typescript/src/SceneView.d.ts +16 -0
- package/lib/typescript/src/SceneView.d.ts.map +1 -0
- package/lib/typescript/src/TabBar.d.ts +44 -0
- package/lib/typescript/src/TabBar.d.ts.map +1 -0
- package/lib/typescript/src/TabBarIndicator.d.ts +13 -0
- package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -0
- package/lib/typescript/src/TabBarItem.d.ts +37 -0
- package/lib/typescript/src/TabBarItem.d.ts.map +1 -0
- package/lib/typescript/src/TabView.d.ts +27 -0
- package/lib/typescript/src/TabView.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +42 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useAnimatedValue.d.ts +3 -0
- package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -0
- package/package.json +1 -0
- package/src/Pager.android.tsx +1 -0
- package/src/Pager.ios.tsx +1 -0
- package/src/Pager.tsx +1 -0
- package/src/PagerViewAdapter.tsx +180 -0
- package/src/PanResponderAdapter.tsx +340 -0
- package/src/PlatformPressable.tsx +42 -0
- package/src/SceneMap.tsx +30 -0
- package/src/SceneView.tsx +107 -0
- package/src/TabBar.tsx +639 -0
- package/src/TabBarIndicator.tsx +153 -0
- package/src/TabBarItem.tsx +319 -0
- package/src/TabView.tsx +155 -0
- package/src/index.tsx +10 -0
- package/src/types.tsx +59 -0
- package/src/useAnimatedValue.tsx +12 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
DimensionValue,
|
|
5
|
+
Easing,
|
|
6
|
+
I18nManager,
|
|
7
|
+
Platform,
|
|
8
|
+
StyleProp,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
|
|
13
|
+
import type { NavigationState, Route, SceneRendererProps } from './types';
|
|
14
|
+
import useAnimatedValue from './useAnimatedValue';
|
|
15
|
+
|
|
16
|
+
export type GetTabWidth = (index: number) => number;
|
|
17
|
+
|
|
18
|
+
export type Props<T extends Route> = SceneRendererProps & {
|
|
19
|
+
navigationState: NavigationState<T>;
|
|
20
|
+
width: string | number;
|
|
21
|
+
style?: StyleProp<ViewStyle>;
|
|
22
|
+
getTabWidth: GetTabWidth;
|
|
23
|
+
gap?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getTranslateX = (
|
|
27
|
+
position: Animated.AnimatedInterpolation<number>,
|
|
28
|
+
routes: Route[],
|
|
29
|
+
getTabWidth: GetTabWidth,
|
|
30
|
+
gap?: number
|
|
31
|
+
) => {
|
|
32
|
+
const inputRange = routes.map((_, i) => i);
|
|
33
|
+
|
|
34
|
+
// every index contains widths at all previous indices
|
|
35
|
+
const outputRange = routes.reduce<number[]>((acc, _, i) => {
|
|
36
|
+
if (i === 0) return [0];
|
|
37
|
+
return [...acc, acc[i - 1] + getTabWidth(i - 1) + (gap ?? 0)];
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const translateX = position.interpolate({
|
|
41
|
+
inputRange,
|
|
42
|
+
outputRange,
|
|
43
|
+
extrapolate: 'clamp',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return Animated.multiply(translateX, I18nManager.isRTL ? -1 : 1);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function TabBarIndicator<T extends Route>({
|
|
50
|
+
getTabWidth,
|
|
51
|
+
layout,
|
|
52
|
+
navigationState,
|
|
53
|
+
position,
|
|
54
|
+
width,
|
|
55
|
+
gap,
|
|
56
|
+
style,
|
|
57
|
+
}: Props<T>) {
|
|
58
|
+
const isIndicatorShown = React.useRef(false);
|
|
59
|
+
const isWidthDynamic = width === 'auto';
|
|
60
|
+
|
|
61
|
+
const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
|
|
62
|
+
|
|
63
|
+
const indicatorVisible = isWidthDynamic
|
|
64
|
+
? layout.width &&
|
|
65
|
+
navigationState.routes
|
|
66
|
+
.slice(0, navigationState.index)
|
|
67
|
+
.every((_, r) => getTabWidth(r))
|
|
68
|
+
: true;
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
const fadeInIndicator = () => {
|
|
72
|
+
if (
|
|
73
|
+
!isIndicatorShown.current &&
|
|
74
|
+
isWidthDynamic &&
|
|
75
|
+
// We should fade-in the indicator when we have widths for all the tab items
|
|
76
|
+
indicatorVisible
|
|
77
|
+
) {
|
|
78
|
+
isIndicatorShown.current = true;
|
|
79
|
+
|
|
80
|
+
Animated.timing(opacity, {
|
|
81
|
+
toValue: 1,
|
|
82
|
+
duration: 150,
|
|
83
|
+
easing: Easing.in(Easing.linear),
|
|
84
|
+
useNativeDriver: true,
|
|
85
|
+
}).start();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
fadeInIndicator();
|
|
90
|
+
|
|
91
|
+
return () => opacity.stopAnimation();
|
|
92
|
+
}, [indicatorVisible, isWidthDynamic, opacity]);
|
|
93
|
+
|
|
94
|
+
const { routes } = navigationState;
|
|
95
|
+
|
|
96
|
+
const transform = [];
|
|
97
|
+
|
|
98
|
+
if (layout.width) {
|
|
99
|
+
const translateX =
|
|
100
|
+
routes.length > 1 ? getTranslateX(position, routes, getTabWidth, gap) : 0;
|
|
101
|
+
|
|
102
|
+
transform.push({ translateX });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (width === 'auto') {
|
|
106
|
+
const inputRange = routes.map((_, i) => i);
|
|
107
|
+
const outputRange = inputRange.map(getTabWidth);
|
|
108
|
+
|
|
109
|
+
transform.push(
|
|
110
|
+
{
|
|
111
|
+
scaleX:
|
|
112
|
+
routes.length > 1
|
|
113
|
+
? position.interpolate({
|
|
114
|
+
inputRange,
|
|
115
|
+
outputRange,
|
|
116
|
+
extrapolate: 'clamp',
|
|
117
|
+
})
|
|
118
|
+
: outputRange[0],
|
|
119
|
+
},
|
|
120
|
+
{ translateX: 0.5 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Animated.View
|
|
126
|
+
style={[
|
|
127
|
+
styles.indicator,
|
|
128
|
+
{ width: (width === 'auto' ? 1 : width) as DimensionValue | undefined },
|
|
129
|
+
// If layout is not available, use `left` property for positioning the indicator
|
|
130
|
+
// This avoids rendering delay until we are able to calculate translateX
|
|
131
|
+
// If platform is macos use `left` property as `transform` is broken at the moment.
|
|
132
|
+
// See: https://github.com/microsoft/react-native-macos/issues/280
|
|
133
|
+
layout.width && Platform.OS !== 'macos'
|
|
134
|
+
? { left: 0 }
|
|
135
|
+
: { left: `${(100 / routes.length) * navigationState.index}%` },
|
|
136
|
+
{ transform },
|
|
137
|
+
width === 'auto' ? { opacity: opacity } : null,
|
|
138
|
+
style,
|
|
139
|
+
]}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const styles = StyleSheet.create({
|
|
145
|
+
indicator: {
|
|
146
|
+
backgroundColor: '#ffeb3b',
|
|
147
|
+
position: 'absolute',
|
|
148
|
+
left: 0,
|
|
149
|
+
bottom: 0,
|
|
150
|
+
right: 0,
|
|
151
|
+
height: 2,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
PressableAndroidRippleConfig,
|
|
6
|
+
StyleProp,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
TextStyle,
|
|
9
|
+
View,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import useLatestCallback from 'use-latest-callback';
|
|
13
|
+
|
|
14
|
+
import PlatformPressable from './PlatformPressable';
|
|
15
|
+
import type { NavigationState, Route, Scene } from './types';
|
|
16
|
+
|
|
17
|
+
export type Props<T extends Route> = {
|
|
18
|
+
position: Animated.AnimatedInterpolation<number>;
|
|
19
|
+
route: T;
|
|
20
|
+
navigationState: NavigationState<T>;
|
|
21
|
+
activeColor?: string;
|
|
22
|
+
inactiveColor?: string;
|
|
23
|
+
pressColor?: string;
|
|
24
|
+
pressOpacity?: number;
|
|
25
|
+
getLabelText: (scene: Scene<T>) => string | undefined;
|
|
26
|
+
getAccessible: (scene: Scene<T>) => boolean | undefined;
|
|
27
|
+
getAccessibilityLabel: (scene: Scene<T>) => string | undefined;
|
|
28
|
+
getTestID: (scene: Scene<T>) => string | undefined;
|
|
29
|
+
renderLabel?: (scene: {
|
|
30
|
+
route: T;
|
|
31
|
+
focused: boolean;
|
|
32
|
+
color: string;
|
|
33
|
+
}) => React.ReactNode;
|
|
34
|
+
renderIcon?: (scene: {
|
|
35
|
+
route: T;
|
|
36
|
+
focused: boolean;
|
|
37
|
+
color: string;
|
|
38
|
+
}) => React.ReactNode;
|
|
39
|
+
renderBadge?: (scene: Scene<T>) => React.ReactNode;
|
|
40
|
+
onLayout?: (event: LayoutChangeEvent) => void;
|
|
41
|
+
onPress: () => void;
|
|
42
|
+
onLongPress: () => void;
|
|
43
|
+
defaultTabWidth?: number;
|
|
44
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
45
|
+
style: StyleProp<ViewStyle>;
|
|
46
|
+
android_ripple?: PressableAndroidRippleConfig;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEFAULT_ACTIVE_COLOR = 'rgba(255, 255, 255, 1)';
|
|
50
|
+
const DEFAULT_INACTIVE_COLOR = 'rgba(255, 255, 255, 0.7)';
|
|
51
|
+
|
|
52
|
+
const getActiveOpacity = (
|
|
53
|
+
position: Animated.AnimatedInterpolation<number>,
|
|
54
|
+
routesLength: number,
|
|
55
|
+
tabIndex: number
|
|
56
|
+
) => {
|
|
57
|
+
if (routesLength > 1) {
|
|
58
|
+
const inputRange = Array.from({ length: routesLength }, (_, i) => i);
|
|
59
|
+
|
|
60
|
+
return position.interpolate({
|
|
61
|
+
inputRange,
|
|
62
|
+
outputRange: inputRange.map((i) => (i === tabIndex ? 1 : 0)),
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getInactiveOpacity = (
|
|
70
|
+
position: Animated.AnimatedInterpolation<number>,
|
|
71
|
+
routesLength: number,
|
|
72
|
+
tabIndex: number
|
|
73
|
+
) => {
|
|
74
|
+
if (routesLength > 1) {
|
|
75
|
+
const inputRange = Array.from({ length: routesLength }, (_, i) => i);
|
|
76
|
+
|
|
77
|
+
return position.interpolate({
|
|
78
|
+
inputRange,
|
|
79
|
+
outputRange: inputRange.map((i: number) => (i === tabIndex ? 0 : 1)),
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type TabBarItemInternalProps<T extends Route> = Omit<
|
|
87
|
+
Props<T>,
|
|
88
|
+
'navigationState'
|
|
89
|
+
> & {
|
|
90
|
+
isFocused: boolean;
|
|
91
|
+
index: number;
|
|
92
|
+
routesLength: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const TabBarItemInternal = <T extends Route>({
|
|
96
|
+
getAccessibilityLabel,
|
|
97
|
+
getAccessible,
|
|
98
|
+
getLabelText,
|
|
99
|
+
getTestID,
|
|
100
|
+
onLongPress,
|
|
101
|
+
onPress,
|
|
102
|
+
isFocused,
|
|
103
|
+
position,
|
|
104
|
+
route,
|
|
105
|
+
style,
|
|
106
|
+
inactiveColor: inactiveColorCustom,
|
|
107
|
+
activeColor: activeColorCustom,
|
|
108
|
+
labelStyle,
|
|
109
|
+
onLayout,
|
|
110
|
+
index: tabIndex,
|
|
111
|
+
pressColor,
|
|
112
|
+
pressOpacity,
|
|
113
|
+
renderBadge,
|
|
114
|
+
renderIcon,
|
|
115
|
+
defaultTabWidth,
|
|
116
|
+
routesLength,
|
|
117
|
+
renderLabel: renderLabelCustom,
|
|
118
|
+
android_ripple = { borderless: true },
|
|
119
|
+
}: TabBarItemInternalProps<T>) => {
|
|
120
|
+
const labelColorFromStyle = StyleSheet.flatten(labelStyle || {}).color;
|
|
121
|
+
|
|
122
|
+
const activeColor =
|
|
123
|
+
activeColorCustom !== undefined
|
|
124
|
+
? activeColorCustom
|
|
125
|
+
: typeof labelColorFromStyle === 'string'
|
|
126
|
+
? labelColorFromStyle
|
|
127
|
+
: DEFAULT_ACTIVE_COLOR;
|
|
128
|
+
const inactiveColor =
|
|
129
|
+
inactiveColorCustom !== undefined
|
|
130
|
+
? inactiveColorCustom
|
|
131
|
+
: typeof labelColorFromStyle === 'string'
|
|
132
|
+
? labelColorFromStyle
|
|
133
|
+
: DEFAULT_INACTIVE_COLOR;
|
|
134
|
+
|
|
135
|
+
const activeOpacity = getActiveOpacity(position, routesLength, tabIndex);
|
|
136
|
+
const inactiveOpacity = getInactiveOpacity(position, routesLength, tabIndex);
|
|
137
|
+
|
|
138
|
+
let icon: React.ReactNode | null = null;
|
|
139
|
+
let label: React.ReactNode | null = null;
|
|
140
|
+
|
|
141
|
+
if (renderIcon) {
|
|
142
|
+
const activeIcon = renderIcon({
|
|
143
|
+
route,
|
|
144
|
+
focused: true,
|
|
145
|
+
color: activeColor,
|
|
146
|
+
});
|
|
147
|
+
const inactiveIcon = renderIcon({
|
|
148
|
+
route,
|
|
149
|
+
focused: false,
|
|
150
|
+
color: inactiveColor,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (inactiveIcon != null && activeIcon != null) {
|
|
154
|
+
icon = (
|
|
155
|
+
<View style={styles.icon}>
|
|
156
|
+
<Animated.View style={{ opacity: inactiveOpacity }}>
|
|
157
|
+
{inactiveIcon}
|
|
158
|
+
</Animated.View>
|
|
159
|
+
<Animated.View
|
|
160
|
+
style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
|
|
161
|
+
>
|
|
162
|
+
{activeIcon}
|
|
163
|
+
</Animated.View>
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const renderLabel =
|
|
170
|
+
renderLabelCustom !== undefined
|
|
171
|
+
? renderLabelCustom
|
|
172
|
+
: (labelProps: { route: T; color: string }) => {
|
|
173
|
+
const labelText = getLabelText({ route: labelProps.route });
|
|
174
|
+
|
|
175
|
+
if (typeof labelText === 'string') {
|
|
176
|
+
return (
|
|
177
|
+
<Animated.Text
|
|
178
|
+
style={[
|
|
179
|
+
styles.label,
|
|
180
|
+
icon ? { marginTop: 0 } : null,
|
|
181
|
+
labelStyle,
|
|
182
|
+
{ color: labelProps.color },
|
|
183
|
+
]}
|
|
184
|
+
>
|
|
185
|
+
{labelText}
|
|
186
|
+
</Animated.Text>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return labelText;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (renderLabel) {
|
|
194
|
+
const activeLabel = renderLabel({
|
|
195
|
+
route,
|
|
196
|
+
focused: true,
|
|
197
|
+
color: activeColor,
|
|
198
|
+
});
|
|
199
|
+
const inactiveLabel = renderLabel({
|
|
200
|
+
route,
|
|
201
|
+
focused: false,
|
|
202
|
+
color: inactiveColor,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
label = (
|
|
206
|
+
<View>
|
|
207
|
+
<Animated.View style={{ opacity: inactiveOpacity }}>
|
|
208
|
+
{inactiveLabel}
|
|
209
|
+
</Animated.View>
|
|
210
|
+
<Animated.View
|
|
211
|
+
style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
|
|
212
|
+
>
|
|
213
|
+
{activeLabel}
|
|
214
|
+
</Animated.View>
|
|
215
|
+
</View>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const tabStyle = StyleSheet.flatten(style);
|
|
220
|
+
const isWidthSet = tabStyle?.width !== undefined;
|
|
221
|
+
|
|
222
|
+
const tabContainerStyle: ViewStyle | null = isWidthSet
|
|
223
|
+
? null
|
|
224
|
+
: { width: defaultTabWidth };
|
|
225
|
+
|
|
226
|
+
const scene = { route };
|
|
227
|
+
|
|
228
|
+
let accessibilityLabel = getAccessibilityLabel(scene);
|
|
229
|
+
|
|
230
|
+
accessibilityLabel =
|
|
231
|
+
typeof accessibilityLabel !== 'undefined'
|
|
232
|
+
? accessibilityLabel
|
|
233
|
+
: getLabelText(scene);
|
|
234
|
+
|
|
235
|
+
const badge = renderBadge ? renderBadge(scene) : null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<PlatformPressable
|
|
239
|
+
android_ripple={android_ripple}
|
|
240
|
+
testID={getTestID(scene)}
|
|
241
|
+
accessible={getAccessible(scene)}
|
|
242
|
+
accessibilityLabel={accessibilityLabel}
|
|
243
|
+
accessibilityRole="tab"
|
|
244
|
+
accessibilityState={{ selected: isFocused }}
|
|
245
|
+
// @ts-ignore: this is to support older React Native versions
|
|
246
|
+
accessibilityStates={isFocused ? ['selected'] : []}
|
|
247
|
+
pressColor={pressColor}
|
|
248
|
+
pressOpacity={pressOpacity}
|
|
249
|
+
delayPressIn={0}
|
|
250
|
+
onLayout={onLayout}
|
|
251
|
+
onPress={onPress}
|
|
252
|
+
onLongPress={onLongPress}
|
|
253
|
+
style={[styles.pressable, tabContainerStyle]}
|
|
254
|
+
>
|
|
255
|
+
<View pointerEvents="none" style={[styles.item, tabStyle]}>
|
|
256
|
+
{icon}
|
|
257
|
+
{label}
|
|
258
|
+
{badge != null ? <View style={styles.badge}>{badge}</View> : null}
|
|
259
|
+
</View>
|
|
260
|
+
</PlatformPressable>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const MemoizedTabBarItemInternal = React.memo(
|
|
265
|
+
TabBarItemInternal
|
|
266
|
+
) as typeof TabBarItemInternal;
|
|
267
|
+
|
|
268
|
+
function TabBarItem<T extends Route>(props: Props<T>) {
|
|
269
|
+
const { onPress, onLongPress, onLayout, navigationState, route, ...rest } =
|
|
270
|
+
props;
|
|
271
|
+
const onPressLatest = useLatestCallback(onPress);
|
|
272
|
+
const onLongPressLatest = useLatestCallback(onLongPress);
|
|
273
|
+
const onLayoutLatest = useLatestCallback(onLayout ? onLayout : () => {});
|
|
274
|
+
|
|
275
|
+
const tabIndex = navigationState.routes.indexOf(route);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<MemoizedTabBarItemInternal
|
|
279
|
+
{...rest}
|
|
280
|
+
onPress={onPressLatest}
|
|
281
|
+
onLayout={onLayoutLatest}
|
|
282
|
+
onLongPress={onLongPressLatest}
|
|
283
|
+
isFocused={navigationState.index === tabIndex}
|
|
284
|
+
route={route}
|
|
285
|
+
index={tabIndex}
|
|
286
|
+
routesLength={navigationState.routes.length}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default TabBarItem;
|
|
292
|
+
|
|
293
|
+
const styles = StyleSheet.create({
|
|
294
|
+
label: {
|
|
295
|
+
margin: 4,
|
|
296
|
+
backgroundColor: 'transparent',
|
|
297
|
+
textTransform: 'uppercase',
|
|
298
|
+
},
|
|
299
|
+
icon: {
|
|
300
|
+
margin: 2,
|
|
301
|
+
},
|
|
302
|
+
item: {
|
|
303
|
+
flex: 1,
|
|
304
|
+
alignItems: 'center',
|
|
305
|
+
justifyContent: 'center',
|
|
306
|
+
padding: 10,
|
|
307
|
+
minHeight: 48,
|
|
308
|
+
},
|
|
309
|
+
badge: {
|
|
310
|
+
position: 'absolute',
|
|
311
|
+
top: 0,
|
|
312
|
+
right: 0,
|
|
313
|
+
},
|
|
314
|
+
pressable: {
|
|
315
|
+
// The label is not pressable on Windows
|
|
316
|
+
// Adding backgroundColor: 'transparent' seems to fix it
|
|
317
|
+
backgroundColor: 'transparent',
|
|
318
|
+
},
|
|
319
|
+
});
|
package/src/TabView.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LayoutChangeEvent,
|
|
4
|
+
StyleProp,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
View,
|
|
7
|
+
ViewStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
|
|
10
|
+
import Pager from './Pager';
|
|
11
|
+
import SceneView from './SceneView';
|
|
12
|
+
import TabBar from './TabBar';
|
|
13
|
+
import type {
|
|
14
|
+
Layout,
|
|
15
|
+
NavigationState,
|
|
16
|
+
PagerProps,
|
|
17
|
+
Route,
|
|
18
|
+
SceneRendererProps,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
export type Props<T extends Route> = PagerProps & {
|
|
22
|
+
onIndexChange: (index: number) => void;
|
|
23
|
+
navigationState: NavigationState<T>;
|
|
24
|
+
renderScene: (props: SceneRendererProps & { route: T }) => React.ReactNode;
|
|
25
|
+
renderLazyPlaceholder?: (props: { route: T }) => React.ReactNode;
|
|
26
|
+
renderTabBar?: (
|
|
27
|
+
props: SceneRendererProps & { navigationState: NavigationState<T> }
|
|
28
|
+
) => React.ReactNode;
|
|
29
|
+
tabBarPosition?: 'top' | 'bottom';
|
|
30
|
+
initialLayout?: Partial<Layout>;
|
|
31
|
+
lazy?: ((props: { route: T }) => boolean) | boolean;
|
|
32
|
+
lazyPreloadDistance?: number;
|
|
33
|
+
sceneContainerStyle?: StyleProp<ViewStyle>;
|
|
34
|
+
pagerStyle?: StyleProp<ViewStyle>;
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default function TabView<T extends Route>({
|
|
39
|
+
onIndexChange,
|
|
40
|
+
navigationState,
|
|
41
|
+
renderScene,
|
|
42
|
+
initialLayout,
|
|
43
|
+
keyboardDismissMode = 'auto',
|
|
44
|
+
lazy = false,
|
|
45
|
+
lazyPreloadDistance = 0,
|
|
46
|
+
onSwipeStart,
|
|
47
|
+
onSwipeEnd,
|
|
48
|
+
renderLazyPlaceholder = () => null,
|
|
49
|
+
renderTabBar = (props) => <TabBar {...props} />,
|
|
50
|
+
sceneContainerStyle,
|
|
51
|
+
pagerStyle,
|
|
52
|
+
style,
|
|
53
|
+
swipeEnabled = true,
|
|
54
|
+
tabBarPosition = 'top',
|
|
55
|
+
animationEnabled = true,
|
|
56
|
+
overScrollMode,
|
|
57
|
+
}: Props<T>) {
|
|
58
|
+
const [layout, setLayout] = React.useState({
|
|
59
|
+
width: 0,
|
|
60
|
+
height: 0,
|
|
61
|
+
...initialLayout,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const jumpToIndex = (index: number) => {
|
|
65
|
+
if (index !== navigationState.index) {
|
|
66
|
+
onIndexChange(index);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleLayout = (e: LayoutChangeEvent) => {
|
|
71
|
+
const { height, width } = e.nativeEvent.layout;
|
|
72
|
+
|
|
73
|
+
setLayout((prevLayout) => {
|
|
74
|
+
if (prevLayout.width === width && prevLayout.height === height) {
|
|
75
|
+
return prevLayout;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { height, width };
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<View onLayout={handleLayout} style={[styles.pager, style]}>
|
|
84
|
+
<Pager
|
|
85
|
+
layout={layout}
|
|
86
|
+
navigationState={navigationState}
|
|
87
|
+
keyboardDismissMode={keyboardDismissMode}
|
|
88
|
+
swipeEnabled={swipeEnabled}
|
|
89
|
+
onSwipeStart={onSwipeStart}
|
|
90
|
+
onSwipeEnd={onSwipeEnd}
|
|
91
|
+
onIndexChange={jumpToIndex}
|
|
92
|
+
animationEnabled={animationEnabled}
|
|
93
|
+
overScrollMode={overScrollMode}
|
|
94
|
+
style={pagerStyle}
|
|
95
|
+
>
|
|
96
|
+
{({ position, render, addEnterListener, jumpTo }) => {
|
|
97
|
+
// All of the props here must not change between re-renders
|
|
98
|
+
// This is crucial to optimizing the routes with PureComponent
|
|
99
|
+
const sceneRendererProps = {
|
|
100
|
+
position,
|
|
101
|
+
layout,
|
|
102
|
+
jumpTo,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<React.Fragment>
|
|
107
|
+
{tabBarPosition === 'top' &&
|
|
108
|
+
renderTabBar({
|
|
109
|
+
...sceneRendererProps,
|
|
110
|
+
navigationState,
|
|
111
|
+
})}
|
|
112
|
+
{render(
|
|
113
|
+
navigationState.routes.map((route, i) => {
|
|
114
|
+
return (
|
|
115
|
+
<SceneView
|
|
116
|
+
{...sceneRendererProps}
|
|
117
|
+
addEnterListener={addEnterListener}
|
|
118
|
+
key={route.key}
|
|
119
|
+
index={i}
|
|
120
|
+
lazy={typeof lazy === 'function' ? lazy({ route }) : lazy}
|
|
121
|
+
lazyPreloadDistance={lazyPreloadDistance}
|
|
122
|
+
navigationState={navigationState}
|
|
123
|
+
style={sceneContainerStyle}
|
|
124
|
+
>
|
|
125
|
+
{({ loading }) =>
|
|
126
|
+
loading
|
|
127
|
+
? renderLazyPlaceholder({ route })
|
|
128
|
+
: renderScene({
|
|
129
|
+
...sceneRendererProps,
|
|
130
|
+
route,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
</SceneView>
|
|
134
|
+
);
|
|
135
|
+
})
|
|
136
|
+
)}
|
|
137
|
+
{tabBarPosition === 'bottom' &&
|
|
138
|
+
renderTabBar({
|
|
139
|
+
...sceneRendererProps,
|
|
140
|
+
navigationState,
|
|
141
|
+
})}
|
|
142
|
+
</React.Fragment>
|
|
143
|
+
);
|
|
144
|
+
}}
|
|
145
|
+
</Pager>
|
|
146
|
+
</View>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const styles = StyleSheet.create({
|
|
151
|
+
pager: {
|
|
152
|
+
flex: 1,
|
|
153
|
+
overflow: 'hidden',
|
|
154
|
+
},
|
|
155
|
+
});
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as SceneMap } from './SceneMap';
|
|
2
|
+
export type { Props as TabBarProps } from './TabBar';
|
|
3
|
+
export { default as TabBar } from './TabBar';
|
|
4
|
+
export type { Props as TabBarIndicatorProps } from './TabBarIndicator';
|
|
5
|
+
export { default as TabBarIndicator } from './TabBarIndicator';
|
|
6
|
+
export type { Props as TabBarItemProps } from './TabBarItem';
|
|
7
|
+
export { default as TabBarItem } from './TabBarItem';
|
|
8
|
+
export type { Props as TabViewProps } from './TabView';
|
|
9
|
+
export { default as TabView } from './TabView';
|
|
10
|
+
export type { NavigationState, Route, SceneRendererProps } from './types';
|
package/src/types.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Animated } from 'react-native';
|
|
2
|
+
import type { PagerViewProps } from 'react-native-pager-view';
|
|
3
|
+
|
|
4
|
+
export type Route = {
|
|
5
|
+
key: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
accessible?: boolean;
|
|
9
|
+
accessibilityLabel?: string;
|
|
10
|
+
testID?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Event = {
|
|
14
|
+
defaultPrevented: boolean;
|
|
15
|
+
preventDefault(): void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type Scene<T extends Route> = {
|
|
19
|
+
route: T;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type NavigationState<T extends Route> = {
|
|
23
|
+
index: number;
|
|
24
|
+
routes: T[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type Layout = {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type Listener = (value: number) => void;
|
|
33
|
+
|
|
34
|
+
export type SceneRendererProps = {
|
|
35
|
+
layout: Layout;
|
|
36
|
+
position: Animated.AnimatedInterpolation<number>;
|
|
37
|
+
jumpTo: (key: string) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type EventEmitterProps = {
|
|
41
|
+
addEnterListener: (listener: Listener) => () => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type PagerProps = Omit<
|
|
45
|
+
PagerViewProps,
|
|
46
|
+
| 'initialPage'
|
|
47
|
+
| 'scrollEnabled'
|
|
48
|
+
| 'onPageScroll'
|
|
49
|
+
| 'onPageSelected'
|
|
50
|
+
| 'onPageScrollStateChanged'
|
|
51
|
+
| 'keyboardDismissMode'
|
|
52
|
+
| 'children'
|
|
53
|
+
> & {
|
|
54
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'auto';
|
|
55
|
+
swipeEnabled?: boolean;
|
|
56
|
+
animationEnabled?: boolean;
|
|
57
|
+
onSwipeStart?: () => void;
|
|
58
|
+
onSwipeEnd?: () => void;
|
|
59
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Animated } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export default function useAnimatedValue(initialValue: number) {
|
|
5
|
+
const lazyRef = React.useRef<Animated.Value>();
|
|
6
|
+
|
|
7
|
+
if (lazyRef.current === undefined) {
|
|
8
|
+
lazyRef.current = new Animated.Value(initialValue);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return lazyRef.current as Animated.Value;
|
|
12
|
+
}
|