@zoverdoser/react-native-tab-view 1.0.0

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/lib/module/Pager.android.js +4 -0
  4. package/lib/module/Pager.android.js.map +1 -0
  5. package/lib/module/Pager.ios.js +4 -0
  6. package/lib/module/Pager.ios.js.map +1 -0
  7. package/lib/module/Pager.js +4 -0
  8. package/lib/module/Pager.js.map +1 -0
  9. package/lib/module/PagerViewAdapter.js +131 -0
  10. package/lib/module/PagerViewAdapter.js.map +1 -0
  11. package/lib/module/PanResponderAdapter.js +209 -0
  12. package/lib/module/PanResponderAdapter.js.map +1 -0
  13. package/lib/module/PlatformPressable.js +61 -0
  14. package/lib/module/PlatformPressable.js.map +1 -0
  15. package/lib/module/SceneMap.js +24 -0
  16. package/lib/module/SceneMap.js.map +1 -0
  17. package/lib/module/SceneView.js +72 -0
  18. package/lib/module/SceneView.js.map +1 -0
  19. package/lib/module/TabBar.js +470 -0
  20. package/lib/module/TabBar.js.map +1 -0
  21. package/lib/module/TabBarIndicator.js +122 -0
  22. package/lib/module/TabBarIndicator.js.map +1 -0
  23. package/lib/module/TabBarItem.js +209 -0
  24. package/lib/module/TabBarItem.js.map +1 -0
  25. package/lib/module/TabBarItemLabel.js +34 -0
  26. package/lib/module/TabBarItemLabel.js.map +1 -0
  27. package/lib/module/TabView.js +141 -0
  28. package/lib/module/TabView.js.map +1 -0
  29. package/lib/module/index.js +8 -0
  30. package/lib/module/index.js.map +1 -0
  31. package/lib/module/package.json +1 -0
  32. package/lib/module/types.js +4 -0
  33. package/lib/module/types.js.map +1 -0
  34. package/lib/module/useAnimatedValue.js +12 -0
  35. package/lib/module/useAnimatedValue.js.map +1 -0
  36. package/lib/typescript/package.json +1 -0
  37. package/lib/typescript/src/Pager.android.d.ts +2 -0
  38. package/lib/typescript/src/Pager.android.d.ts.map +1 -0
  39. package/lib/typescript/src/Pager.d.ts +2 -0
  40. package/lib/typescript/src/Pager.d.ts.map +1 -0
  41. package/lib/typescript/src/Pager.ios.d.ts +2 -0
  42. package/lib/typescript/src/Pager.ios.d.ts.map +1 -0
  43. package/lib/typescript/src/PagerViewAdapter.d.ts +18 -0
  44. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -0
  45. package/lib/typescript/src/PanResponderAdapter.d.ts +19 -0
  46. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -0
  47. package/lib/typescript/src/PlatformPressable.d.ts +14 -0
  48. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -0
  49. package/lib/typescript/src/SceneMap.d.ts +10 -0
  50. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  51. package/lib/typescript/src/SceneView.d.ts +16 -0
  52. package/lib/typescript/src/SceneView.d.ts.map +1 -0
  53. package/lib/typescript/src/TabBar.d.ts +31 -0
  54. package/lib/typescript/src/TabBar.d.ts.map +1 -0
  55. package/lib/typescript/src/TabBarIndicator.d.ts +15 -0
  56. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -0
  57. package/lib/typescript/src/TabBarItem.d.ts +18 -0
  58. package/lib/typescript/src/TabBarItem.d.ts.map +1 -0
  59. package/lib/typescript/src/TabBarItemLabel.d.ts +13 -0
  60. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -0
  61. package/lib/typescript/src/TabView.d.ts +33 -0
  62. package/lib/typescript/src/TabView.d.ts.map +1 -0
  63. package/lib/typescript/src/index.d.ts +11 -0
  64. package/lib/typescript/src/index.d.ts.map +1 -0
  65. package/lib/typescript/src/types.d.ts +76 -0
  66. package/lib/typescript/src/types.d.ts.map +1 -0
  67. package/lib/typescript/src/useAnimatedValue.d.ts +3 -0
  68. package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -0
  69. package/package.json +84 -0
  70. package/src/Pager.android.tsx +1 -0
  71. package/src/Pager.ios.tsx +1 -0
  72. package/src/Pager.tsx +1 -0
  73. package/src/PagerViewAdapter.tsx +186 -0
  74. package/src/PanResponderAdapter.tsx +344 -0
  75. package/src/PlatformPressable.tsx +91 -0
  76. package/src/SceneMap.tsx +30 -0
  77. package/src/SceneView.tsx +106 -0
  78. package/src/TabBar.tsx +725 -0
  79. package/src/TabBarIndicator.tsx +190 -0
  80. package/src/TabBarItem.tsx +301 -0
  81. package/src/TabBarItemLabel.tsx +50 -0
  82. package/src/TabView.tsx +198 -0
  83. package/src/index.tsx +16 -0
  84. package/src/types.tsx +94 -0
  85. package/src/useAnimatedValue.tsx +12 -0
