UNPKG

7.44 kBTypeScriptView Raw
1import * as React from 'react';
2import {
3 Animated,
4 StyleSheet,
5 View,
6 StyleProp,
7 LayoutChangeEvent,
8 TextStyle,
9 ViewStyle,
10} from 'react-native';
11import PlatformPressable from './PlatformPressable';
12import type { Scene, Route, NavigationState } from './types';
13
14export type Props<T extends Route> = {
15 position: Animated.AnimatedInterpolation;
16 route: T;
17 navigationState: NavigationState<T>;
18 activeColor?: string;
19 inactiveColor?: string;
20 pressColor?: string;
21 pressOpacity?: number;
22 getLabelText: (scene: Scene<T>) => string | undefined;
23 getAccessible: (scene: Scene<T>) => boolean | undefined;
24 getAccessibilityLabel: (scene: Scene<T>) => string | undefined;
25 getTestID: (scene: Scene<T>) => string | undefined;
26 renderLabel?: (scene: {
27 route: T;
28 focused: boolean;
29 color: string;
30 }) => React.ReactNode;
31 renderIcon?: (scene: {
32 route: T;
33 focused: boolean;
34 color: string;
35 }) => React.ReactNode;
36 renderBadge?: (scene: Scene<T>) => React.ReactNode;
37 onLayout?: (event: LayoutChangeEvent) => void;
38 onPress: () => void;
39 onLongPress: () => void;
40 labelStyle?: StyleProp<TextStyle>;
41 style: StyleProp<ViewStyle>;
42};
43
44const DEFAULT_ACTIVE_COLOR = 'rgba(255, 255, 255, 1)';
45const DEFAULT_INACTIVE_COLOR = 'rgba(255, 255, 255, 0.7)';
46
47export default class TabBarItem<T extends Route> extends React.Component<
48 Props<T>
49> {
50 private getActiveOpacity = (
51 position: Animated.AnimatedInterpolation,
52 routes: Route[],
53 tabIndex: number
54 ) => {
55 if (routes.length > 1) {
56 const inputRange = routes.map((_, i) => i);
57
58 return position.interpolate({
59 inputRange,
60 outputRange: inputRange.map((i) => (i === tabIndex ? 1 : 0)),
61 });
62 } else {
63 return 1;
64 }
65 };
66
67 private getInactiveOpacity = (
68 position: Animated.AnimatedInterpolation,
69 routes: Route[],
70 tabIndex: number
71 ) => {
72 if (routes.length > 1) {
73 const inputRange = routes.map((_: Route, i: number) => i);
74
75 return position.interpolate({
76 inputRange,
77 outputRange: inputRange.map((i: number) => (i === tabIndex ? 0 : 1)),
78 });
79 } else {
80 return 0;
81 }
82 };
83
84 render() {
85 const {
86 route,
87 position,
88 navigationState,
89 renderLabel: renderLabelCustom,
90 renderIcon,
91 renderBadge,
92 getLabelText,
93 getTestID,
94 getAccessibilityLabel,
95 getAccessible,
96 activeColor: activeColorCustom,
97 inactiveColor: inactiveColorCustom,
98 pressColor,
99 pressOpacity,
100 labelStyle,
101 style,
102 onLayout,
103 onPress,
104 onLongPress,
105 } = this.props;
106
107 const tabIndex = navigationState.routes.indexOf(route);
108 const isFocused = navigationState.index === tabIndex;
109
110 const labelColorFromStyle = StyleSheet.flatten(labelStyle || {}).color;
111
112 const activeColor =
113 activeColorCustom !== undefined
114 ? activeColorCustom
115 : typeof labelColorFromStyle === 'string'
116 ? labelColorFromStyle
117 : DEFAULT_ACTIVE_COLOR;
118 const inactiveColor =
119 inactiveColorCustom !== undefined
120 ? inactiveColorCustom
121 : typeof labelColorFromStyle === 'string'
122 ? labelColorFromStyle
123 : DEFAULT_INACTIVE_COLOR;
124
125 const activeOpacity = this.getActiveOpacity(
126 position,
127 navigationState.routes,
128 tabIndex
129 );
130 const inactiveOpacity = this.getInactiveOpacity(
131 position,
132 navigationState.routes,
133 tabIndex
134 );
135
136 let icon: React.ReactNode | null = null;
137 let label: React.ReactNode | null = null;
138
139 if (renderIcon) {
140 const activeIcon = renderIcon({
141 route,
142 focused: true,
143 color: activeColor,
144 });
145 const inactiveIcon = renderIcon({
146 route,
147 focused: false,
148 color: inactiveColor,
149 });
150
151 if (inactiveIcon != null && activeIcon != null) {
152 icon = (
153 <View style={styles.icon}>
154 <Animated.View style={{ opacity: inactiveOpacity }}>
155 {inactiveIcon}
156 </Animated.View>
157 <Animated.View
158 style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
159 >
160 {activeIcon}
161 </Animated.View>
162 </View>
163 );
164 }
165 }
166
167 const renderLabel =
168 renderLabelCustom !== undefined
169 ? renderLabelCustom
170 : ({ route, color }: { route: T; color: string }) => {
171 const labelText = getLabelText({ route });
172
173 if (typeof labelText === 'string') {
174 return (
175 <Animated.Text
176 style={[
177 styles.label,
178 icon ? { marginTop: 0 } : null,
179 labelStyle,
180 { color },
181 ]}
182 >
183 {labelText}
184 </Animated.Text>
185 );
186 }
187
188 return labelText;
189 };
190
191 if (renderLabel) {
192 const activeLabel = renderLabel({
193 route,
194 focused: true,
195 color: activeColor,
196 });
197 const inactiveLabel = renderLabel({
198 route,
199 focused: false,
200 color: inactiveColor,
201 });
202
203 label = (
204 <View>
205 <Animated.View style={{ opacity: inactiveOpacity }}>
206 {inactiveLabel}
207 </Animated.View>
208 <Animated.View
209 style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
210 >
211 {activeLabel}
212 </Animated.View>
213 </View>
214 );
215 }
216
217 const tabStyle = StyleSheet.flatten(style);
218 const isWidthSet = tabStyle?.width !== undefined;
219 const tabContainerStyle: ViewStyle | null = isWidthSet ? null : { flex: 1 };
220
221 const scene = { route };
222
223 let accessibilityLabel = getAccessibilityLabel(scene);
224
225 accessibilityLabel =
226 typeof accessibilityLabel !== 'undefined'
227 ? accessibilityLabel
228 : getLabelText(scene);
229
230 const badge = renderBadge ? renderBadge(scene) : null;
231
232 return (
233 <PlatformPressable
234 android_ripple={{ borderless: true }}
235 testID={getTestID(scene)}
236 accessible={getAccessible(scene)}
237 accessibilityLabel={accessibilityLabel}
238 accessibilityRole="tab"
239 accessibilityState={{ selected: isFocused }}
240 // @ts-ignore: this is to support older React Native versions
241 accessibilityStates={isFocused ? ['selected'] : []}
242 pressColor={pressColor}
243 pressOpacity={pressOpacity}
244 delayPressIn={0}
245 onLayout={onLayout}
246 onPress={onPress}
247 onLongPress={onLongPress}
248 style={[styles.pressable, tabContainerStyle]}
249 >
250 <View pointerEvents="none" style={[styles.item, tabStyle]}>
251 {icon}
252 {label}
253 {badge != null ? <View style={styles.badge}>{badge}</View> : null}
254 </View>
255 </PlatformPressable>
256 );
257 }
258}
259
260const styles = StyleSheet.create({
261 label: {
262 margin: 4,
263 backgroundColor: 'transparent',
264 textTransform: 'uppercase',
265 },
266 icon: {
267 margin: 2,
268 },
269 item: {
270 flex: 1,
271 alignItems: 'center',
272 justifyContent: 'center',
273 padding: 10,
274 minHeight: 48,
275 },
276 badge: {
277 position: 'absolute',
278 top: 0,
279 right: 0,
280 },
281 pressable: {
282 // The label is not pressable on Windows
283 // Adding backgroundColor: 'transparent' seems to fix it
284 backgroundColor: 'transparent',
285 },
286});