react-native-tab-view 4.2.2 → 5.0.0-alpha.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 (70) hide show
  1. package/lib/module/Pager.android.js +1 -1
  2. package/lib/module/Pager.android.js.map +1 -1
  3. package/lib/module/Pager.ios.js +1 -1
  4. package/lib/module/Pager.ios.js.map +1 -1
  5. package/lib/module/PagerViewAdapter.js +3 -127
  6. package/lib/module/PagerViewAdapter.js.map +1 -1
  7. package/lib/module/PagerViewAdapter.native.js +130 -0
  8. package/lib/module/PagerViewAdapter.native.js.map +1 -0
  9. package/lib/module/PanResponderAdapter.js +43 -31
  10. package/lib/module/PanResponderAdapter.js.map +1 -1
  11. package/lib/module/PlatformPressable.js.map +1 -1
  12. package/lib/module/SceneView.js +22 -31
  13. package/lib/module/SceneView.js.map +1 -1
  14. package/lib/module/ScrollViewAdapter.js +210 -0
  15. package/lib/module/ScrollViewAdapter.js.map +1 -0
  16. package/lib/module/TabBar.js +98 -61
  17. package/lib/module/TabBar.js.map +1 -1
  18. package/lib/module/TabBarIndicator.js +6 -9
  19. package/lib/module/TabBarIndicator.js.map +1 -1
  20. package/lib/module/TabBarItem.js +4 -4
  21. package/lib/module/TabBarItem.js.map +1 -1
  22. package/lib/module/TabView.js +65 -84
  23. package/lib/module/TabView.js.map +1 -1
  24. package/lib/module/index.js +3 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/useMeasureLayout.js +36 -0
  27. package/lib/module/useMeasureLayout.js.map +1 -0
  28. package/lib/typescript/src/PagerViewAdapter.d.ts +3 -17
  29. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
  30. package/lib/typescript/src/PagerViewAdapter.native.d.ts +6 -0
  31. package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -0
  32. package/lib/typescript/src/PanResponderAdapter.d.ts +3 -17
  33. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
  34. package/lib/typescript/src/PlatformPressable.d.ts +2 -2
  35. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  36. package/lib/typescript/src/SceneView.d.ts +1 -1
  37. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  38. package/lib/typescript/src/ScrollViewAdapter.d.ts +12 -0
  39. package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -0
  40. package/lib/typescript/src/TabBar.d.ts +7 -7
  41. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  42. package/lib/typescript/src/TabBarIndicator.d.ts +1 -1
  43. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  44. package/lib/typescript/src/TabBarItem.d.ts +4 -4
  45. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  46. package/lib/typescript/src/TabBarItemLabel.d.ts +2 -2
  47. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  48. package/lib/typescript/src/TabView.d.ts +99 -5
  49. package/lib/typescript/src/TabView.d.ts.map +1 -1
  50. package/lib/typescript/src/index.d.ts +4 -1
  51. package/lib/typescript/src/index.d.ts.map +1 -1
  52. package/lib/typescript/src/types.d.ts +64 -8
  53. package/lib/typescript/src/types.d.ts.map +1 -1
  54. package/lib/typescript/src/useMeasureLayout.d.ts +5 -0
  55. package/lib/typescript/src/useMeasureLayout.d.ts.map +1 -0
  56. package/package.json +8 -8
  57. package/src/PagerViewAdapter.native.tsx +174 -0
  58. package/src/PagerViewAdapter.tsx +8 -181
  59. package/src/PanResponderAdapter.tsx +64 -77
  60. package/src/PlatformPressable.tsx +2 -1
  61. package/src/SceneView.tsx +23 -45
  62. package/src/ScrollViewAdapter.tsx +268 -0
  63. package/src/TabBar.tsx +134 -133
  64. package/src/TabBarIndicator.tsx +7 -11
  65. package/src/TabBarItem.tsx +8 -6
  66. package/src/TabBarItemLabel.tsx +2 -2
  67. package/src/TabView.tsx +173 -100
  68. package/src/index.tsx +10 -0
  69. package/src/types.tsx +75 -17
  70. package/src/useMeasureLayout.tsx +34 -0