@@ -0,0 +1,190 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Platform,
6
+ type StyleProp,
7
+ StyleSheet,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+
11
+ import type {
12
+ LocaleDirection,
13
+ NavigationState,
14
+ Route,
15
+ SceneRendererProps,
16
+ } from './types';
17
+ import { useAnimatedValue } from './useAnimatedValue';
18
+
19
+ export type GetTabWidth = (index: number) => number;
20
+
21
+ export type Props<T extends Route> = SceneRendererProps & {
22
+ navigationState: NavigationState<T>;
23
+ width: 'auto' | `${number}%` | number;
24
+ getTabWidth: GetTabWidth;
25
+ direction: LocaleDirection;
26
+ style?: StyleProp<ViewStyle>;
27
+ gap?: number;
28
+ children?: React.ReactNode;
29
+ };
30
+
31
+ const useNativeDriver = Platform.OS !== 'web';
32
+
33
+ const getTranslateX = (
34
+ position: Animated.AnimatedInterpolation<number>,
35
+ routes: Route[],
36
+ getTabWidth: GetTabWidth,
37
+ direction: LocaleDirection,
38
+ gap?: number,
39
+ width?: number | string
40
+ ) => {
41
+ const inputRange = routes.map((_, i) => i);
42
+
43
+ // every index contains widths at all previous indices
44
+ const outputRange = routes.reduce<number[]>((acc, _, i) => {
45
+ if (typeof width === 'number') {
46
+ if (i === 0) return [getTabWidth(i) / 2 - width / 2];
47
+
48
+ let sumTabWidth = 0;
49
+ for (let j = 0; j < i; j++) {
50
+ sumTabWidth += getTabWidth(j);
51
+ }
52
+
53
+ return [
54
+ ...acc,
55
+ sumTabWidth + getTabWidth(i) / 2 + (gap ? gap * i : 0) - width / 2,
56
+ ];
57
+ } else {
58
+ if (i === 0) return [0];
59
+ return [...acc, acc[i - 1] + getTabWidth(i - 1) + (gap ?? 0)];
60
+ }
61
+ }, []);
62
+
63
+ const translateX = position.interpolate({
64
+ inputRange,
65
+ outputRange,
66
+ extrapolate: 'clamp',
67
+ });
68
+
69
+ return Animated.multiply(translateX, direction === 'rtl' ? -1 : 1);
70
+ };
71
+
72
+ export function TabBarIndicator<T extends Route>({
73
+ getTabWidth,
74
+ layout,
75
+ navigationState,
76
+ position,
77
+ width,
78
+ direction,
79
+ gap,
80
+ style,
81
+ children,
82
+ }: Props<T>) {
83
+ const isIndicatorShown = React.useRef(false);
84
+ const isWidthDynamic = width === 'auto';
85
+
86
+ const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
87
+
88
+ const indicatorVisible = isWidthDynamic
89
+ ? layout.width &&
90
+ navigationState.routes
91
+ .slice(0, navigationState.index)
92
+ .every((_, r) => getTabWidth(r))
93
+ : true;
94
+
95
+ React.useEffect(() => {
96
+ const fadeInIndicator = () => {
97
+ if (
98
+ !isIndicatorShown.current &&
99
+ isWidthDynamic &&
100
+ // We should fade-in the indicator when we have widths for all the tab items
101
+ indicatorVisible
102
+ ) {
103
+ isIndicatorShown.current = true;
104
+
105
+ Animated.timing(opacity, {
106
+ toValue: 1,
107
+ duration: 150,
108
+ easing: Easing.in(Easing.linear),
109
+ useNativeDriver,
110
+ }).start();
111
+ }
112
+ };
113
+
114
+ fadeInIndicator();
115
+
116
+ return () => opacity.stopAnimation();
117
+ }, [indicatorVisible, isWidthDynamic, opacity]);
118
+
119
+ const { routes } = navigationState;
120
+
121
+ const transform = [];
122
+
123
+ if (layout.width) {
124
+ const translateX =
125
+ routes.length > 1
126
+ ? getTranslateX(position, routes, getTabWidth, direction, gap, width)
127
+ : 0;
128
+
129
+ transform.push({ translateX });
130
+ }
131
+
132
+ if (width === 'auto') {
133
+ const inputRange = routes.map((_, i) => i);
134
+ const outputRange = inputRange.map(getTabWidth);
135
+
136
+ transform.push(
137
+ {
138
+ scaleX:
139
+ routes.length > 1
140
+ ? position.interpolate({
141
+ inputRange,
142
+ outputRange,
143
+ extrapolate: 'clamp',
144
+ })
145
+ : outputRange[0],
146
+ },
147
+ { translateX: direction === 'rtl' ? -0.5 : 0.5 }
148
+ );
149
+ }
150
+
151
+ const styleList: StyleProp<ViewStyle> = [];
152
+
153
+ // scaleX doesn't work properly on chrome and opera for linux and android
154
+ if (Platform.OS === 'web' && width === 'auto') {
155
+ styleList.push(
156
+ { width: transform[1].scaleX },
157
+ { left: transform[0].translateX }
158
+ );
159
+ } else {
160
+ styleList.push(
161
+ { width: width === 'auto' ? 1 : width },
162
+ { start: `${(100 / routes.length) * navigationState.index}%` },
163
+ { transform }
164
+ );
165
+ }
166
+
167
+ return (
168
+ <Animated.View
169
+ style={[
170
+ styles.indicator,
171
+ styleList,
172
+ width === 'auto' ? { opacity: opacity } : null,
173
+ style,
174
+ ]}
175
+ >
176
+ {children}
177
+ </Animated.View>
178
+ );
179
+ }
180
+
181
+ const styles = StyleSheet.create({
182
+ indicator: {
183
+ backgroundColor: '#ffeb3b',
184
+ position: 'absolute',
185
+ start: 0,
186
+ bottom: 0,
187
+ end: 0,
188
+ height: 2,
189
+ },
190
+ });
@@ -0,0 +1,301 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Animated,
4
+ type LayoutChangeEvent,
5
+ Platform,
6
+ type PressableAndroidRippleConfig,
7
+ type StyleProp,
8
+ StyleSheet,
9
+ View,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import {
13
+ interpolateColor,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ } from 'react-native-reanimated';
17
+ import useLatestCallback from 'use-latest-callback';
18
+
19
+ import { PlatformPressable } from './PlatformPressable';
20
+ import { TabBarItemLabel } from './TabBarItemLabel';
21
+ import type {
22
+ AnimatedStyles,
23
+ NavigationState,
24
+ Route,
25
+ TabDescriptor,
26
+ } from './types';
27
+
28
+ export type Props<T extends Route> = TabDescriptor<T> & {
29
+ position: Animated.AnimatedInterpolation<number>;
30
+ route: T;
31
+ navigationState: NavigationState<T>;
32
+ pressColor?: string;
33
+ pressOpacity?: number;
34
+ onLayout?: (event: LayoutChangeEvent) => void;
35
+ onPress: () => void;
36
+ onLongPress: () => void;
37
+ defaultTabWidth?: number;
38
+ style: StyleProp<ViewStyle>;
39
+ android_ripple?: PressableAndroidRippleConfig;
40
+ animatedStyles?: AnimatedStyles;
41
+ };
42
+
43
+ const DEFAULT_ACTIVE_COLOR = 'rgba(255, 255, 255, 1)';
44
+ const DEFAULT_INACTIVE_COLOR = 'rgba(255, 255, 255, 0.7)';
45
+ const ICON_SIZE = 24;
46
+
47
+ type TabBarItemInternalProps<T extends Route> = Omit<
48
+ Props<T>,
49
+ | 'navigationState'
50
+ | 'getAccessibilityLabel'
51
+ | 'getLabelText'
52
+ | 'getTestID'
53
+ | 'getAccessible'
54
+ | 'options'
55
+ > & {
56
+ isFocused: boolean;
57
+ index: number;
58
+ routesLength: number;
59
+ } & TabDescriptor<T>;
60
+
61
+ const ANDROID_RIPPLE_DEFAULT = { borderless: true };
62
+
63
+ const TabBarItemInternal = <T extends Route>({
64
+ accessibilityLabel,
65
+ accessible,
66
+ label: customlabel,
67
+ testID,
68
+ onLongPress,
69
+ onPress,
70
+ isFocused,
71
+ position,
72
+ style,
73
+ labelStyle,
74
+ onLayout,
75
+ index: tabIndex,
76
+ pressColor,
77
+ pressOpacity,
78
+ defaultTabWidth,
79
+ icon: customIcon,
80
+ badge: customBadge,
81
+ href,
82
+ labelText,
83
+ routesLength,
84
+ android_ripple = ANDROID_RIPPLE_DEFAULT,
85
+ labelAllowFontScaling,
86
+ route,
87
+ animatedStyles,
88
+ }: TabBarItemInternalProps<T>) => {
89
+ const inputRange = React.useMemo(
90
+ () => Array.from({ length: routesLength }, (_, i) => i),
91
+ [routesLength]
92
+ );
93
+ const labelColorFromStyle = StyleSheet.flatten(labelStyle || {}).color;
94
+
95
+ const activeColor = animatedStyles?.color
96
+ ? animatedStyles?.color[1]
97
+ : typeof labelColorFromStyle === 'string'
98
+ ? labelColorFromStyle
99
+ : DEFAULT_ACTIVE_COLOR;
100
+ const inactiveColor = animatedStyles?.color
101
+ ? animatedStyles?.color[0]
102
+ : typeof labelColorFromStyle === 'string'
103
+ ? labelColorFromStyle
104
+ : DEFAULT_INACTIVE_COLOR;
105
+
106
+ const ReAnimatedProgress = useSharedValue(isFocused ? 1 : 0);
107
+
108
+ React.useEffect(() => {
109
+ const listenerId = position.addListener(({ value }) => {
110
+ const progress =
111
+ Math.abs(tabIndex - value) > 1 ? 0 : 1 - Math.abs(tabIndex - value);
112
+ ReAnimatedProgress.value = progress;
113
+ });
114
+ return () => {
115
+ position.removeListener(listenerId);
116
+ };
117
+ }, [position, ReAnimatedProgress, tabIndex, route.key]);
118
+
119
+ const ReAnimatedStyleRef = React.useRef(
120
+ useAnimatedStyle(() => {
121
+ const color = interpolateColor(
122
+ ReAnimatedProgress.value,
123
+ [0, 1],
124
+ [inactiveColor, activeColor]
125
+ );
126
+ return { color };
127
+ }, [ReAnimatedProgress, activeColor, inactiveColor])
128
+ );
129
+
130
+ const opacity = animatedStyles?.opacity
131
+ ? position.interpolate({
132
+ inputRange: Array.from({ length: routesLength }, (_, i) => i),
133
+ outputRange: inputRange.map((i) =>
134
+ i === tabIndex
135
+ ? animatedStyles.opacity![1]
136
+ : animatedStyles.opacity![0]
137
+ ),
138
+ extrapolate: 'clamp',
139
+ })
140
+ : 1;
141
+
142
+ const scale = animatedStyles?.scale
143
+ ? position.interpolate({
144
+ inputRange: Array.from({ length: routesLength }, (_, i) => i),
145
+ outputRange: inputRange.map((i) =>
146
+ i === tabIndex ? animatedStyles.scale![1] : animatedStyles.scale![0]
147
+ ),
148
+ extrapolate: 'clamp',
149
+ })
150
+ : 1;
151
+
152
+ const icon = React.useMemo(() => {
153
+ if (!customIcon) {
154
+ return null;
155
+ }
156
+
157
+ const iconEle = customIcon({
158
+ focused: isFocused,
159
+ color: ReAnimatedStyleRef.current.color,
160
+ size: ICON_SIZE,
161
+ route,
162
+ });
163
+
164
+ return (
165
+ <View style={styles.icon}>
166
+ <Animated.View style={{ opacity }}>{iconEle}</Animated.View>
167
+ </View>
168
+ );
169
+ }, [customIcon, route, isFocused, ReAnimatedStyleRef, opacity]);
170
+
171
+ const renderLabel = React.useCallback(
172
+ () =>
173
+ customlabel ? (
174
+ customlabel({
175
+ focused: isFocused,
176
+ style: labelStyle,
177
+ animatedStyles: ReAnimatedStyleRef.current,
178
+ labelText,
179
+ allowFontScaling: labelAllowFontScaling,
180
+ route,
181
+ })
182
+ ) : (
183
+ <TabBarItemLabel
184
+ icon={icon}
185
+ label={labelText}
186
+ style={labelStyle}
187
+ animatedStyles={ReAnimatedStyleRef.current}
188
+ labelAllowFontScaling={labelAllowFontScaling}
189
+ />
190
+ ),
191
+ [
192
+ customlabel,
193
+ labelStyle,
194
+ labelText,
195
+ labelAllowFontScaling,
196
+ route,
197
+ icon,
198
+ isFocused,
199
+ ReAnimatedStyleRef,
200
+ ]
201
+ );
202
+
203
+ const tabStyle = StyleSheet.flatten(style);
204
+ const isWidthSet = tabStyle?.width !== undefined;
205
+
206
+ const tabContainerStyle: ViewStyle | null = isWidthSet
207
+ ? null
208
+ : { width: defaultTabWidth };
209
+
210
+ const ariaLabel =
211
+ typeof accessibilityLabel !== 'undefined' ? accessibilityLabel : labelText;
212
+
213
+ return (
214
+ <PlatformPressable
215
+ android_ripple={android_ripple}
216
+ testID={testID}
217
+ accessible={accessible}
218
+ role="tab"
219
+ aria-label={ariaLabel}
220
+ aria-selected={isFocused}
221
+ pressColor={pressColor}
222
+ pressOpacity={pressOpacity}
223
+ unstable_pressDelay={0}
224
+ onLayout={onLayout}
225
+ onPress={onPress}
226
+ onLongPress={onLongPress}
227
+ href={href}
228
+ style={[styles.pressable, tabContainerStyle]}
229
+ >
230
+ <Animated.View
231
+ pointerEvents="none"
232
+ style={[styles.item, tabStyle, { opacity, transform: [{ scale }] }]}
233
+ >
234
+ {icon}
235
+ <View>{renderLabel()}</View>
236
+ {customBadge != null ? (
237
+ <View style={styles.badge}>{customBadge({ route })}</View>
238
+ ) : null}
239
+ </Animated.View>
240
+ </PlatformPressable>
241
+ );
242
+ };
243
+
244
+ const MemoizedTabBarItemInternal = React.memo(
245
+ TabBarItemInternal
246
+ ) as typeof TabBarItemInternal;
247
+
248
+ export function TabBarItem<T extends Route>(props: Props<T>) {
249
+ const { onPress, onLongPress, onLayout, navigationState, route, ...rest } =
250
+ props;
251
+
252
+ const onPressLatest = useLatestCallback(onPress);
253
+ const onLongPressLatest = useLatestCallback(onLongPress);
254
+ const onLayoutLatest = useLatestCallback(onLayout ? onLayout : () => {});
255
+
256
+ const tabIndex = navigationState.routes.indexOf(route);
257
+
258
+ return (
259
+ <MemoizedTabBarItemInternal
260
+ {...rest}
261
+ onPress={onPressLatest}
262
+ onLayout={onLayoutLatest}
263
+ onLongPress={onLongPressLatest}
264
+ isFocused={navigationState.index === tabIndex}
265
+ route={route}
266
+ index={tabIndex}
267
+ routesLength={navigationState.routes.length}
268
+ />
269
+ );
270
+ }
271
+
272
+ const styles = StyleSheet.create({
273
+ icon: {
274
+ margin: 2,
275
+ },
276
+ item: {
277
+ flex: 1,
278
+ alignItems: 'center',
279
+ justifyContent: 'center',
280
+ padding: 10,
281
+ minHeight: 48,
282
+ },
283
+ badge: {
284
+ position: 'absolute',
285
+ top: 0,
286
+ end: 0,
287
+ },
288
+ pressable: {
289
+ // The label is not pressable on Windows
290
+ // Adding backgroundColor: 'transparent' seems to fix it
291
+ backgroundColor: 'transparent',
292
+ ...Platform.select({
293
+ // Roundness for iPad hover effect
294
+ ios: {
295
+ borderRadius: 10,
296
+ borderCurve: 'continuous',
297
+ },
298
+ default: null,
299
+ }),
300
+ },
301
+ });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import type { StyleProp, TextStyle } from 'react-native';
3
+ import { StyleSheet } from 'react-native';
4
+ import Animated, { type AnimatedStyle } from 'react-native-reanimated';
5
+ interface TabBarItemLabelProps {
6
+ label?: string;
7
+ style: StyleProp<TextStyle>;
8
+ animatedStyles: AnimatedStyle<TextStyle>;
9
+ icon: React.ReactNode;
10
+ labelAllowFontScaling?: boolean;
11
+ }
12
+
13
+ export const TabBarItemLabel = React.memo(
14
+ ({
15
+ label,
16
+ style,
17
+ animatedStyles,
18
+ icon,
19
+ labelAllowFontScaling,
20
+ }: TabBarItemLabelProps) => {
21
+ if (!label) {
22
+ return null;
23
+ }
24
+
25
+ return (
26
+ <Animated.Text
27
+ style={[
28
+ styles.label,
29
+ icon ? { marginTop: 0 } : undefined,
30
+ style,
31
+ animatedStyles,
32
+ ]}
33
+ allowFontScaling={labelAllowFontScaling}
34
+ >
35
+ {label}
36
+ </Animated.Text>
37
+ );
38
+ }
39
+ );
40
+
41
+ TabBarItemLabel.displayName = 'TabBarItemLabel';
42
+
43
+ const styles = StyleSheet.create({
44
+ label: {
45
+ margin: 4,
46
+ fontSize: 14,
47
+ fontWeight: '500',
48
+ backgroundColor: 'transparent',
49
+ },
50
+ });