1 | import * as React from 'react';
|
2 | import {
|
3 | Animated,
|
4 | StyleSheet,
|
5 | View,
|
6 | StyleProp,
|
7 | LayoutChangeEvent,
|
8 | TextStyle,
|
9 | ViewStyle,
|
10 | } from 'react-native';
|
11 | import PlatformPressable from './PlatformPressable';
|
12 | import type { Scene, Route, NavigationState } from './types';
|
13 |
|
14 | export 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 |
|
44 | const DEFAULT_ACTIVE_COLOR = 'rgba(255, 255, 255, 1)';
|
45 | const DEFAULT_INACTIVE_COLOR = 'rgba(255, 255, 255, 0.7)';
|
46 |
|
47 | export 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 |
|
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 |
|
260 | const 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 |
|
283 |
|
284 | backgroundColor: 'transparent',
|
285 | },
|
286 | });
|