react-native-tab-view 5.0.0-alpha.0 → 5.0.0-alpha.10

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 (108) hide show
  1. package/lib/module/DefaultAdapter.android.js +4 -0
  2. package/lib/module/DefaultAdapter.android.js.map +1 -0
  3. package/lib/module/DefaultAdapter.ios.js +4 -0
  4. package/lib/module/DefaultAdapter.ios.js.map +1 -0
  5. package/lib/module/DefaultAdapter.js +5 -0
  6. package/lib/module/DefaultAdapter.js.map +1 -0
  7. package/lib/module/PagerViewAdapter.native.js +29 -13
  8. package/lib/module/PagerViewAdapter.native.js.map +1 -1
  9. package/lib/module/PanResponderAdapter.js +3 -0
  10. package/lib/module/PanResponderAdapter.js.map +1 -1
  11. package/lib/module/PlatformPressable.js +1 -1
  12. package/lib/module/PlatformPressable.js.map +1 -1
  13. package/lib/module/SceneMap.js +12 -6
  14. package/lib/module/SceneMap.js.map +1 -1
  15. package/lib/module/SceneView.js +25 -1
  16. package/lib/module/SceneView.js.map +1 -1
  17. package/lib/module/ScrollViewAdapter.js +49 -18
  18. package/lib/module/ScrollViewAdapter.js.map +1 -1
  19. package/lib/module/TabBar.js +279 -169
  20. package/lib/module/TabBar.js.map +1 -1
  21. package/lib/module/TabBarIndicator.js +314 -90
  22. package/lib/module/TabBarIndicator.js.map +1 -1
  23. package/lib/module/TabBarItem.js +94 -44
  24. package/lib/module/TabBarItem.js.map +1 -1
  25. package/lib/module/TabBarItemLabel.js +3 -2
  26. package/lib/module/TabBarItemLabel.js.map +1 -1
  27. package/lib/module/TabView.js +2 -2
  28. package/lib/module/TabView.js.map +1 -1
  29. package/lib/module/constants.js +10 -0
  30. package/lib/module/constants.js.map +1 -0
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/useLayoutWidths.js +46 -0
  34. package/lib/module/useLayoutWidths.js.map +1 -0
  35. package/lib/typescript/src/DefaultAdapter.android.d.ts +2 -0
  36. package/lib/typescript/src/DefaultAdapter.android.d.ts.map +1 -0
  37. package/lib/typescript/src/DefaultAdapter.d.ts +6 -0
  38. package/lib/typescript/src/DefaultAdapter.d.ts.map +1 -0
  39. package/lib/typescript/src/DefaultAdapter.ios.d.ts +2 -0
  40. package/lib/typescript/src/DefaultAdapter.ios.d.ts.map +1 -0
  41. package/lib/typescript/src/PagerViewAdapter.d.ts +1 -1
  42. package/lib/typescript/src/PagerViewAdapter.d.ts.map +1 -1
  43. package/lib/typescript/src/PagerViewAdapter.native.d.ts +2 -2
  44. package/lib/typescript/src/PagerViewAdapter.native.d.ts.map +1 -1
  45. package/lib/typescript/src/PanResponderAdapter.d.ts +1 -1
  46. package/lib/typescript/src/PanResponderAdapter.d.ts.map +1 -1
  47. package/lib/typescript/src/PlatformPressable.d.ts +5 -5
  48. package/lib/typescript/src/PlatformPressable.d.ts.map +1 -1
  49. package/lib/typescript/src/SceneMap.d.ts +2 -2
  50. package/lib/typescript/src/SceneMap.d.ts.map +1 -1
  51. package/lib/typescript/src/SceneView.d.ts +2 -2
  52. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  53. package/lib/typescript/src/ScrollViewAdapter.d.ts +2 -3
  54. package/lib/typescript/src/ScrollViewAdapter.d.ts.map +1 -1
  55. package/lib/typescript/src/TabBar.d.ts +26 -25
  56. package/lib/typescript/src/TabBar.d.ts.map +1 -1
  57. package/lib/typescript/src/TabBarIndicator.d.ts +5 -7
  58. package/lib/typescript/src/TabBarIndicator.d.ts.map +1 -1
  59. package/lib/typescript/src/TabBarItem.d.ts +17 -10
  60. package/lib/typescript/src/TabBarItem.d.ts.map +1 -1
  61. package/lib/typescript/src/TabBarItemLabel.d.ts +4 -3
  62. package/lib/typescript/src/TabBarItemLabel.d.ts.map +1 -1
  63. package/lib/typescript/src/TabView.d.ts +111 -17
  64. package/lib/typescript/src/TabView.d.ts.map +1 -1
  65. package/lib/typescript/src/constants.d.ts +8 -0
  66. package/lib/typescript/src/constants.d.ts.map +1 -0
  67. package/lib/typescript/src/index.d.ts +13 -12
  68. package/lib/typescript/src/index.d.ts.map +1 -1
  69. package/lib/typescript/src/types.d.ts +31 -31
  70. package/lib/typescript/src/types.d.ts.map +1 -1
  71. package/lib/typescript/src/useLayoutWidths.d.ts +2 -0
  72. package/lib/typescript/src/useLayoutWidths.d.ts.map +1 -0
  73. package/lib/typescript/src/useMeasureLayout.d.ts +1 -1
  74. package/lib/typescript/src/useMeasureLayout.d.ts.map +1 -1
  75. package/package.json +13 -12
  76. package/src/DefaultAdapter.android.tsx +1 -0
  77. package/src/DefaultAdapter.ios.tsx +1 -0
  78. package/src/DefaultAdapter.tsx +13 -0
  79. package/src/PagerViewAdapter.native.tsx +36 -18
  80. package/src/PanResponderAdapter.tsx +4 -0
  81. package/src/PlatformPressable.tsx +11 -7
  82. package/src/SceneMap.tsx +17 -9
  83. package/src/SceneView.tsx +41 -2
  84. package/src/ScrollViewAdapter.tsx +88 -22
  85. package/src/TabBar.tsx +440 -235
  86. package/src/TabBarIndicator.tsx +389 -124
  87. package/src/TabBarItem.tsx +107 -46
  88. package/src/TabBarItemLabel.tsx +5 -4
  89. package/src/TabView.tsx +118 -20
  90. package/src/constants.tsx +8 -0
  91. package/src/index.tsx +5 -1
  92. package/src/types.tsx +40 -36
  93. package/src/useLayoutWidths.tsx +51 -0
  94. package/lib/module/Pager.android.js +0 -4
  95. package/lib/module/Pager.android.js.map +0 -1
  96. package/lib/module/Pager.ios.js +0 -4
  97. package/lib/module/Pager.ios.js.map +0 -1
  98. package/lib/module/Pager.js +0 -4
  99. package/lib/module/Pager.js.map +0 -1
  100. package/lib/typescript/src/Pager.android.d.ts +0 -2
  101. package/lib/typescript/src/Pager.android.d.ts.map +0 -1
  102. package/lib/typescript/src/Pager.d.ts +0 -2
  103. package/lib/typescript/src/Pager.d.ts.map +0 -1
  104. package/lib/typescript/src/Pager.ios.d.ts +0 -2
  105. package/lib/typescript/src/Pager.ios.d.ts.map +0 -1
  106. package/src/Pager.android.tsx +0 -1
  107. package/src/Pager.ios.tsx +0 -1
  108. package/src/Pager.tsx +0 -1
