@react-native-ohos/react-native-tab-view 4.0.11-rc.1

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.OpenSource +11 -0
  3. package/README.md +9 -0
  4. package/lib/module/Pager.android.js +4 -0
  5. package/lib/module/Pager.android.js.map +1 -0
  6. package/lib/module/Pager.ios.js +4 -0
  7. package/lib/module/Pager.ios.js.map +1 -0
  8. package/lib/module/Pager.js +4 -0
  9. package/lib/module/Pager.js.map +1 -0
  10. package/lib/module/PagerViewAdapter.js +126 -0
  11. package/lib/module/PagerViewAdapter.js.map +1 -0
  12. package/lib/module/PanResponderAdapter.js +200 -0
  13. package/lib/module/PanResponderAdapter.js.map +1 -0
  14. package/lib/module/PlatformPressable.js +59 -0
  15. package/lib/module/PlatformPressable.js.map +1 -0
  16. package/lib/module/SceneMap.js +24 -0
  17. package/lib/module/SceneMap.js.map +1 -0
  18. package/lib/module/SceneView.js +73 -0
  19. package/lib/module/SceneView.js.map +1 -0
  20. package/lib/module/TabBar.js +472 -0
  21. package/lib/module/TabBar.js.map +1 -0
  22. package/lib/module/TabBarIndicator.js +122 -0
  23. package/lib/module/TabBarIndicator.js.map +1 -0
  24. package/lib/module/TabBarItem.js +218 -0
  25. package/lib/module/TabBarItem.js.map +1 -0
  26. package/lib/module/TabBarItemLabel.js +33 -0
  27. package/lib/module/TabBarItemLabel.js.map +1 -0
  28. package/lib/module/TabView.js +140 -0
  29. package/lib/module/TabView.js.map +1 -0
  30. package/lib/module/index.js +8 -0
  31. package/lib/module/index.js.map +1 -0
  32. package/lib/module/package.json +1 -0
  33. package/lib/module/types.js +4 -0
  34. package/lib/module/types.js.map +1 -0
  35. package/lib/module/useAnimatedValue.js +12 -0
  36. package/lib/module/useAnimatedValue.js.map +1 -0
  37. package/lib/typescript/package.json +1 -0
  38. package/lib/typescript/src/Pager.android.d.ts +2 -0
  39. package/lib/typescript/src/Pager.android.d.ts.map +1 -0
  40. package/lib/typescript/src/Pager.d.ts +2 -0
  41. package/lib/typescript/src/Pager.d.ts.map +1 -0
  42. package/lib/typescript/src/Pager.ios.d.ts +2 -0
  43. package/lib/typescript/src/Pager.ios.d.ts.map +1 -0
  44. package/lib/typescript/src/PagerViewAdapter.d.ts +15 -0
  45. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -0
  46. package/lib/typescript/src/PanResponderAdapter.d.ts +16 -0
  47. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -0
  48. package/lib/typescript/src/PlatformPressable.d.ts +13 -0
  49. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -0
  50. package/lib/typescript/src/SceneMap.d.ts +10 -0
  51. package/lib/typescript/src/SceneMap.d.ts.map +1 -0
  52. package/lib/typescript/src/SceneView.d.ts +16 -0
  53. package/lib/typescript/src/SceneView.d.ts.map +1 -0
  54. package/lib/typescript/src/TabBar.d.ts +32 -0
  55. package/lib/typescript/src/TabBar.d.ts.map +1 -0
  56. package/lib/typescript/src/TabBarIndicator.d.ts +15 -0
  57. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -0
  58. package/lib/typescript/src/TabBarItem.d.ts +19 -0
  59. package/lib/typescript/src/TabBarItem.d.ts.map +1 -0
  60. package/lib/typescript/src/TabBarItemLabel.d.ts +11 -0
  61. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -0
  62. package/lib/typescript/src/TabView.d.ts +30 -0
  63. package/lib/typescript/src/TabView.d.ts.map +1 -0
  64. package/lib/typescript/src/index.d.ts +11 -0
  65. package/lib/typescript/src/index.d.ts.map +1 -0
  66. package/lib/typescript/src/types.d.ts +70 -0
  67. package/lib/typescript/src/types.d.ts.map +1 -0
  68. package/lib/typescript/src/useAnimatedValue.d.ts +3 -0
  69. package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -0
  70. package/package.json +79 -0
  71. package/src/Pager.android.tsx +1 -0
  72. package/src/Pager.ios.tsx +1 -0
  73. package/src/Pager.tsx +1 -0
  74. package/src/PagerViewAdapter.tsx +182 -0
  75. package/src/PanResponderAdapter.tsx +339 -0
  76. package/src/PlatformPressable.tsx +75 -0
  77. package/src/SceneMap.tsx +30 -0
  78. package/src/SceneView.tsx +107 -0
  79. package/src/TabBar.tsx +729 -0
  80. package/src/TabBarIndicator.tsx +190 -0
  81. package/src/TabBarItem.tsx +305 -0
  82. package/src/TabBarItemLabel.tsx +42 -0
  83. package/src/TabView.tsx +195 -0
  84. package/src/index.tsx +15 -0
  85. package/src/types.tsx +87 -0
  86. 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,305 @@
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 useLatestCallback from 'use-latest-callback';
13
+
14
+ import { PlatformPressable } from './PlatformPressable';
15
+ import { TabBarItemLabel } from './TabBarItemLabel';
16
+ import type { NavigationState, Route, TabDescriptor } from './types';
17
+
18
+ export type Props<T extends Route> = TabDescriptor<T> & {
19
+ position: Animated.AnimatedInterpolation<number>;
20
+ route: T;
21
+ navigationState: NavigationState<T>;
22
+ activeColor?: string;
23
+ inactiveColor?: string;
24
+ pressColor?: string;
25
+ pressOpacity?: number;
26
+ onLayout?: (event: LayoutChangeEvent) => void;
27
+ onPress: () => void;
28
+ onLongPress: () => void;
29
+ defaultTabWidth?: number;
30
+ style: StyleProp<ViewStyle>;
31
+ android_ripple?: PressableAndroidRippleConfig;
32
+ };
33
+
34
+ const DEFAULT_ACTIVE_COLOR = 'rgba(255, 255, 255, 1)';
35
+ const DEFAULT_INACTIVE_COLOR = 'rgba(255, 255, 255, 0.7)';
36
+ const ICON_SIZE = 24;
37
+
38
+ const getActiveOpacity = (
39
+ position: Animated.AnimatedInterpolation<number>,
40
+ routesLength: number,
41
+ tabIndex: number
42
+ ) => {
43
+ if (routesLength > 1) {
44
+ const inputRange = Array.from({ length: routesLength }, (_, i) => i);
45
+
46
+ return position.interpolate({
47
+ inputRange,
48
+ outputRange: inputRange.map((i) => (i === tabIndex ? 1 : 0)),
49
+ });
50
+ } else {
51
+ return 1;
52
+ }
53
+ };
54
+
55
+ const getInactiveOpacity = (
56
+ position: Animated.AnimatedInterpolation<number>,
57
+ routesLength: number,
58
+ tabIndex: number
59
+ ) => {
60
+ if (routesLength > 1) {
61
+ const inputRange = Array.from({ length: routesLength }, (_, i) => i);
62
+
63
+ return position.interpolate({
64
+ inputRange,
65
+ outputRange: inputRange.map((i: number) => (i === tabIndex ? 0 : 1)),
66
+ });
67
+ } else {
68
+ return 0;
69
+ }
70
+ };
71
+
72
+ type TabBarItemInternalProps<T extends Route> = Omit<
73
+ Props<T>,
74
+ | 'navigationState'
75
+ | 'getAccessibilityLabel'
76
+ | 'getLabelText'
77
+ | 'getTestID'
78
+ | 'getAccessible'
79
+ | 'options'
80
+ > & {
81
+ isFocused: boolean;
82
+ index: number;
83
+ routesLength: number;
84
+ } & TabDescriptor<T>;
85
+
86
+ const ANDROID_RIPPLE_DEFAULT = { borderless: true };
87
+
88
+ const TabBarItemInternal = <T extends Route>({
89
+ accessibilityLabel,
90
+ accessible,
91
+ label: customlabel,
92
+ testID,
93
+ onLongPress,
94
+ onPress,
95
+ isFocused,
96
+ position,
97
+ style,
98
+ inactiveColor: inactiveColorCustom,
99
+ activeColor: activeColorCustom,
100
+ labelStyle,
101
+ onLayout,
102
+ index: tabIndex,
103
+ pressColor,
104
+ pressOpacity,
105
+ defaultTabWidth,
106
+ icon: customIcon,
107
+ badge: customBadge,
108
+ href,
109
+ labelText,
110
+ routesLength,
111
+ android_ripple = ANDROID_RIPPLE_DEFAULT,
112
+ labelAllowFontScaling,
113
+ route,
114
+ }: TabBarItemInternalProps<T>) => {
115
+ const labelColorFromStyle = StyleSheet.flatten(labelStyle || {}).color;
116
+
117
+ const activeColor =
118
+ activeColorCustom !== undefined
119
+ ? activeColorCustom
120
+ : typeof labelColorFromStyle === 'string'
121
+ ? labelColorFromStyle
122
+ : DEFAULT_ACTIVE_COLOR;
123
+ const inactiveColor =
124
+ inactiveColorCustom !== undefined
125
+ ? inactiveColorCustom
126
+ : typeof labelColorFromStyle === 'string'
127
+ ? labelColorFromStyle
128
+ : DEFAULT_INACTIVE_COLOR;
129
+
130
+ const activeOpacity = getActiveOpacity(position, routesLength, tabIndex);
131
+ const inactiveOpacity = getInactiveOpacity(position, routesLength, tabIndex);
132
+
133
+ const icon = React.useMemo(() => {
134
+ if (!customIcon) {
135
+ return null;
136
+ }
137
+
138
+ const inactiveIcon = customIcon({
139
+ focused: false,
140
+ color: inactiveColor,
141
+ size: ICON_SIZE,
142
+ route,
143
+ });
144
+
145
+ const activeIcon = customIcon({
146
+ focused: true,
147
+ color: activeColor,
148
+ size: ICON_SIZE,
149
+ route,
150
+ });
151
+
152
+ return (
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
+ activeColor,
166
+ activeOpacity,
167
+ customIcon,
168
+ inactiveColor,
169
+ inactiveOpacity,
170
+ route,
171
+ ]);
172
+
173
+ const renderLabel = React.useCallback(
174
+ (focused: boolean) =>
175
+ customlabel ? (
176
+ customlabel({
177
+ focused,
178
+ color: focused ? activeColor : inactiveColor,
179
+ style: labelStyle,
180
+ labelText,
181
+ allowFontScaling: labelAllowFontScaling,
182
+ route,
183
+ })
184
+ ) : (
185
+ <TabBarItemLabel
186
+ color={focused ? activeColor : inactiveColor}
187
+ icon={icon}
188
+ label={labelText}
189
+ style={labelStyle}
190
+ />
191
+ ),
192
+ [
193
+ customlabel,
194
+ activeColor,
195
+ labelStyle,
196
+ labelText,
197
+ labelAllowFontScaling,
198
+ route,
199
+ inactiveColor,
200
+ icon,
201
+ ]
202
+ );
203
+
204
+ const tabStyle = StyleSheet.flatten(style);
205
+ const isWidthSet = tabStyle?.width !== undefined;
206
+
207
+ const tabContainerStyle: ViewStyle | null = isWidthSet
208
+ ? null
209
+ : { width: defaultTabWidth };
210
+
211
+ accessibilityLabel =
212
+ typeof accessibilityLabel !== 'undefined' ? accessibilityLabel : labelText;
213
+
214
+ return (
215
+ <PlatformPressable
216
+ android_ripple={android_ripple}
217
+ testID={testID}
218
+ accessible={accessible}
219
+ accessibilityLabel={accessibilityLabel}
220
+ accessibilityRole="tab"
221
+ accessibilityState={{ selected: isFocused }}
222
+ pressColor={pressColor}
223
+ pressOpacity={pressOpacity}
224
+ unstable_pressDelay={0}
225
+ onLayout={onLayout}
226
+ onPress={onPress}
227
+ onLongPress={onLongPress}
228
+ href={href}
229
+ style={[styles.pressable, tabContainerStyle]}
230
+ >
231
+ <View pointerEvents="none" style={[styles.item, tabStyle]}>
232
+ {icon}
233
+ <View>
234
+ <Animated.View style={{ opacity: inactiveOpacity }}>
235
+ {renderLabel(false)}
236
+ </Animated.View>
237
+ <Animated.View
238
+ style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
239
+ >
240
+ {renderLabel(true)}
241
+ </Animated.View>
242
+ </View>
243
+ {customBadge != null ? (
244
+ <View style={styles.badge}>{customBadge({ route })}</View>
245
+ ) : null}
246
+ </View>
247
+ </PlatformPressable>
248
+ );
249
+ };
250
+
251
+ const MemoizedTabBarItemInternal = React.memo(
252
+ TabBarItemInternal
253
+ ) as typeof TabBarItemInternal;
254
+
255
+ export function TabBarItem<T extends Route>(props: Props<T>) {
256
+ const { onPress, onLongPress, onLayout, navigationState, route, ...rest } =
257
+ props;
258
+
259
+ const onPressLatest = useLatestCallback(onPress);
260
+ const onLongPressLatest = useLatestCallback(onLongPress);
261
+ const onLayoutLatest = useLatestCallback(onLayout ? onLayout : () => {});
262
+
263
+ const tabIndex = navigationState.routes.indexOf(route);
264
+
265
+ return (
266
+ <MemoizedTabBarItemInternal
267
+ {...rest}
268
+ onPress={onPressLatest}
269
+ onLayout={onLayoutLatest}
270
+ onLongPress={onLongPressLatest}
271
+ isFocused={navigationState.index === tabIndex}
272
+ route={route}
273
+ index={tabIndex}
274
+ routesLength={navigationState.routes.length}
275
+ />
276
+ );
277
+ }
278
+
279
+ const styles = StyleSheet.create({
280
+ icon: {
281
+ margin: 2,
282
+ },
283
+ item: {
284
+ flex: 1,
285
+ alignItems: 'center',
286
+ justifyContent: 'center',
287
+ padding: 10,
288
+ minHeight: 48,
289
+ },
290
+ badge: {
291
+ position: 'absolute',
292
+ top: 0,
293
+ end: 0,
294
+ },
295
+ pressable: {
296
+ // The label is not pressable on Windows
297
+ // Adding backgroundColor: 'transparent' seems to fix it
298
+ backgroundColor: 'transparent',
299
+ ...Platform.select({
300
+ // Roundness for iPad hover effect
301
+ ios: { borderRadius: 10 },
302
+ default: null,
303
+ }),
304
+ },
305
+ });
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import { Animated, StyleSheet } from 'react-native';
4
+
5
+ interface TabBarItemLabelProps {
6
+ color: string;
7
+ label?: string;
8
+ style: StyleProp<ViewStyle>;
9
+ icon: React.ReactNode;
10
+ }
11
+
12
+ export const TabBarItemLabel = React.memo(
13
+ ({ color, label, style, icon }: TabBarItemLabelProps) => {
14
+ if (!label) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <Animated.Text
20
+ style={[
21
+ styles.label,
22
+ icon ? { marginTop: 0 } : null,
23
+ style,
24
+ { color: color },
25
+ ]}
26
+ >
27
+ {label}
28
+ </Animated.Text>
29
+ );
30
+ }
31
+ );
32
+
33
+ TabBarItemLabel.displayName = 'TabBarItemLabel';
34
+
35
+ const styles = StyleSheet.create({
36
+ label: {
37
+ margin: 4,
38
+ fontSize: 14,
39
+ fontWeight: '500',
40
+ backgroundColor: 'transparent',
41
+ },
42
+ });