@zoverdoser/react-native-tab-view 1.0.1 → 1.3.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 (46) hide show
  1. package/lib/module/PagerViewAdapter.js +1 -1
  2. package/lib/module/PagerViewAdapter.js.map +1 -1
  3. package/lib/module/PanResponderAdapter.js +1 -1
  4. package/lib/module/PanResponderAdapter.js.map +1 -1
  5. package/lib/module/TabBar.js +7 -30
  6. package/lib/module/TabBar.js.map +1 -1
  7. package/lib/module/TabBarIndicator.js +110 -35
  8. package/lib/module/TabBarIndicator.js.map +1 -1
  9. package/lib/module/TabBarItem.js +1 -1
  10. package/lib/module/TabBarItem.js.map +1 -1
  11. package/lib/module/TabBarItemLabel.js.map +1 -1
  12. package/lib/module/useAnimatedValue.js.map +1 -1
  13. package/lib/typescript/src/Pager.android.d.ts +1 -1
  14. package/lib/typescript/src/Pager.android.d.ts.map +1 -1
  15. package/lib/typescript/src/Pager.d.ts +1 -1
  16. package/lib/typescript/src/Pager.d.ts.map +1 -1
  17. package/lib/typescript/src/Pager.ios.d.ts +1 -1
  18. package/lib/typescript/src/Pager.ios.d.ts.map +1 -1
  19. package/lib/typescript/src/PagerViewAdapter.d.ts +1 -1
  20. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
  21. package/lib/typescript/src/PanResponderAdapter.d.ts +1 -1
  22. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
  23. package/lib/typescript/src/SceneMap.d.ts +1 -1
  24. package/lib/typescript/src/SceneMap.d.ts.map +1 -1
  25. package/lib/typescript/src/SceneView.d.ts +1 -1
  26. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  27. package/lib/typescript/src/TabBar.d.ts +3 -3
  28. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  29. package/lib/typescript/src/TabBarIndicator.d.ts +1 -1
  30. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  31. package/lib/typescript/src/TabBarItem.d.ts +1 -1
  32. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  33. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  34. package/lib/typescript/src/TabView.d.ts +1 -1
  35. package/lib/typescript/src/TabView.d.ts.map +1 -1
  36. package/lib/typescript/src/index.d.ts +10 -10
  37. package/lib/typescript/src/index.d.ts.map +1 -1
  38. package/lib/typescript/src/useAnimatedValue.d.ts.map +1 -1
  39. package/package.json +8 -8
  40. package/src/PagerViewAdapter.tsx +1 -1
  41. package/src/PanResponderAdapter.tsx +1 -1
  42. package/src/TabBar.tsx +11 -45
  43. package/src/TabBarIndicator.tsx +182 -41
  44. package/src/TabBarItem.tsx +1 -1
  45. package/src/TabBarItemLabel.tsx +1 -0
  46. package/src/useAnimatedValue.tsx +2 -2
@@ -30,35 +30,94 @@ export type Props<T extends Route> = SceneRendererProps & {
30
30
 
31
31
  const useNativeDriver = Platform.OS !== 'web';
32
32
 
33
+ const calculateSize = (
34
+ value: ViewStyle['width'] | undefined,
35
+ referenceWidth: number
36
+ ): number | undefined => {
37
+ if (typeof value === 'number') {
38
+ return value;
39
+ }
40
+
41
+ if (typeof value === 'string' && value.endsWith('%')) {
42
+ const parsed = parseFloat(value);
43
+
44
+ if (Number.isFinite(parsed)) {
45
+ return referenceWidth * (parsed / 100);
46
+ }
47
+ }
48
+
49
+ return undefined;
50
+ };
51
+
52
+ const getIndicatorWidthWithMargins = (
53
+ width: number,
54
+ style: ViewStyle | undefined,
55
+ direction: LocaleDirection
56
+ ) => {
57
+ const marginHorizontal = style?.marginHorizontal ?? style?.margin;
58
+
59
+ const leftMargin =
60
+ (direction === 'ltr' ? style?.marginStart : style?.marginEnd) ??
61
+ style?.marginLeft ??
62
+ marginHorizontal;
63
+
64
+ const rightMargin =
65
+ (direction === 'rtl' ? style?.marginStart : style?.marginEnd) ??
66
+ style?.marginRight ??
67
+ marginHorizontal;
68
+
69
+ return Math.max(
70
+ 0,
71
+ width -
72
+ (calculateSize(leftMargin, width) ?? 0) -
73
+ (calculateSize(rightMargin, width) ?? 0)
74
+ );
75
+ };
76
+
77
+ const getIndicatorWidth = (
78
+ tabWidth: number,
79
+ width: number | `${number}%`,
80
+ style: ViewStyle | undefined,
81
+ direction: LocaleDirection
82
+ ): number | `${number}%` => {
83
+ const customWidth = calculateSize(style?.width, tabWidth);
84
+
85
+ if (customWidth !== undefined) {
86
+ return customWidth;
87
+ }
88
+
89
+ if (typeof width === 'number') {
90
+ return getIndicatorWidthWithMargins(width, style, direction);
91
+ }
92
+
93
+ return width;
94
+ };
95
+
33
96
  const getTranslateX = (
34
97
  position: Animated.AnimatedInterpolation<number>,
35
98
  routes: Route[],
36
99
  getTabWidth: GetTabWidth,
37
100
  direction: LocaleDirection,
38
101
  gap?: number,
39
- width?: number | string
102
+ getWidth?: (index: number) => number | undefined
40
103
  ) => {
41
104
  const inputRange = routes.map((_, i) => i);
105
+ const outputRange = routes.map((_, i) => {
106
+ let sumTabWidth = 0;
42
107
 
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];
108
+ for (let j = 0; j < i; j++) {
109
+ sumTabWidth += getTabWidth(j);
110
+ }
47
111
 
48
- let sumTabWidth = 0;
49
- for (let j = 0; j < i; j++) {
50
- sumTabWidth += getTabWidth(j);
51
- }
112
+ const indicatorWidth = getWidth?.(i);
113
+ const tabOffset = sumTabWidth + (gap ? gap * i : 0);
52
114
 
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)];
115
+ if (indicatorWidth === undefined) {
116
+ return tabOffset;
60
117
  }
61
- }, []);
118
+
119
+ return tabOffset + getTabWidth(i) / 2 - indicatorWidth / 2;
120
+ });
62
121
 