@@ -12,28 +12,37 @@ import {
12
12
  } from 'react-native';
13
13
  import useLatestCallback from 'use-latest-callback';
14
14
 
15
+ import {
16
+ TAB_BAR_INACTIVE_COLOR,
17
+ TAB_BAR_PRIMARY_ACTIVE_COLOR,
18
+ TAB_BAR_SECONDARY_ACTIVE_COLOR,
19
+ } from './constants';
15
20
  import { PlatformPressable } from './PlatformPressable';
16
21
  import { TabBarItemLabel } from './TabBarItemLabel';
17
22
  import type { NavigationState, Route, TabDescriptor } from './types';
18
23
 
24
+ type Layout = {
25
+ width: number;
26
+ height: number;
27
+ };
28
+
19
29
  export type Props<T extends Route> = TabDescriptor<T> & {
30
+ variant?: 'primary' | 'secondary' | undefined;
20
31
  position: Animated.AnimatedInterpolation<number>;
21
32
  route: T;
22
33
  navigationState: NavigationState<T>;
23
- activeColor?: ColorValue;
24
- inactiveColor?: ColorValue;
25
- pressColor?: ColorValue;
26
- pressOpacity?: number;
27
- onLayout?: (event: LayoutChangeEvent) => void;
34
+ activeColor?: ColorValue | undefined;
35
+ inactiveColor?: ColorValue | undefined;
36
+ pressColor?: ColorValue | undefined;
37
+ pressOpacity?: number | undefined;
38
+ onMeasureLayout: (layout: Layout) => void;
39
+ onMeasureLabelLayout: (layout: Layout) => void;
28
40
  onPress: () => void;
29
41
  onLongPress: () => void;
30
- defaultTabWidth?: number;
31
42
  style: StyleProp<ViewStyle>;
32
- android_ripple?: PressableAndroidRippleConfig;
43
+ android_ripple?: PressableAndroidRippleConfig | undefined;
33
44
  };
