react-native-tab-view 5.0.0-alpha.2 → 5.0.0-alpha.4

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.
@@ -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,
@@ -82,30 +141,59 @@ export function TabBarIndicator<T extends Route>({
82
141
  const isIndicatorShown = React.useRef(false);
83
142
  const isWidthDynamic = width === 'auto';
84
143
 
144
+ const flattenedStyle = StyleSheet.flatten(style);
145
+
146
+ const hasCustomIndicatorWidth =
147
+ typeof flattenedStyle?.width === 'number' ||
148
+ (typeof flattenedStyle?.width === 'string' &&
149
+ flattenedStyle?.width.endsWith('%'));
150
+
151
+ const constantIndicatorWidth =
152
+ typeof flattenedStyle?.width === 'number'
153
+ ? flattenedStyle.width
154
+ : undefined;
155
+
156
+ const isCentered =
157
+ hasCustomIndicatorWidth &&
158
+ (flattenedStyle?.margin === 'auto' ||
159
+ flattenedStyle?.marginHorizontal === 'auto');
160
+
161
+ // If indicator has a custom width, we need to adjust calculations to account for it
162
+ // It should be centered relative to the tab if the margin is set to auto
163
+ const getCenteredIndicatorWidth = (tabWidth: number) => {
164
+ if (isCentered) {
165
+ return calculateSize(flattenedStyle?.width, tabWidth);
166
+ }
167
+
168
+ if (typeof width === 'number') {
169
+ return width;
170
+ }
171
+
172
+ return undefined;
173
+ };
174
+
85
175
  const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
86
176
 
177
+ // We should fade-in the indicator when we have widths for all the tab items
87
178
  const indicatorVisible = isWidthDynamic
88
179
  ? navigationState.routes
89
- .slice(0, navigationState.index)
180
+ .slice(0, navigationState.index + 1)
90
181
  .every((_, r) => getTabWidth(r))
91
182
  : true;
92
183
 
93
184
  React.useEffect(() => {
94
185
  const fadeInIndicator = () => {
95
- if (
96
- !isIndicatorShown.current &&
97
- isWidthDynamic &&
98
- // We should fade-in the indicator when we have widths for all the tab items
99
- indicatorVisible
100
- ) {
101
- isIndicatorShown.current = true;
102
-
186
+ if (!isIndicatorShown.current && isWidthDynamic && indicatorVisible) {
103
187
  Animated.timing(opacity, {
104
188
  toValue: 1,
105
189
  duration: 150,
106
190
  easing: Easing.in(Easing.linear),
107
191
  useNativeDriver,
108
- }).start();
192
+ }).start(({ finished }) => {
193
+ if (finished) {
194
+ isIndicatorShown.current = true;
195
+ }
196
+ });
109
197
  }
110
198
  };
111
199
 
@@ -120,14 +208,23 @@ export function TabBarIndicator<T extends Route>({
120
208
 
121
209
  const translateX =
122
210
  routes.length > 1
123
- ? getTranslateX(position, routes, getTabWidth, direction, gap, width)
211
+ ? getTranslateX(position, routes, getTabWidth, direction, gap, (index) =>
212
+ getCenteredIndicatorWidth(getTabWidth(index))
213
+ )
124
214
  : 0;
125
215
 
126
216
  transform.push({ translateX });
127
217
 
128
- if (width === 'auto') {
218
+ if (width === 'auto' && constantIndicatorWidth == null) {
129
219
  const inputRange = routes.map((_, i) => i);
130
- const outputRange = inputRange.map(getTabWidth);
220
+ const outputRange = inputRange.map((i) => {
221
+ const tabW = getTabWidth(i);
222
+
223
+ return (
224
+ calculateSize(flattenedStyle?.width, tabW) ??
225
+ getIndicatorWidthWithMargins(tabW, flattenedStyle, direction)
226
+ );
227
+ });
131
228
 
132
229
  transform.push(
133
230
  {
@@ -146,27 +243,73 @@ export function TabBarIndicator<T extends Route>({
146
243
 
147
244
  const styleList: StyleProp<ViewStyle> = [];
148
245
 
149
- // scaleX doesn't work properly on chrome and opera for linux and android
150
- if (Platform.OS === 'web' && width === 'auto') {
246
+ // transform doesn't work properly on chrome and opera for linux and android
247
+ // so we need to use width and left/right instead of scaleX and translateX
248
+ // https://github.com/react-navigation/react-navigation/pull/11440
249
+ if (
250
+ Platform.OS === 'web' &&
251
+ width === 'auto' &&
252
+ constantIndicatorWidth == null
253
+ ) {
254
+ const start = flattenedStyle?.start;
255
+ const translate =
256
+ direction === 'rtl' ? Animated.multiply(translateX, -1) : translateX;
257
+ const offset =
258
+ typeof start === 'number' ? Animated.add(translate, start) : translate;
259
+
151
260
  styleList.push(
152
261
  { width: transform[1].scaleX },
153
- { left: transform[0].translateX }
262
+ direction === 'rtl' ? { right: offset } : { left: offset }
154
263
  );
155
264
  } else {
156
265
  styleList.push(
157
- { width: width === 'auto' ? 1 : width },
266
+ {
267
+ width:
268
+ width === 'auto'
269
+ ? // if the indicator has a constant width, use it as is
270
+ // we don't need to scale it to match tab width
271
+ (constantIndicatorWidth ?? 1)
272
+ : getIndicatorWidth(
273
+ getTabWidth(navigationState.index),
274
+ width,
275
+ flattenedStyle,
276
+ direction
277
+ ),
278
+ },
158
279
  { start: `${(100 / routes.length) * navigationState.index}%` },
159
280
  { transform }
160
281
  );
161
282
  }
162
283
 
284
+ let finalStyle;
285
+
286
+ if (hasCustomIndicatorWidth && style != null) {
287
+ const rest = { ...flattenedStyle };
288
+
289
+ delete rest.width;
290
+
291
+ if (isCentered) {
292
+ if (rest.margin === 'auto') {
293
+ delete rest.margin;
294
+ }
295
+
296
+ if (rest.marginHorizontal === 'auto') {
297
+ delete rest.marginHorizontal;
298
+ }
299
+ }
300
+
301
+ finalStyle = rest;
302
+ } else {
303
+ finalStyle = style;
304
+ }
305
+
163
306
  return (
164
307
  <Animated.View
165
308
  style={[
166
309
  styles.indicator,
167
310
  styleList,
168
311
  width === 'auto' ? { opacity: opacity } : null,
169
- style,
312
+ finalStyle,
170
313
  ]}
171
314
  >
172
315
  {children}
@@ -20,16 +20,16 @@ export type Props<T extends Route> = TabDescriptor<T> & {
20
20
  position: Animated.AnimatedInterpolation<number>;
21
21
  route: T;
22
22
  navigationState: NavigationState<T>;
23
- activeColor?: ColorValue;
24
- inactiveColor?: ColorValue;
25
- pressColor?: ColorValue;
26
- pressOpacity?: number;
27
- onLayout?: (event: LayoutChangeEvent) => void;
23
+ activeColor?: ColorValue | undefined;
24
+ inactiveColor?: ColorValue | undefined;
25
+ pressColor?: ColorValue | undefined;
26
+ pressOpacity?: number | undefined;
27
+ onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
28
28
  onPress: () => void;
29
29
  onLongPress: () => void;
30
- defaultTabWidth?: number;
30
+ defaultTabWidth?: number | undefined;
31
31
  style: StyleProp<ViewStyle>;
32
- android_ripple?: PressableAndroidRippleConfig;
32
+ android_ripple?: PressableAndroidRippleConfig | undefined;
33
33
  };
34
34
 
35
35
  const DEFAULT_ACTIVE_COLOR = 'rgba(0, 0, 0, 1)';
@@ -4,7 +4,7 @@ import { Animated, StyleSheet } from 'react-native';
4
4
 
5
5
  interface TabBarItemLabelProps {
6
6
  color: ColorValue;
7
- label?: string;
7
+ label?: string | undefined;
8
8
  style: StyleProp<ViewStyle>;
9
9
  icon: React.ReactNode;
10
10
  }
package/src/TabView.tsx CHANGED
@@ -39,7 +39,7 @@ export type Props<T extends Route> = AdapterCommonProps & {
39
39
  *
40
40
  * Unlike `onIndexChange`, this is called regardless of whether the index changed or not.
41
41
  */
42
- onTabSelect?: (props: { index: number }) => void;
42
+ onTabSelect?: ((props: { index: number }) => void) | undefined;
43
43
  /**
44
44
  * State for the tab view containing the current index and routes.
45
45
  *
@@ -59,17 +59,21 @@ export type Props<T extends Route> = AdapterCommonProps & {
59
59
  * Callback which returns a custom placeholder element.
60
60
  * The placeholder is shown when a scene is not yet loaded when `lazy` is enabled.
61
61
  */
62
- renderLazyPlaceholder?: (props: { route: T }) => React.ReactNode;
62
+ renderLazyPlaceholder?:
63
+ | ((props: { route: T }) => React.ReactNode)
64
+ | undefined;
63
65
  /**
64
66
  * Callback which returns a custom tab bar element to display.
65
67
  */
66
- renderTabBar?: (
67
- props: SceneRendererProps &
68
- EventEmitterProps & {
69
- navigationState: NavigationState<T>;
70
- options: Record<string, TabDescriptor<T>> | undefined;
71
- }
72
- ) => React.ReactNode;
68
+ renderTabBar?:
69
+ | ((
70
+ props: SceneRendererProps &
71
+ EventEmitterProps & {
72
+ navigationState: NavigationState<T>;
73
+ options: Record<string, TabDescriptor<T>> | undefined;
74
+ }
75
+ ) => React.ReactNode)
76
+ | undefined;
73
77
  /**
74
78
  * Callback which returns a custom adapter to use for the tab view.
75
79
  * Adapters are responsible for handling gestures and animations between tabs.
@@ -81,12 +85,12 @@ export type Props<T extends Route> = AdapterCommonProps & {
81
85
  *
82
86
  * Defaults to `PagerViewAdapter` on Android and iOS, and `PanResponderAdapter` on other platforms.
83
87
  */
84
- renderAdapter?: (props: AdapterProps) => React.ReactElement;
88
+ renderAdapter?: ((props: AdapterProps) => React.ReactElement) | undefined;
85
89
  /**
86
90
  * Position of the tab bar in the tab view.
87
91
  * Defaults to `'top'`.
88
92
  */
89
- tabBarPosition?: 'top' | 'bottom';
93
+ tabBarPosition?: 'top' | 'bottom' | undefined;
90
94
  /**
91
95
  * Whether to lazily render the scenes.
92
96
  * When enabled, scenes are rendered only when they come into view.
@@ -94,27 +98,27 @@ export type Props<T extends Route> = AdapterCommonProps & {
94
98
  * Can be a boolean or a function that receives the route and returns a boolean.
95
99
  * Defaults to `false`.
96
100
  */
97
- lazy?: ((props: { route: T }) => boolean) | boolean;
101
+ lazy?: ((props: { route: T }) => boolean) | boolean | undefined;
98
102
  /**
99
103
  * How many screens to preload when `lazy` is enabled.
100
104
  *
101
105
  * Defaults to `0`.
102
106
  */
103
- lazyPreloadDistance?: number;
107
+ lazyPreloadDistance?: number | undefined;
104
108
  /**
105
109
  * The layout direction of the tab view.
106
110
  *
107
111
  * Defaults to the app's locale direction (RTL or LTR).
108
112
  */
109
- direction?: LocaleDirection;
113
+ direction?: LocaleDirection | undefined;
110
114
  /**
111
115
  * Style to apply to the pager container.
112
116
  */
113
- pagerStyle?: StyleProp<ViewStyle>;
117
+ pagerStyle?: StyleProp<ViewStyle> | undefined;
114
118
  /**
115
119
  * Style to apply to the tab view container.
116
120
  */
117
- style?: StyleProp<ViewStyle>;
121
+ style?: StyleProp<ViewStyle> | undefined;
118
122
  /**
119
123
  * Callback which returns a React element to render for each route.
120
124
  */
@@ -131,13 +135,13 @@ export type Props<T extends Route> = AdapterCommonProps & {
131
135
  *
132
136
  * These options are merged with `commonOptions`.
133
137
  */
134
- options?: Record<string, TabDescriptor<T>>;
138
+ options?: Record<string, TabDescriptor<T>> | undefined;
135
139
  /**
136
140
  * Options that apply to all tabs.
137
141
  *
138
142
  * Individual tab options from `options` will override these.
139
143
  */
140
- commonOptions?: TabDescriptor<T>;
144
+ commonOptions?: TabDescriptor<T> | undefined;
141
145
  };
142
146
 
143
147
  const renderLazyPlaceholderDefault = () => null;
package/src/types.tsx CHANGED
@@ -8,40 +8,44 @@ import type {
8
8
  } from 'react-native';
9
9
 
10
10
  export type TabDescriptor<T extends Route> = {
11
- accessibilityLabel?: string;
12
- accessible?: boolean;
13
- testID?: string;
14
- labelText?: string;
15
- labelAllowFontScaling?: boolean;
16
- href?: string;
17
- label?: (props: {
18
- route: T;
19
- labelText?: string;
20
- focused: boolean;
21
- color: ColorValue;
22
- allowFontScaling?: boolean;
23
- style?: StyleProp<TextStyle>;
24
- }) => React.ReactNode;
25
- labelStyle?: StyleProp<TextStyle>;
26
- icon?: (props: {
27
- route: T;
28
- focused: boolean;
29
- color: ColorValue;
30
- size: number;
31
- }) => React.ReactNode;
32
- badge?: (props: { route: T }) => React.ReactElement;
33
- sceneStyle?: StyleProp<ViewStyle>;
11
+ accessibilityLabel?: string | undefined;
12
+ accessible?: boolean | undefined;
13
+ testID?: string | undefined;
14
+ labelText?: string | undefined;
15
+ labelAllowFontScaling?: boolean | undefined;
16
+ href?: string | undefined;
17
+ label?:
18
+ | ((props: {
19
+ route: T;
20
+ labelText?: string | undefined;
21
+ focused: boolean;
22
+ color: ColorValue;
23
+ allowFontScaling?: boolean | undefined;
24
+ style?: StyleProp<TextStyle> | undefined;
25
+ }) => React.ReactNode)
26
+ | undefined;
27
+ labelStyle?: StyleProp<TextStyle> | undefined;
28
+ icon?:
29
+ | ((props: {
30
+ route: T;
31
+ focused: boolean;
32
+ color: ColorValue;
33
+ size: number;
34
+ }) => React.ReactNode)
35
+ | undefined;
36
+ badge?: ((props: { route: T }) => React.ReactElement) | undefined;
37
+ sceneStyle?: StyleProp<ViewStyle> | undefined;
34
38
  };
35
39
 
36
40
  export type LocaleDirection = 'ltr' | 'rtl';
37
41
 
38
42
  export type Route = {
39
43
  key: string;
40
- icon?: string;
41
- title?: string;
42
- accessible?: boolean;
43
- accessibilityLabel?: string;
44
- testID?: string;
44
+ icon?: string | undefined;
45
+ title?: string | undefined;
46
+ accessible?: boolean | undefined;
47
+ accessibilityLabel?: string | undefined;
48
+ testID?: string | undefined;
45
49
  };
46
50
 
47
51
  export type Event = {
@@ -81,33 +85,33 @@ export type AdapterCommonProps = {
81
85
  * - 'on-drag' - the keyboard is dismissed when a drag begins
82
86
  * - 'none' - drags and tab changes do not dismiss the keyboard
83
87
  */
84
- keyboardDismissMode?: 'none' | 'on-drag' | 'auto';
88
+ keyboardDismissMode?: 'none' | 'on-drag' | 'auto' | undefined;
85
89
  /**
86
90
  * Whether swiping between tabs is enabled.
87
91
  */
88
- swipeEnabled?: boolean;
92
+ swipeEnabled?: boolean | undefined;
89
93
  /**
90
94
  * Whether the tab switch animation is enabled.
91
95
  * If set to false, the tab switch will happen immediately without animation.
92
96
  */
93
- animationEnabled?: boolean;
97
+ animationEnabled?: boolean | undefined;
94
98
  /**
95
99
  * Callback that is called when the swipe gesture starts.
96
100
  */
97
- onSwipeStart?: () => void;
101
+ onSwipeStart?: (() => void) | undefined;
98
102
  /**
99
103
  * Callback that is called when the swipe gesture ends.
100
104
  */
101
- onSwipeEnd?: () => void;
105
+ onSwipeEnd?: (() => void) | undefined;
102
106
  /**
103
107
  * Callback that is called when a tab is selected.
104
108
  * This is called regardless of whether the index has changed or not.
105
109
  */
106
- onTabSelect?: (props: { index: number }) => void;
110
+ onTabSelect?: ((props: { index: number }) => void) | undefined;
107
111
  /**
108
112
  * Style for the pager adapter.
109
113
  */
110
- style?: StyleProp<ViewStyle>;
114
+ style?: StyleProp<ViewStyle> | undefined;
111
115
  };
112
116
 
113
117
  export type AdapterRendererProps = {
@@ -123,7 +127,7 @@ export type AdapterRendererProps = {
123
127
  * The writing direction of the layout.
124
128
  * This can be 'ltr' or 'rtl' based on tab view's `direction` prop.
125
129
  */
126
- layoutDirection?: LocaleDirection;
130
+ layoutDirection?: LocaleDirection | undefined;
127
131
  /**
128
132
  * Render callback that should render the pages of the tab view.
129
133
  */