63
122
  const translateX = position.interpolate({
64
123
  inputRange,
@@ -83,31 +142,60 @@ export function TabBarIndicator<T extends Route>({
83
142
  const isIndicatorShown = React.useRef(false);
84
143
  const isWidthDynamic = width === 'auto';
85
144
 
145
+ const flattenedStyle = StyleSheet.flatten(style);
146
+
147
+ const hasCustomIndicatorWidth =
148
+ typeof flattenedStyle?.width === 'number' ||
149
+ (typeof flattenedStyle?.width === 'string' &&
150
+ flattenedStyle?.width.endsWith('%'));
151
+
152
+ const constantIndicatorWidth =
153
+ typeof flattenedStyle?.width === 'number'
154
+ ? flattenedStyle.width
155
+ : undefined;
156
+
157
+ const isCentered =
158
+ hasCustomIndicatorWidth &&
159
+ (flattenedStyle?.margin === 'auto' ||
160
+ flattenedStyle?.marginHorizontal === 'auto');
161
+
162
+ // If indicator has a custom width, we need to adjust calculations to account for it
163
+ // It should be centered relative to the tab if the margin is set to auto
164
+ const getCenteredIndicatorWidth = (tabWidth: number) => {
165
+ if (isCentered) {
166
+ return calculateSize(flattenedStyle?.width, tabWidth);
167
+ }
168
+
169
+ if (typeof width === 'number') {
170
+ return width;
171
+ }
172
+
173
+ return undefined;
174
+ };
175
+
86
176
  const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
87
177
 
178
+ // We should fade-in the indicator when we have widths for all the tab items
88
179
  const indicatorVisible = isWidthDynamic
89
180
  ? layout.width &&
90
181
  navigationState.routes
91
- .slice(0, navigationState.index)
182
+ .slice(0, navigationState.index + 1)
92
183
  .every((_, r) => getTabWidth(r))
93
184
  : true;
94
185
 
95
186
  React.useEffect(() => {
96
187
  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
-
188
+ if (!isIndicatorShown.current && isWidthDynamic && indicatorVisible) {
105
189
  Animated.timing(opacity, {
106
190
  toValue: 1,
107
191
  duration: 150,
108
192
  easing: Easing.in(Easing.linear),
109
193
  useNativeDriver,
110
- }).start();
194
+ }).start(({ finished }) => {
195
+ if (finished) {
196
+ isIndicatorShown.current = true;
197
+ }
198
+ });
111
199
  }
112
200
  };
113
201
 
@@ -120,18 +208,25 @@ export function TabBarIndicator<T extends Route>({
120
208
 
121
209
  const transform = [];
122
210
 
123
- if (layout.width) {
124
- const translateX =
125
- routes.length > 1
126
- ? getTranslateX(position, routes, getTabWidth, direction, gap, width)
127
- : 0;
211
+ const translateX =
212
+ layout.width && routes.length > 1
213
+ ? getTranslateX(position, routes, getTabWidth, direction, gap, (index) =>
214
+ getCenteredIndicatorWidth(getTabWidth(index))
215
+ )
216
+ : 0;
128
217
 
129
- transform.push({ translateX });
130
- }
218
+ transform.push({ translateX });
131
219
 
132
- if (width === 'auto') {
220
+ if (width === 'auto' && constantIndicatorWidth == null) {
133
221
  const inputRange = routes.map((_, i) => i);
134
- const outputRange = inputRange.map(getTabWidth);
222
+ const outputRange = inputRange.map((i) => {
223
+ const tabW = getTabWidth(i);
224
+
225
+ return (
226
+ calculateSize(flattenedStyle?.width, tabW) ??
227
+ getIndicatorWidthWithMargins(tabW, flattenedStyle, direction)
228
+ );
229
+ });
135
230
 
136
231
  transform.push(
137
232
  {
@@ -150,27 +245,73 @@ export function TabBarIndicator<T extends Route>({
150
245
 
151
246
  const styleList: StyleProp<ViewStyle> = [];
152
247
 
153
- // scaleX doesn't work properly on chrome and opera for linux and android
154
- if (Platform.OS === 'web' && width === 'auto') {
248
+ // transform doesn't work properly on chrome and opera for linux and android
249
+ // so we need to use width and left/right instead of scaleX and translateX
250
+ // https://github.com/react-navigation/react-navigation/pull/11440
251
+ if (
252
+ Platform.OS === 'web' &&
253
+ width === 'auto' &&
254
+ constantIndicatorWidth == null
255
+ ) {
256
+ const start = flattenedStyle?.start;
257
+ const translate =
258
+ direction === 'rtl' ? Animated.multiply(translateX, -1) : translateX;
259
+ const offset =
260
+ typeof start === 'number' ? Animated.add(translate, start) : translate;
261
+
155
262
  styleList.push(
156
263
  { width: transform[1].scaleX },
157
- { left: transform[0].translateX }
264
+ direction === 'rtl' ? { right: offset } : { left: offset }
158
265
  );
159
266
  } else {
160
267
  styleList.push(
161
- { width: width === 'auto' ? 1 : width },
268
+ {
269
+ width:
270
+ width === 'auto'
271
+ ? // if the indicator has a constant width, use it as is
272
+ // we don't need to scale it to match tab width
273
+ (constantIndicatorWidth ?? 1)
274
+ : getIndicatorWidth(
275
+ getTabWidth(navigationState.index),
276
+ width,
277
+ flattenedStyle,
278
+ direction
279
+ ),
280
+ },
162
281
  { start: `${(100 / routes.length) * navigationState.index}%` },
163
282
  { transform }
164
283
  );
165
284
  }
166
285
 
286
+ let finalStyle;
287
+
288
+ if (hasCustomIndicatorWidth && style != null) {
289
+ const rest = { ...flattenedStyle };
290
+
291
+ delete rest.width;
292
+
293
+ if (isCentered) {
294
+ if (rest.margin === 'auto') {
295
+ delete rest.margin;
296
+ }
297
+
298
+ if (rest.marginHorizontal === 'auto') {
299
+ delete rest.marginHorizontal;
300
+ }
301
+ }
302
+
303
+ finalStyle = rest;
304
+ } else {
305
+ finalStyle = style;
306
+ }
307
+
167
308
  return (
168
309
  <Animated.View
169
310
  style={[
170
311
  styles.indicator,
171
312
  styleList,
172
313
  width === 'auto' ? { opacity: opacity } : null,
173
- style,
314
+ finalStyle,
174
315
  ]}
175
316
  >
176
317
  {children}
@@ -169,7 +169,7 @@ const TabBarItemInternal = <T extends Route>({
169
169
  <Animated.View style={{ opacity }}>{iconEle}</Animated.View>
170
170
  </View>
171
171
  );
172
- }, [customIcon, route, isFocused, ReAnimatedStyleRef, opacity]);
172
+ }, [route, customIcon, isFocused, ReAnimatedStyleRef, opacity]);
173
173
 
174
174
  const renderLabel = React.useCallback(
175
175
  () =>
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import type { StyleProp, TextStyle } from 'react-native';
3
3
  import { StyleSheet } from 'react-native';
4
4
  import Animated, { type AnimatedStyle } from 'react-native-reanimated';
5
+
5
6
  interface TabBarItemLabelProps {
6
7
  label?: string;
7
8
  style: StyleProp<TextStyle>;
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import { Animated } from 'react-native';
3
3
 
4
- export function useAnimatedValue(initialValue: number) {
4
+ export function useAnimatedValue(initialValue: number): Animated.Value {
5
5
  const lazyRef = React.useRef<Animated.Value>(undefined);
6
6
 
7
7
  if (lazyRef.current === undefined) {
8
8
  lazyRef.current = new Animated.Value(initialValue);
9
9
  }
10
10
 
11
- return lazyRef.current as Animated.Value;
11
+ return lazyRef.current;
12
12
  }