@@ -0,0 +1,268 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Animated,
4
+ Keyboard,
5
+ Platform,
6
+ ScrollView,
7
+ StyleSheet,
8
+ View,
9
+ type ViewProps,
10
+ } from 'react-native';
11
+ import useLatestCallback from 'use-latest-callback';
12
+
13
+ import type { AdapterProps, Listener } from './types';
14
+ import { useMeasureLayout } from './useMeasureLayout';
15
+
16
+ export type ScrollViewAdapterProps = AdapterProps &
17
+ Omit<ViewProps, 'children'> & {
18
+ decelerationRate?: 'fast' | 'normal';
19
+ keyboardShouldPersistTaps?: 'always' | 'never' | 'handled';
20
+ bounces?: boolean;
21
+ overScrollMode?: 'always' | 'never' | 'auto';
22
+ };
23
+
24
+ type ScrollEvent = Parameters<
25
+ NonNullable<React.ComponentProps<typeof Animated.ScrollView>['onScroll']>
26
+ >[0];
27
+
28
+ export function ScrollViewAdapter({
29
+ keyboardDismissMode,
30
+ swipeEnabled = true,
31
+ navigationState,
32
+ onIndexChange,
33
+ onTabSelect,
34
+ onSwipeStart,
35
+ onSwipeEnd,
36
+ children,
37
+ style,
38
+ animationEnabled = true,
39
+ layoutDirection: _, // Not supported in ScrollViewAdapter
40
+ decelerationRate = 'fast',
41
+ bounces = false,
42
+ overScrollMode = 'never',
43
+ keyboardShouldPersistTaps = 'always',
44
+ ...rest
45
+ }: ScrollViewAdapterProps) {
46
+ const { index, routes } = navigationState;
47
+
48
+ const listeners = React.useRef<Set<Listener>>(new Set()).current;
49
+
50
+ const scrollViewRef = React.useRef<ScrollView>(null);
51
+ const containerRef = React.useRef<View>(null);
52
+
53
+ const isInitialRef = React.useRef(true);
54
+
55
+ const [layout, onLayout] = useMeasureLayout(containerRef, ({ width }) => {
56
+ if (isInitialRef.current) {
57
+ const x = index * width;
58
+
59
+ setContentOffset({ x, y: 0 });
60
+
61
+ offsetX.setValue(x);
62
+ } else if (indexRef.current !== index) {
63
+ scrollToIndex(index);
64
+ }
65
+
66
+ isInitialRef.current = false;
67
+ });
68
+
69
+ const [contentOffset, setContentOffset] = React.useState(() => ({
70
+ x: index * layout.width,
71
+ y: 0,
72
+ }));
73
+
74
+ React.useEffect(() => {
75
+ // FIXME: contentOffset is not supported on Android
76
+ // So we manually scroll after state update
77
+ if (Platform.OS === 'android') {
78
+ requestAnimationFrame(() => {
79
+ scrollViewRef.current?.scrollTo({
80
+ x: contentOffset.x,
81
+ animated: false,
82
+ });
83
+ });
84
+ }
85
+ }, [animationEnabled, contentOffset.x]);
86
+
87
+ const [offsetX] = React.useState(() => new Animated.Value(contentOffset.x));
88
+
89
+ const scrollToIndex = useLatestCallback((index: number) => {
90
+ scrollViewRef.current?.scrollTo({
91
+ x: index * layout.width,
92
+ animated: animationEnabled,
93
+ });
94
+ });
95
+
96
+ const jumpTo = useLatestCallback((key: string) => {
97
+ const i = routes.findIndex((route) => route.key === key);
98
+
99
+ scrollToIndex(i);
100
+
101
+ if (keyboardDismissMode === 'auto') {
102
+ Keyboard.dismiss();
103
+ }
104
+ });
105
+
106
+ const handleSwipeStart = React.useCallback(() => {
107
+ onSwipeStart?.();
108
+ }, [onSwipeStart]);
109
+
110
+ const handleSwipeEnd = React.useCallback(() => {
111
+ onSwipeEnd?.();
112
+ }, [onSwipeEnd]);
113
+
114
+ const subscribe = useLatestCallback((listener: Listener) => {
115
+ listeners.add(listener);
116
+
117
+ return () => {
118
+ listeners.delete(listener);
119
+ };
120
+ });
121
+
122
+ const position = React.useMemo(
123
+ () => (layout.width ? Animated.divide(offsetX, layout.width) : null),
124
+ [layout.width, offsetX]
125
+ );
126
+
127
+ const indexRef = React.useRef(index);
128
+ const timerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
129
+
130
+ const onScrollEnd = (x: number) => {
131
+ const value = clamp(x / layout.width, 0, routes.length - 1);
132
+
133
+ if (value % 1 === 0) {
134
+ indexRef.current = value;
135
+
136
+ if (value !== index) {
137
+ onIndexChange(value);
138
+ }
139
+
140
+ onTabSelect?.({ index: value });
141
+ }
142
+ };
143
+
144
+ const onScroll = Animated.event(
145
+ [
146
+ {
147
+ nativeEvent: {
148
+ contentOffset: { x: offsetX },
149
+ },
150
+ },
151
+ ],
152
+ {
153
+ useNativeDriver: true,
154
+ listener: (event: ScrollEvent) => {
155
+ const { x } = event.nativeEvent.contentOffset;
156
+
157
+ const value = clamp(x / layout.width, 0, routes.length - 1);
158
+
159
+ // The offset will overlap the current and the adjacent page
160
+ // So we need to get the index of the adjacent page
161
+ const next = [Math.ceil(value), Math.floor(value)].find(
162
+ (i) => i !== index
163
+ );
164
+
165
+ if (next != null) {
166
+ listeners.forEach((listener) =>
167
+ listener({
168
+ type: 'enter',
169
+ index: next,
170
+ })
171
+ );
172
+ }
173
+
174
+ // FIXME: onMomentumScrollEnd is not supported on Web
175
+ // So we workaround by using a timer
176
+ if (Platform.OS === 'web') {
177
+ clearTimeout(timerRef.current);
178
+
179
+ timerRef.current = setTimeout(() => {
180
+ onScrollEnd(x);
181
+ }, 100);
182
+ }
183
+ },
184
+ }
185
+ );
186
+
187
+ const onMomentumScrollEnd = (event: ScrollEvent) => {
188
+ onScrollEnd(event.nativeEvent.contentOffset.x);
189
+ };
190
+
191
+ return children({
192
+ position: position ?? new Animated.Value(index),
193
+ subscribe,
194
+ jumpTo,
195
+ render: (children) => (
196
+ <View ref={containerRef} onLayout={onLayout} style={styles.container}>
197
+ <Animated.ScrollView
198
+ {...rest}
199
+ ref={scrollViewRef}
200
+ horizontal
201
+ pagingEnabled
202
+ directionalLockEnabled
203
+ decelerationRate={decelerationRate}
204
+ bounces={bounces}
205
+ overScrollMode={overScrollMode}
206
+ keyboardShouldPersistTaps={keyboardShouldPersistTaps}
207
+ keyboardDismissMode={
208
+ keyboardDismissMode === 'auto' ? 'on-drag' : keyboardDismissMode
209
+ }
210
+ scrollEnabled={swipeEnabled && Boolean(layout.width)}
211
+ scrollToOverflowEnabled={false}
212
+ scrollsToTop={false}
213
+ automaticallyAdjustContentInsets={false}
214
+ showsHorizontalScrollIndicator={false}
215
+ scrollEventThrottle={1}
216
+ onScroll={onScroll}
217
+ onScrollBeginDrag={handleSwipeStart}
218
+ onScrollEndDrag={handleSwipeEnd}
219
+ onMomentumScrollEnd={onMomentumScrollEnd}
220
+ contentOffset={contentOffset}
221
+ contentContainerStyle={{
222
+ width: layout.width ? `${routes.length * 100}%` : '100%',
223
+ }}
224
+ style={[styles.scroll, style]}
225
+ >
226
+ {children
227
+ .map((child, i) => {
228
+ const route = routes[i];
229
+ const focused = i === index;
230
+
231
+ if (!layout.width && !focused) {
232
+ return null;
233
+ }
234
+
235
+ return (
236
+ <View
237
+ key={route.key}
238
+ style={
239
+ layout.width
240
+ ? // FIXME: percentage width doesn't work on web
241
+ // So we use a fixed width instead
242
+ { width: layout.width }
243
+ : { width: '100%' }
244
+ }
245
+ >
246
+ {child}
247
+ </View>
248
+ );
249
+ })
250
+ .filter((child) => child !== null)}
251
+ </Animated.ScrollView>
252
+ </View>
253
+ ),
254
+ });
255
+ }
256
+
257
+ const clamp = (value: number, min: number, max: number) =>
258
+ Math.max(min, Math.min(max, value));
259
+
260
+ const styles = StyleSheet.create({
261
+ container: {
262
+ flex: 1,
263
+ overflow: 'hidden',
264
+ },
265
+ scroll: {
266
+ flex: 1,
267
+ },
268
+ });