34
45
 
35
- const DEFAULT_ACTIVE_COLOR = 'rgba(0, 0, 0, 1)';
36
- const DEFAULT_INACTIVE_COLOR = 'rgba(0, 0, 0, 0.5)';
37
46
  const ICON_SIZE = 24;
38
47
 
39
48
  const getActiveOpacity = (
@@ -78,15 +87,18 @@ type TabBarItemInternalProps<T extends Route> = Omit<
78
87
  | 'getTestID'
79
88
  | 'getAccessible'
80
89
  | 'options'
90
+ | 'variant'
81
91
  > & {
82
92
  isFocused: boolean;
83
93
  index: number;
84
94
  routesLength: number;
95
+ variant: NonNullable<Props<T>['variant']>;
85
96
  } & TabDescriptor<T>;
86
97
 
87
- const ANDROID_RIPPLE_DEFAULT = { borderless: true };
98
+ const ANDROID_RIPPLE_DEFAULT = { borderless: false, foreground: true };
88
99
 
89
100
  const TabBarItemInternal = <T extends Route>({
101
+ variant,
90
102
  accessibilityLabel,
91
103
  accessible,
92
104
  label: customlabel,
@@ -99,11 +111,11 @@ const TabBarItemInternal = <T extends Route>({
99
111
  inactiveColor: inactiveColorCustom,
100
112
  activeColor: activeColorCustom,
101
113
  labelStyle,
102
- onLayout,
114
+ onMeasureLayout,
115
+ onMeasureLabelLayout,
103
116
  index: tabIndex,
104
117
  pressColor,
105
118
  pressOpacity,
106
- defaultTabWidth,
107
119
  icon: customIcon,
108
120
  badge: customBadge,
109
121
  href,
@@ -114,19 +126,23 @@ const TabBarItemInternal = <T extends Route>({
114
126
  route,
115
127
  }: TabBarItemInternalProps<T>) => {
116
128
  const labelColorFromStyle = StyleSheet.flatten(labelStyle || {}).color;
129
+ const defaultActiveColor =
130
+ variant === 'primary'
131
+ ? TAB_BAR_PRIMARY_ACTIVE_COLOR
132
+ : TAB_BAR_SECONDARY_ACTIVE_COLOR;
117
133
 
118
134
  const activeColor =
119
135
  activeColorCustom !== undefined
120
136
  ? activeColorCustom
121
137
  : typeof labelColorFromStyle === 'string'
122
138
  ? labelColorFromStyle
123
- : DEFAULT_ACTIVE_COLOR;
139
+ : defaultActiveColor;
124
140
  const inactiveColor =
125
141
  inactiveColorCustom !== undefined
126
142
  ? inactiveColorCustom
127
143
  : typeof labelColorFromStyle === 'string'
128
144
  ? labelColorFromStyle
129
- : DEFAULT_INACTIVE_COLOR;
145
+ : TAB_BAR_INACTIVE_COLOR;
130
146
 
131
147
  const activeOpacity = getActiveOpacity(position, routesLength, tabIndex);
132
148
  const inactiveOpacity = getInactiveOpacity(position, routesLength, tabIndex);
@@ -151,7 +167,7 @@ const TabBarItemInternal = <T extends Route>({
151
167
  });
152
168
 
153
169
  return (
154
- <View style={styles.icon}>
170
+ <View>
155
171
  <Animated.View style={{ opacity: inactiveOpacity }}>
156
172
  {inactiveIcon}
157
173
  </Animated.View>
@@ -188,6 +204,7 @@ const TabBarItemInternal = <T extends Route>({
188
204
  icon={icon}
189
205
  label={labelText}
190
206
  style={labelStyle}
207
+ allowFontScaling={labelAllowFontScaling}
191
208
  />
192
209
  ),
193
210
  [
@@ -202,16 +219,39 @@ const TabBarItemInternal = <T extends Route>({
202
219
  ]
203
220
  );
204
221
 
205
- const tabStyle = StyleSheet.flatten(style);
206
- const isWidthSet = tabStyle?.width !== undefined;
207
-
208
- const tabContainerStyle: ViewStyle | null = isWidthSet
209
- ? null
210
- : { width: defaultTabWidth };
211
-
212
222
  const ariaLabel =
213
223
  typeof accessibilityLabel !== 'undefined' ? accessibilityLabel : labelText;
214
224
 
225
+ const viewRef = React.useRef<View>(null);
226
+ const labelRef = React.useRef<View>(null);
227
+
228
+ React.useLayoutEffect(() => {
229
+ viewRef.current?.measure((_x, _y, width, height) => {
230
+ onMeasureLayout({ width, height });
231
+ });
232
+
233
+ labelRef.current?.measure((_x, _y, width, height) => {
234
+ onMeasureLabelLayout({ width, height });
235
+ });
236
+
237
+ // Only measure on mount. onLayout handles updates.
238
+ // eslint-disable-next-line react-hooks/exhaustive-deps
239
+ }, []);
240
+
241
+ const onLayout = (event: LayoutChangeEvent) => {
242
+ const { width, height } = event.nativeEvent.layout;
243
+
244
+ onMeasureLayout({ width, height });
245
+ };
246
+
247
+ const onLabelLayout = (event: LayoutChangeEvent) => {
248
+ const { width, height } = event.nativeEvent.layout;
249
+
250
+ onMeasureLabelLayout({ width, height });
251
+ };
252
+
253
+ const hasIconAndLabel = Boolean(customIcon && labelText);
254
+
215
255
  return (
216
256
  <PlatformPressable
217
257
  android_ripple={android_ripple}
@@ -223,23 +263,32 @@ const TabBarItemInternal = <T extends Route>({
223
263
  pressColor={pressColor}
224
264
  pressOpacity={pressOpacity}
225
265
  unstable_pressDelay={0}
226
- onLayout={onLayout}
227
266
  onPress={onPress}
228
267
  onLongPress={onLongPress}
229
268
  href={href}
230
- style={[styles.pressable, tabContainerStyle]}
269
+ style={styles.pressable}
231
270
  >
232
- <View style={[styles.item, tabStyle]}>
233
- {icon}
234
- <View>
235
- <Animated.View style={{ opacity: inactiveOpacity }}>
236
- {renderLabel(false)}
237
- </Animated.View>
238
- <Animated.View
239
- style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
240
- >
241
- {renderLabel(true)}
242
- </Animated.View>
271
+ <View
272
+ ref={viewRef}
273
+ onLayout={onLayout}
274
+ style={[
275
+ styles.item,
276
+ hasIconAndLabel ? styles.itemWithIcon : null,
277
+ style,
278
+ ]}
279
+ >
280
+ <View ref={labelRef} onLayout={onLabelLayout} style={styles.content}>
281
+ {icon}
282
+ <View>
283
+ <Animated.View style={{ opacity: inactiveOpacity }}>
284
+ {renderLabel(false)}
285
+ </Animated.View>
286
+ <Animated.View
287
+ style={[StyleSheet.absoluteFill, { opacity: activeOpacity }]}
288
+ >
289
+ {renderLabel(true)}
290
+ </Animated.View>
291
+ </View>
243
292
  </View>
244
293
  {customBadge != null ? (
245
294
  <View style={styles.badge}>{customBadge({ route })}</View>
@@ -253,21 +302,30 @@ const MemoizedTabBarItemInternal = React.memo(
253
302
  TabBarItemInternal
254
303
  ) as typeof TabBarItemInternal;
255
304
 
256
- export function TabBarItem<T extends Route>(props: Props<T>) {
257
- const { onPress, onLongPress, onLayout, navigationState, route, ...rest } =
258
- props;
259
-
305
+ export function TabBarItem<T extends Route>({
306
+ variant = 'primary',
307
+ onPress,
308
+ onLongPress,
309
+ onMeasureLayout,
310
+ onMeasureLabelLayout,
311
+ navigationState,
312
+ route,
313
+ ...rest
314
+ }: Props<T>) {
260
315
  const onPressLatest = useLatestCallback(onPress);
261
316
  const onLongPressLatest = useLatestCallback(onLongPress);
262
- const onLayoutLatest = useLatestCallback(onLayout ? onLayout : () => {});
317
+ const onMeasureLayoutLatest = useLatestCallback(onMeasureLayout);
318
+ const onMeasureLabelLayoutLatest = useLatestCallback(onMeasureLabelLayout);
263
319
 
264
320
  const tabIndex = navigationState.routes.indexOf(route);
265
321
 
266
322
  return (
267
323
  <MemoizedTabBarItemInternal
268
324
  {...rest}
325
+ variant={variant}
269
326
  onPress={onPressLatest}
270
- onLayout={onLayoutLatest}
327
+ onMeasureLayout={onMeasureLayoutLatest}
328
+ onMeasureLabelLayout={onMeasureLabelLayoutLatest}
271
329
  onLongPress={onLongPressLatest}
272
330
  isFocused={navigationState.index === tabIndex}
273
331
  route={route}
@@ -278,17 +336,20 @@ export function TabBarItem<T extends Route>(props: Props<T>) {
278
336
  }
279
337
 
280
338
  const styles = StyleSheet.create({
281
- icon: {
282
- margin: 2,
283
- },
284
339
  item: {
285
- flex: 1,
286
340
  alignItems: 'center',
287
341
  justifyContent: 'center',
288
- padding: 10,
342
+ paddingHorizontal: 16,
289
343
  minHeight: 48,
290
344
  pointerEvents: 'none',
291
345
  },
346
+ itemWithIcon: {
347
+ minHeight: 62,
348
+ },
349
+ content: {
350
+ alignItems: 'center',
351
+ justifyContent: 'center',
352
+ },
292
353
  badge: {
293
354
  position: 'absolute',
294
355
  top: 0,
@@ -4,19 +4,21 @@ import { Animated, StyleSheet } from 'react-native';
4
4
 
5
5
  interface TabBarItemLabelProps {
6
6
  color: ColorValue;
7
- label?: string;
8
- style: StyleProp<ViewStyle>;
7
+ label: string | undefined;
9
8
  icon: React.ReactNode;
9
+ style: StyleProp<ViewStyle>;
10
+ allowFontScaling?: boolean | undefined;
10
11
  }
11
12
 
12
13
  export const TabBarItemLabel = React.memo(
13
- ({ color, label, style, icon }: TabBarItemLabelProps) => {
14
+ ({ color, label, style, icon, allowFontScaling }: TabBarItemLabelProps) => {
14
15
  if (!label) {
15
16
  return null;
16
17
  }
17
18
 
18
19
  return (
19
20
  <Animated.Text
21
+ allowFontScaling={allowFontScaling}
20
22
  style={[
21
23
  styles.label,
22
24
  icon ? { marginTop: 0 } : null,
@@ -34,7 +36,6 @@ TabBarItemLabel.displayName = 'TabBarItemLabel';
34
36
 
35
37
  const styles = StyleSheet.create({
36
38
  label: {
37
- margin: 4,
38
39
  fontSize: 14,
39
40
  fontWeight: '500',
40
41
  backgroundColor: 'transparent',
package/src/TabView.tsx CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type ViewStyle,
9
9
  } from 'react-native';
10
10
 
11
- import { Pager } from './Pager';
11
+ import { DefaultAdapter } from './DefaultAdapter';
12
12
  import { SceneView } from './SceneView';
13
13
  import { TabBar } from './TabBar';
14
14
  import type {
@@ -23,27 +23,125 @@ import type {
23
23
  } from './types';
24
24
 
25
25
  export type Props<T extends Route> = AdapterCommonProps & {
26
+ /**
27
+ * Callback which is called when the index of the active tab changes.
28
+ * Must update the `navigationState.index` prop to the new index.
29
+ *
30
+ * Example:
31
+ * ```js
32
+ * onIndexChange={(index) => setState({ ...state, index })}
33
+ * ```
34
+ */
26
35
  onIndexChange: (index: number) => void;
27
- onTabSelect?: (props: { index: number }) => void;
36
+ /**
37
+ * Callback which is called when the tab navigation animation ends.
38
+ * Useful for side effects that should run after the tab change animation.
39
+ *
40
+ * Unlike `onIndexChange`, this is called regardless of whether the index changed or not.
41
+ */
42
+ onTabSelect?: ((props: { index: number }) => void) | undefined;
43
+ /**
44
+ * State for the tab view containing the current index and routes.
45
+ *
46
+ * Example:
47
+ * ```js
48
+ * {
49
+ * index: 0,
50
+ * routes: [
51
+ * { key: 'first' },
52
+ * { key: 'second' },
53
+ * ],
54
+ * }
55
+ * ```
56
+ */
28
57
  navigationState: NavigationState<T>;
29
- renderLazyPlaceholder?: (props: { route: T }) => React.ReactNode;
30
- renderTabBar?: (
31
- props: SceneRendererProps &
32
- EventEmitterProps & {
33
- navigationState: NavigationState<T>;
34
- options: Record<string, TabDescriptor<T>> | undefined;
35
- }
36
- ) => React.ReactNode;
37
- renderAdapter?: (props: AdapterProps) => React.ReactElement;
38
- tabBarPosition?: 'top' | 'bottom';
39
- lazy?: ((props: { route: T }) => boolean) | boolean;
40
- lazyPreloadDistance?: number;
41
- direction?: LocaleDirection;
42
- pagerStyle?: StyleProp<ViewStyle>;
43
- style?: StyleProp<ViewStyle>;
58
+ /**
59
+ * Callback which returns a custom placeholder element.
60
+ * The placeholder is shown when a scene is not yet loaded when `lazy` is enabled.
61
+ */
62
+ renderLazyPlaceholder?:
63
+ | ((props: { route: T }) => React.ReactNode)
64
+ | undefined;
65
+ /**
66
+ * Callback which returns a custom tab bar element to display.
67
+ */
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;
77
+ /**
78
+ * Callback which returns a custom adapter to use for the tab view.
79
+ * Adapters are responsible for handling gestures and animations between tabs.
80
+ *
81
+ * The following adapters are provided out of the box:
82
+ * - `PagerViewAdapter`: Uses `react-native-pager-view` for native experience.
83
+ * - `PanResponderAdapter`: Uses `PanResponder` for a JS-based implementation.
84
+ * - `ScrollViewAdapter`: Uses `ScrollView` for an implementation based on `ScrollView`.
85
+ *
86
+ * Defaults to `PagerViewAdapter` on Android and iOS, and `PanResponderAdapter` on other platforms.
87
+ */
88
+ renderAdapter?: ((props: AdapterProps) => React.ReactElement) | undefined;
89
+ /**
90
+ * Position of the tab bar in the tab view.
91
+ * Defaults to `'top'`.
92
+ */
93
+ tabBarPosition?: 'top' | 'bottom' | undefined;
94
+ /**
95
+ * Whether to lazily render the scenes.
96
+ * When enabled, scenes are rendered only when they come into view.
97
+ *
98
+ * Can be a boolean or a function that receives the route and returns a boolean.
99
+ * Defaults to `false`.
100
+ */
101
+ lazy?: ((props: { route: T }) => boolean) | boolean | undefined;
102
+ /**
103
+ * How many screens to preload when `lazy` is enabled.
104
+ *
105
+ * Defaults to `0`.
106
+ */
107
+ lazyPreloadDistance?: number | undefined;
108
+ /**
109
+ * The layout direction of the tab view.
110
+ *
111
+ * Defaults to the app's locale direction (RTL or LTR).
112
+ */
113
+ direction?: LocaleDirection | undefined;
114
+ /**
115
+ * Style to apply to the pager container.
116
+ */
117
+ pagerStyle?: StyleProp<ViewStyle> | undefined;
118
+ /**
119
+ * Style to apply to the tab view container.
120
+ */
121
+ style?: StyleProp<ViewStyle> | undefined;
122
+ /**
123
+ * Callback which returns a React element to render for each route.
124
+ */
44
125
  renderScene: (props: SceneRendererProps & { route: T }) => React.ReactNode;
45
- options?: Record<string, TabDescriptor<T>>;
46
- commonOptions?: TabDescriptor<T>;
126
+ /**
127
+ * Options for individual tabs, keyed by route key.
128
+ *
129
+ * Example:
130
+ * ```js
131
+ * {
132
+ * first: { labelText: 'First Tab' },
133
+ * second: { labelText: 'Second Tab' },
134
+ * }
135
+ *
136
+ * These options are merged with `commonOptions`.
137
+ */
138
+ options?: Record<string, TabDescriptor<T>> | undefined;
139
+ /**
140
+ * Options that apply to all tabs.
141
+ *
142
+ * Individual tab options from `options` will override these.
143
+ */
144
+ commonOptions?: TabDescriptor<T> | undefined;
47
145
  };
48
146
 
49
147
  const renderLazyPlaceholderDefault = () => null;
@@ -62,7 +160,7 @@ export function TabView<T extends Route>({
62
160
  // eslint-disable-next-line @eslint-react/no-unstable-default-props
63
161
  renderTabBar = (props) => <TabBar {...props} />,
64
162
  // eslint-disable-next-line @eslint-react/no-unstable-default-props
65
- renderAdapter = (props) => <Pager {...props} />,
163
+ renderAdapter = (props) => <DefaultAdapter {...props} />,
66
164
  pagerStyle,
67
165
  style,
68
166
  direction = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr',
@@ -0,0 +1,8 @@
1
+ export const TAB_BAR_PRIMARY_ACTIVE_COLOR = '#6750a4';
2
+ export const TAB_BAR_SECONDARY_ACTIVE_COLOR = '#1d1b20';
3
+ export const TAB_BAR_INACTIVE_COLOR = '#49454f';
4
+ export const TAB_BAR_BACKGROUND_COLOR = '#fef7ff';
5
+ export const TAB_BAR_BORDER_COLOR = '#cac4d0';
6
+
7
+ export const PRIMARY_INDICATOR_MIN_WIDTH = 24;
8
+ export const TAB_MIN_WIDTH = 90;
package/src/index.tsx CHANGED
@@ -1,8 +1,12 @@
1
+ export { DefaultAdapter, type DefaultAdapterProps } from './DefaultAdapter';
1
2
  export {
2
3
  PagerViewAdapter,
3
4
  type PagerViewAdapterProps,
4
5
  } from './PagerViewAdapter';
5
- export { PanResponderAdapter } from './PanResponderAdapter';
6
+ export {
7
+ PanResponderAdapter,
8
+ type PanResponderAdapterProps,
9
+ } from './PanResponderAdapter';
6
10
  export { SceneMap } from './SceneMap';
7
11
  export {
8
12
  ScrollViewAdapter,
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
  */
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+
3
+ // Batches onLayout updates from multiple keyed views into a single state update
4
+ // per frame, so layout passes don't trigger N renders.
5
+ export function useLayoutWidths(priorityKeys: string[]) {
6
+ const [widths, setWidths] = React.useState<Record<string, number>>({});
7
+
8
+ const measured = React.useRef<Record<string, number>>({});
9
+ const handle = React.useRef<ReturnType<typeof requestAnimationFrame>>(null);
10
+
11
+ const priorityKeysRef = React.useRef(priorityKeys);
12
+
13
+ React.useEffect(() => {
14
+ priorityKeysRef.current = priorityKeys;
15
+ });
16
+
17
+ const hasFinishedLayoutRef = React.useRef(false);
18
+
19
+ React.useLayoutEffect(() => {
20
+ hasFinishedLayoutRef.current = true;
21
+ }, []);
22
+
23
+ const onMeasureWidth = React.useCallback((key: string, width: number) => {
24
+ measured.current[key] = width;
25
+
26
+ if (handle.current != null) {
27
+ cancelAnimationFrame(handle.current);
28
+ }
29
+
30
+ if (
31
+ priorityKeysRef.current.includes(key) &&
32
+ !hasFinishedLayoutRef.current
33
+ ) {
34
+ // Synchronously set widths during the initial layout phase
35
+ // So we don't have to wait for the next frame to render the indicator
36
+ setWidths((prev) => ({ ...prev, [key]: width }));
37
+ } else if (hasFinishedLayoutRef.current) {
38
+ handle.current = requestAnimationFrame(() => {
39
+ setWidths({ ...measured.current });
40
+ });
41
+ }
42
+
43
+ return () => {
44
+ if (handle.current != null) {
45
+ cancelAnimationFrame(handle.current);
46
+ }
47
+ };
48
+ }, []);
49
+
50
+ return [widths, onMeasureWidth] as const;
51
+ }
@@ -1,4 +0,0 @@
1
- "use strict";
2
-
3
- export { PagerViewAdapter as Pager } from './PagerViewAdapter';
4
- //# sourceMappingURL=Pager.android.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["PagerViewAdapter","Pager"],"sourceRoot":"../../src","sources":["Pager.android.tsx"],"mappings":";;AAAA,SAASA,gBAAgB,IAAIC,KAAK,QAAQ,oBAAoB","ignoreList":[]}
@@ -1,4 +0,0 @@
1
- "use strict";
2
-
3
- export { PagerViewAdapter as Pager } from './PagerViewAdapter';
4
- //# sourceMappingURL=Pager.ios.js.map