@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,339 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Animated,
4
+ type GestureResponderEvent,
5
+ Keyboard,
6
+ PanResponder,
7
+ type PanResponderGestureState,
8
+ StyleSheet,
9
+ View,
10
+ } from 'react-native';
11
+ import useLatestCallback from 'use-latest-callback';
12
+
13
+ import type {
14
+ EventEmitterProps,
15
+ Layout,
16
+ Listener,
17
+ NavigationState,
18
+ PagerProps,
19
+ Route,
20
+ } from './types';
21
+ import { useAnimatedValue } from './useAnimatedValue';
22
+
23
+ type Props<T extends Route> = PagerProps & {
24
+ layout: Layout;
25
+ onIndexChange: (index: number) => void;
26
+ navigationState: NavigationState<T>;
27
+ children: (
28
+ props: EventEmitterProps & {
29
+ // Animated value which represents the state of current index
30
+ // It can include fractional digits as it represents the intermediate value
31
+ position: Animated.AnimatedInterpolation<number>;
32
+ // Function to actually render the content of the pager
33
+ // The parent component takes care of rendering
34
+ render: (children: React.ReactNode) => React.ReactNode;
35
+ // Callback to call when switching the tab
36
+ // The tab switch animation is performed even if the index in state is unchanged
37
+ jumpTo: (key: string) => void;
38
+ }
39
+ ) => React.ReactElement;
40
+ };
41
+
42
+ const DEAD_ZONE = 12;
43
+
44
+ const DefaultTransitionSpec = {
45
+ timing: Animated.spring,
46
+ stiffness: 1000,
47
+ damping: 500,
48
+ mass: 3,
49
+ overshootClamping: true,
50
+ };
51
+
52
+ export function PanResponderAdapter<T extends Route>({
53
+ layout,
54
+ keyboardDismissMode = 'auto',
55
+ swipeEnabled = true,
56
+ navigationState,
57
+ onIndexChange,
58
+ onSwipeStart,
59
+ onSwipeEnd,
60
+ children,
61
+ style,
62
+ animationEnabled = false,
63
+ layoutDirection = 'ltr',
64
+ }: Props<T>) {
65
+ const { routes, index } = navigationState;
66
+
67
+ const panX = useAnimatedValue(0);
68
+
69
+ const listenersRef = React.useRef<Listener[]>([]);
70
+
71
+ const navigationStateRef = React.useRef(navigationState);
72
+ const layoutRef = React.useRef(layout);
73
+ const onIndexChangeRef = React.useRef(onIndexChange);
74
+
75
+ const currentIndexRef = React.useRef(index);
76
+ const pendingIndexRef = React.useRef<number>();
77
+
78
+ const swipeVelocityThreshold = 0.15;
79
+ const swipeDistanceThreshold = layout.width / 1.75;
80
+
81
+ const jumpToIndex = useLatestCallback(
82
+ (index: number, animate = animationEnabled) => {
83
+ const offset = -index * layoutRef.current.width;
84
+
85
+ const { timing, ...transitionConfig } = DefaultTransitionSpec;
86
+
87
+ if (animate) {
88
+ Animated.parallel([
89
+ timing(panX, {
90
+ ...transitionConfig,
91
+ toValue: offset,
92
+ useNativeDriver: false,
93
+ }),
94
+ ]).start(({ finished }) => {
95
+ if (finished) {
96
+ onIndexChangeRef.current(index);
97
+ pendingIndexRef.current = undefined;
98
+ }
99
+ });
100
+ pendingIndexRef.current = index;
101
+ } else {
102
+ panX.setValue(offset);
103
+ onIndexChangeRef.current(index);
104
+ pendingIndexRef.current = undefined;
105
+ }
106
+ }
107
+ );
108
+
109
+ React.useEffect(() => {
110
+ navigationStateRef.current = navigationState;
111
+ layoutRef.current = layout;
112
+ onIndexChangeRef.current = onIndexChange;
113
+ });
114
+
115
+ React.useEffect(() => {
116
+ const offset = -navigationStateRef.current.index * layout.width;
117
+
118
+ panX.setValue(offset);
119
+ }, [layout.width, panX]);
120
+
121
+ React.useEffect(() => {
122
+ if (keyboardDismissMode === 'auto') {
123
+ Keyboard.dismiss();
124
+ }
125
+
126
+ if (layout.width && currentIndexRef.current !== index) {
127
+ currentIndexRef.current = index;
128
+ jumpToIndex(index);
129
+ }
130
+ }, [jumpToIndex, keyboardDismissMode, layout.width, index]);
131
+
132
+ const isMovingHorizontally = (
133
+ _: GestureResponderEvent,
134
+ gestureState: PanResponderGestureState
135
+ ) => {
136
+ return (
137
+ Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 2) &&
138
+ Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 2)
139
+ );
140
+ };
141
+
142
+ const canMoveScreen = (
143
+ event: GestureResponderEvent,
144
+ gestureState: PanResponderGestureState
145
+ ) => {
146
+ if (swipeEnabled === false) {
147
+ return false;
148
+ }
149
+
150
+ const diffX =
151
+ layoutDirection === 'rtl' ? -gestureState.dx : gestureState.dx;
152
+
153
+ return (
154
+ isMovingHorizontally(event, gestureState) &&
155
+ ((diffX >= DEAD_ZONE && currentIndexRef.current > 0) ||
156
+ (diffX <= -DEAD_ZONE && currentIndexRef.current < routes.length - 1))
157
+ );
158
+ };
159
+
160
+ const startGesture = () => {
161
+ onSwipeStart?.();
162
+
163
+ if (keyboardDismissMode === 'on-drag') {
164
+ Keyboard.dismiss();
165
+ }
166
+
167
+ panX.stopAnimation();
168
+ // @ts-expect-error: _value is private, but docs use it as well
169
+ panX.setOffset(panX._value);
170
+ };
171
+
172
+ const respondToGesture = (
173
+ _: GestureResponderEvent,
174
+ gestureState: PanResponderGestureState
175
+ ) => {
176
+ const diffX =
177
+ layoutDirection === 'rtl' ? -gestureState.dx : gestureState.dx;
178
+
179
+ if (
180
+ // swiping left
181
+ (diffX > 0 && index <= 0) ||
182
+ // swiping right
183
+ (diffX < 0 && index >= routes.length - 1)
184
+ ) {
185
+ return;
186
+ }
187
+
188
+ if (layout.width) {
189
+ // @ts-expect-error: _offset is private, but docs use it as well
190
+ const position = (panX._offset + diffX) / -layout.width;
191
+ const next =
192
+ position > index ? Math.ceil(position) : Math.floor(position);
193
+
194
+ if (next !== index) {
195
+ listenersRef.current.forEach((listener) => listener(next));
196
+ }
197
+ }
198
+
199
+ panX.setValue(diffX);
200
+ };
201
+
202
+ const finishGesture = (
203
+ _: GestureResponderEvent,
204
+ gestureState: PanResponderGestureState
205
+ ) => {
206
+ panX.flattenOffset();
207
+
208
+ onSwipeEnd?.();
209
+
210
+ const currentIndex =
211
+ typeof pendingIndexRef.current === 'number'
212
+ ? pendingIndexRef.current
213
+ : currentIndexRef.current;
214
+
215
+ let nextIndex = currentIndex;
216
+
217
+ if (
218
+ Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
219
+ Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
220
+ (Math.abs(gestureState.dx) > swipeDistanceThreshold ||
221
+ Math.abs(gestureState.vx) > swipeVelocityThreshold)
222
+ ) {
223
+ nextIndex = Math.round(
224
+ Math.min(
225
+ Math.max(
226
+ 0,
227
+ layoutDirection === 'rtl'
228
+ ? currentIndex + gestureState.dx / Math.abs(gestureState.dx)
229
+ : currentIndex - gestureState.dx / Math.abs(gestureState.dx)
230
+ ),
231
+ routes.length - 1
232
+ )
233
+ );
234
+
235
+ currentIndexRef.current = nextIndex;
236
+ }
237
+
238
+ if (!isFinite(nextIndex)) {
239
+ nextIndex = currentIndex;
240
+ }
241
+
242
+ jumpToIndex(nextIndex, true);
243
+ };
244
+
245
+ const addEnterListener = useLatestCallback((listener: Listener) => {
246
+ listenersRef.current.push(listener);
247
+
248
+ return () => {
249
+ const index = listenersRef.current.indexOf(listener);
250
+
251
+ if (index > -1) {
252
+ listenersRef.current.splice(index, 1);
253
+ }
254
+ };
255
+ });
256
+
257
+ const jumpTo = useLatestCallback((key: string) => {
258
+ const index = navigationStateRef.current.routes.findIndex(
259
+ (route: { key: string }) => route.key === key
260
+ );
261
+
262
+ jumpToIndex(index);
263
+ onIndexChange(index);
264
+ });
265
+
266
+ const panResponder = PanResponder.create({
267
+ onMoveShouldSetPanResponder: canMoveScreen,
268
+ onMoveShouldSetPanResponderCapture: canMoveScreen,
269
+ onPanResponderGrant: startGesture,
270
+ onPanResponderMove: respondToGesture,
271
+ onPanResponderTerminate: finishGesture,
272
+ onPanResponderRelease: finishGesture,
273
+ onPanResponderTerminationRequest: () => true,
274
+ });
275
+
276
+ const maxTranslate = layout.width * (routes.length - 1);
277
+ const translateX = Animated.multiply(
278
+ panX.interpolate({
279
+ inputRange: [-maxTranslate, 0],
280
+ outputRange: [-maxTranslate, 0],
281
+ extrapolate: 'clamp',
282
+ }),
283
+ layoutDirection === 'rtl' ? -1 : 1
284
+ );
285
+
286
+ const position = React.useMemo(
287
+ () => (layout.width ? Animated.divide(panX, -layout.width) : null),
288
+ [layout.width, panX]
289
+ );
290
+
291
+ return children({
292
+ position: position ?? new Animated.Value(index),
293
+ addEnterListener,
294
+ jumpTo,
295
+ render: (children) => (
296
+ <Animated.View
297
+ style={[
298
+ styles.sheet,
299
+ layout.width
300
+ ? {
301
+ width: routes.length * layout.width,
302
+ transform: [{ translateX }],
303
+ }
304
+ : null,
305
+ style,
306
+ ]}
307
+ {...panResponder.panHandlers}
308
+ >
309
+ {React.Children.map(children, (child, i) => {
310
+ const route = routes[i];
311
+ const focused = i === index;
312
+
313
+ return (
314
+ <View
315
+ key={route.key}
316
+ style={
317
+ layout.width
318
+ ? { width: layout.width }
319
+ : focused
320
+ ? StyleSheet.absoluteFill
321
+ : null
322
+ }
323
+ >
324
+ {focused || layout.width ? child : null}
325
+ </View>
326
+ );
327
+ })}
328
+ </Animated.View>
329
+ ),
330
+ });
331
+ }
332
+
333
+ const styles = StyleSheet.create({
334
+ sheet: {
335
+ flex: 1,
336
+ flexDirection: 'row',
337
+ alignItems: 'stretch',
338
+ },
339
+ });
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import {
3
+ type GestureResponderEvent,
4
+ Platform,
5
+ Pressable,
6
+ type PressableProps,
7
+ } from 'react-native';
8
+
9
+ export type Props = PressableProps & {
10
+ children: React.ReactNode;
11
+ pressColor?: string;
12
+ pressOpacity?: number;
13
+ href?: string;
14
+ };
15
+
16
+ const ANDROID_VERSION_LOLLIPOP = 21;
17
+ const ANDROID_SUPPORTS_RIPPLE =
18
+ Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_LOLLIPOP;
19
+
20
+ /**
21
+ * PlatformPressable provides an abstraction on top of Pressable to handle platform differences.
22
+ */
23
+ export function PlatformPressable({
24
+ disabled,
25
+ android_ripple,
26
+ pressColor = 'rgba(0, 0, 0, .32)',
27
+ pressOpacity,
28
+ style,
29
+ onPress,
30
+ ...rest
31
+ }: Props) {
32
+ const handlePress = (e: GestureResponderEvent) => {
33
+ if (Platform.OS === 'web' && rest.href !== null) {
34
+ // @ts-expect-error: these properties exist on web, but not in React Native
35
+ const hasModifierKey = e.metaKey || e.altKey || e.ctrlKey || e.shiftKey; // ignore clicks with modifier keys
36
+ // @ts-expect-error: these properties exist on web, but not in React Native
37
+ const isLeftClick = e.button === null || e.button === 0; // only handle left clicks
38
+ const isSelfTarget = [undefined, null, '', 'self'].includes(
39
+ // @ts-expect-error: these properties exist on web, but not in React Native
40
+ e.currentTarget?.target
41
+ ); // let browser handle "target=_blank" etc.
42
+
43
+ if (!hasModifierKey && isLeftClick && isSelfTarget) {
44
+ e.preventDefault();
45
+ onPress?.(e);
46
+ }
47
+ } else {
48
+ onPress?.(e);
49
+ }
50
+ };
51
+
52
+ return (
53
+ <Pressable
54
+ android_ripple={
55
+ ANDROID_SUPPORTS_RIPPLE
56
+ ? { color: pressColor, ...android_ripple }
57
+ : undefined
58
+ }
59
+ style={({ pressed }) => [
60
+ {
61
+ cursor:
62
+ Platform.OS === 'web' || Platform.OS === 'ios'
63
+ ? // Pointer cursor on web
64
+ // Hover effect on iPad and visionOS
65
+ 'pointer'
66
+ : 'auto',
67
+ opacity: pressed && !ANDROID_SUPPORTS_RIPPLE ? pressOpacity : 1,
68
+ },
69
+ typeof style === 'function' ? style({ pressed }) : style,
70
+ ]}
71
+ onPress={disabled ? undefined : handlePress}
72
+ {...rest}
73
+ />
74
+ );
75
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+
3
+ import type { SceneRendererProps } from './types';
4
+
5
+ type SceneProps = {
6
+ route: any;
7
+ } & Omit<SceneRendererProps, 'layout'>;
8
+
9
+ const SceneComponent = React.memo(
10
+ <T extends { component: React.ComponentType<any> } & SceneProps>({
11
+ component,
12
+ ...rest
13
+ }: T) => {
14
+ return React.createElement(component, rest);
15
+ }
16
+ );
17
+
18
+ SceneComponent.displayName = 'SceneComponent';
19
+
20
+ export function SceneMap<T>(scenes: { [key: string]: React.ComponentType<T> }) {
21
+ return ({ route, jumpTo, position }: SceneProps) => (
22
+ <SceneComponent
23
+ key={route.key}
24
+ component={scenes[route.key]}
25
+ route={route}
26
+ jumpTo={jumpTo}
27
+ position={position}
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+ import { type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native';
3
+
4
+ import type {
5
+ EventEmitterProps,
6
+ NavigationState,
7
+ Route,
8
+ SceneRendererProps,
9
+ } from './types';
10
+
11
+ type Props<T extends Route> = SceneRendererProps &
12
+ EventEmitterProps & {
13
+ navigationState: NavigationState<T>;
14
+ lazy: boolean;
15
+ lazyPreloadDistance: number;
16
+ index: number;
17
+ children: (props: { loading: boolean }) => React.ReactNode;
18
+ style?: StyleProp<ViewStyle>;
19
+ };
20
+
21
+ export function SceneView<T extends Route>({
22
+ children,
23
+ navigationState,
24
+ lazy,
25
+ layout,
26
+ index,
27
+ lazyPreloadDistance,
28
+ addEnterListener,
29
+ style,
30
+ }: Props<T>) {
31
+ const [isLoading, setIsLoading] = React.useState(
32
+ Math.abs(navigationState.index - index) > lazyPreloadDistance
33
+ );
34
+
35
+ if (
36
+ isLoading &&
37
+ Math.abs(navigationState.index - index) <= lazyPreloadDistance
38
+ ) {
39
+ // Always render the route when it becomes focused
40
+ setIsLoading(false);
41
+ }
42
+
43
+ React.useEffect(() => {
44
+ const handleEnter = (value: number) => {
45
+ // If we're entering the current route, we need to load it
46
+ if (value === index) {
47
+ setIsLoading((prevState) => {
48
+ if (prevState) {
49
+ return false;
50
+ }
51
+ return prevState;
52
+ });
53
+ }
54
+ };
55
+
56
+ let unsubscribe: (() => void) | undefined;
57
+ let timer: ReturnType<typeof setTimeout> | undefined;
58
+
59
+ if (lazy && isLoading) {
60
+ // If lazy mode is enabled, listen to when we enter screens
61
+ unsubscribe = addEnterListener(handleEnter);
62
+ } else if (isLoading) {
63
+ // If lazy mode is not enabled, render the scene with a delay if not loaded already
64
+ // This improves the initial startup time as the scene is no longer blocking
65
+ timer = setTimeout(() => setIsLoading(false), 0);
66
+ }
67
+
68
+ return () => {
69
+ unsubscribe?.();
70
+ clearTimeout(timer);
71
+ };
72
+ }, [addEnterListener, index, isLoading, lazy]);
73
+
74
+ const focused = navigationState.index === index;
75
+
76
+ return (
77
+ <View
78
+ accessibilityElementsHidden={!focused}
79
+ importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
80
+ style={[
81
+ styles.route,
82
+ // If we don't have the layout yet, make the focused screen fill the container
83
+ // This avoids delay before we are able to render pages side by side
84
+ layout.width
85
+ ? { width: layout.width }
86
+ : focused
87
+ ? StyleSheet.absoluteFill
88
+ : null,
89
+ style,
90
+ ]}
91
+ >
92
+ {
93
+ // Only render the route only if it's either focused or layout is available
94
+ // When layout is not available, we must not render unfocused routes
95
+ // so that the focused route can fill the screen
96
+ focused || layout.width ? children({ loading: isLoading }) : null
97
+ }
98
+ </View>
99
+ );
100
+ }
101
+
102
+ const styles = StyleSheet.create({
103
+ route: {
104
+ flex: 1,
105
+ overflow: 'hidden',
106
+ },
107
+ });