react-native-molecules 0.5.0-beta.3 → 0.5.0-beta.31

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 (227) hide show
  1. package/components/Accordion/Accordion.tsx +2 -6
  2. package/components/Accordion/AccordionItem.tsx +16 -12
  3. package/components/Accordion/AccordionItemContent.tsx +6 -1
  4. package/components/Accordion/AccordionItemHeader.tsx +1 -1
  5. package/components/Accordion/utils.ts +6 -0
  6. package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
  7. package/components/Appbar/AppbarBase.tsx +18 -13
  8. package/components/Button/Button.tsx +211 -264
  9. package/components/Button/index.tsx +9 -3
  10. package/components/Button/types.ts +16 -2
  11. package/components/Button/utils.ts +230 -208
  12. package/components/Card/Card.tsx +1 -1
  13. package/components/Checkbox/Checkbox.tsx +125 -88
  14. package/components/Checkbox/CheckboxBase.ios.tsx +14 -23
  15. package/components/Checkbox/CheckboxBase.tsx +21 -137
  16. package/components/Checkbox/context.tsx +14 -0
  17. package/components/Checkbox/index.tsx +11 -4
  18. package/components/Checkbox/types.ts +63 -29
  19. package/components/Checkbox/utils.ts +25 -108
  20. package/components/Chip/Chip.tsx +41 -52
  21. package/components/Chip/utils.ts +3 -7
  22. package/components/DateField/DateField.tsx +111 -0
  23. package/components/DateField/index.tsx +6 -0
  24. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +19 -51
  25. package/components/DatePicker/DateCalendar.tsx +83 -0
  26. package/components/DatePicker/DatePickerActions.tsx +73 -0
  27. package/components/DatePicker/DatePickerModal.tsx +246 -0
  28. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  29. package/components/DatePicker/DatePickerProvider.tsx +158 -0
  30. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  31. package/components/DatePicker/context.tsx +83 -0
  32. package/components/DatePicker/index.tsx +45 -0
  33. package/components/DatePicker/utils.ts +295 -0
  34. package/components/DatePickerInline/DatePickerDockedHeader.tsx +117 -0
  35. package/components/DatePickerInline/DatePickerInline.tsx +17 -16
  36. package/components/DatePickerInline/DatePickerInlineBase.tsx +11 -5
  37. package/components/DatePickerInline/DatePickerInlineHeader.tsx +50 -20
  38. package/components/DatePickerInline/Day.tsx +25 -1
  39. package/components/DatePickerInline/DayNames.tsx +13 -10
  40. package/components/DatePickerInline/DayRange.tsx +2 -4
  41. package/components/DatePickerInline/HeaderItem.tsx +44 -29
  42. package/components/DatePickerInline/Month.tsx +48 -67
  43. package/components/DatePickerInline/MonthPicker.tsx +80 -92
  44. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  45. package/components/DatePickerInline/Swiper.tsx +169 -14
  46. package/components/DatePickerInline/SwiperUtils.ts +1 -1
  47. package/components/DatePickerInline/Week.tsx +6 -1
  48. package/components/DatePickerInline/YearPicker.tsx +220 -78
  49. package/components/DatePickerInline/dateUtils.tsx +18 -13
  50. package/components/DatePickerInline/store.tsx +27 -0
  51. package/components/DatePickerInline/types.ts +6 -2
  52. package/components/DatePickerInline/utils.ts +66 -29
  53. package/components/Divider/Divider.tsx +192 -0
  54. package/components/Divider/index.tsx +10 -0
  55. package/components/Drawer/Drawer.tsx +17 -6
  56. package/components/Drawer/DrawerItemGroup.tsx +3 -7
  57. package/components/ElementGroup/ElementGroup.tsx +1 -1
  58. package/components/FilePicker/FilePicker.tsx +48 -78
  59. package/components/FilePicker/index.tsx +2 -1
  60. package/components/FilePicker/utils.ts +9 -0
  61. package/components/HelperText/HelperText.tsx +0 -35
  62. package/components/Icon/iconFactory.tsx +5 -4
  63. package/components/Icon/index.tsx +1 -1
  64. package/components/Icon/types.ts +17 -6
  65. package/components/IconButton/IconButton.tsx +84 -84
  66. package/components/IconButton/index.tsx +1 -0
  67. package/components/IconButton/types.ts +10 -0
  68. package/components/IconButton/utils.ts +167 -33
  69. package/components/List/List.tsx +276 -0
  70. package/components/List/context.tsx +27 -0
  71. package/components/List/index.ts +8 -0
  72. package/components/List/types.ts +117 -0
  73. package/components/List/utils.ts +79 -0
  74. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  75. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  76. package/components/LoadingIndicator/index.tsx +13 -0
  77. package/components/LoadingIndicator/utils.ts +117 -0
  78. package/components/Menu/Menu.tsx +162 -39
  79. package/components/Menu/index.tsx +10 -7
  80. package/components/Menu/utils.ts +21 -70
  81. package/components/NavigationRail/NavigationRail.tsx +15 -9
  82. package/components/Popover/Popover.tsx +119 -145
  83. package/components/Popover/PopoverRoot.tsx +60 -0
  84. package/components/Popover/common.ts +54 -34
  85. package/components/Popover/index.ts +12 -1
  86. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  87. package/components/Popover/usePlatformMeasure.ts +120 -0
  88. package/components/Popover/utils.ts +34 -0
  89. package/components/Portal/Portal.tsx +1 -2
  90. package/components/Radio/Radio.tsx +188 -0
  91. package/components/Radio/RadioBase.ios.tsx +69 -0
  92. package/components/Radio/RadioBase.tsx +136 -0
  93. package/components/Radio/context.tsx +23 -0
  94. package/components/Radio/index.tsx +20 -0
  95. package/components/Radio/types.ts +101 -0
  96. package/components/Radio/utils.ts +115 -0
  97. package/components/Rating/Rating.tsx +1 -1
  98. package/components/Select/Select.tsx +521 -785
  99. package/components/Select/context.tsx +81 -0
  100. package/components/Select/index.ts +26 -14
  101. package/components/Select/types.ts +65 -58
  102. package/components/Select/utils.ts +126 -0
  103. package/components/Slot/Slot.tsx +224 -0
  104. package/components/Slot/compose-refs.tsx +62 -0
  105. package/components/Slot/index.tsx +8 -0
  106. package/components/Surface/Surface.android.tsx +32 -7
  107. package/components/Surface/Surface.ios.tsx +34 -29
  108. package/components/Surface/Surface.tsx +31 -4
  109. package/components/Surface/utils.ts +44 -6
  110. package/components/Switch/Switch.ios.tsx +1 -1
  111. package/components/Switch/Switch.tsx +10 -3
  112. package/components/Tabs/TabItem.tsx +35 -58
  113. package/components/Tabs/TabLabel.tsx +5 -9
  114. package/components/Tabs/Tabs.tsx +156 -150
  115. package/components/Tabs/utils.ts +15 -2
  116. package/components/Text/textFactory.tsx +17 -5
  117. package/components/TextInput/TextInput.tsx +663 -579
  118. package/components/TextInput/index.tsx +19 -3
  119. package/components/TextInput/types.ts +77 -28
  120. package/components/TextInput/utils.ts +235 -145
  121. package/components/TimeField/TimeField.tsx +75 -0
  122. package/components/TimeField/index.tsx +6 -0
  123. package/components/TimeField/useTimeFieldState.ts +70 -0
  124. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  125. package/components/TimePicker/AnalogClock.tsx +1 -1
  126. package/components/TimePicker/TimeInput.tsx +87 -42
  127. package/components/TimePicker/TimeInputs.tsx +138 -50
  128. package/components/TimePicker/TimePicker.tsx +74 -11
  129. package/components/TimePicker/TimePickerModal.tsx +186 -0
  130. package/components/TimePicker/context.tsx +17 -0
  131. package/components/TimePicker/index.tsx +15 -3
  132. package/components/TimePicker/utils.ts +93 -4
  133. package/components/Tooltip/Tooltip.tsx +42 -67
  134. package/components/Tooltip/TooltipContent.tsx +32 -5
  135. package/components/Tooltip/TooltipTrigger.tsx +21 -24
  136. package/components/Tooltip/index.tsx +1 -1
  137. package/components/TouchableRipple/TouchableRipple.native.tsx +83 -16
  138. package/components/TouchableRipple/TouchableRipple.tsx +150 -102
  139. package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
  140. package/hocs/index.tsx +1 -1
  141. package/hocs/withKeyboardAccessibility.tsx +2 -3
  142. package/hocs/withPortal.tsx +1 -1
  143. package/hooks/index.tsx +2 -12
  144. package/hooks/useActionState.tsx +19 -8
  145. package/hooks/useContrastColor.ts +1 -2
  146. package/hooks/useFilePicker.tsx +7 -17
  147. package/hooks/useHandleNumberFormat.tsx +2 -2
  148. package/hooks/useMediaQuery.tsx +1 -2
  149. package/package.json +95 -111
  150. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +6 -3
  151. package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
  152. package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
  153. package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
  154. package/styles/shadow.ts +2 -1
  155. package/styles/themes/LightTheme.tsx +1 -1
  156. package/utils/DocumentPicker/documentPicker.ts +78 -27
  157. package/utils/DocumentPicker/types.ts +0 -1
  158. package/utils/extractSubcomponents.ts +89 -0
  159. package/utils/extractTextStyles.ts +1 -2
  160. package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
  161. package/utils/index.ts +0 -3
  162. package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
  163. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  164. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  165. package/components/DatePickerDocked/index.tsx +0 -17
  166. package/components/DatePickerDocked/types.ts +0 -11
  167. package/components/DatePickerDocked/utils.ts +0 -157
  168. package/components/DatePickerInline/DatePickerContext.tsx +0 -21
  169. package/components/DatePickerInput/DatePickerInput.tsx +0 -139
  170. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  171. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
  172. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  173. package/components/DatePickerInput/index.tsx +0 -10
  174. package/components/DatePickerInput/types.ts +0 -28
  175. package/components/DatePickerInput/utils.ts +0 -15
  176. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  177. package/components/DatePickerModal/CalendarEdit.tsx +0 -139
  178. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  179. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  180. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  181. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  182. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  183. package/components/DatePickerModal/index.tsx +0 -16
  184. package/components/DatePickerModal/types.ts +0 -92
  185. package/components/DatePickerModal/utils.ts +0 -122
  186. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  187. package/components/DateTimePicker/index.tsx +0 -10
  188. package/components/DateTimePicker/utils.ts +0 -12
  189. package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
  190. package/components/HorizontalDivider/index.tsx +0 -9
  191. package/components/ListItem/ListItem.tsx +0 -136
  192. package/components/ListItem/ListItemDescription.tsx +0 -25
  193. package/components/ListItem/ListItemTitle.tsx +0 -25
  194. package/components/ListItem/index.tsx +0 -14
  195. package/components/ListItem/utils.ts +0 -115
  196. package/components/Menu/MenuDivider.tsx +0 -13
  197. package/components/Menu/MenuItem.tsx +0 -128
  198. package/components/Popover/Popover.native.tsx +0 -185
  199. package/components/RadioButton/RadioButton.tsx +0 -138
  200. package/components/RadioButton/RadioButtonAndroid.tsx +0 -188
  201. package/components/RadioButton/RadioButtonGroup.tsx +0 -98
  202. package/components/RadioButton/RadioButtonIOS.tsx +0 -106
  203. package/components/RadioButton/RadioButtonItem.tsx +0 -232
  204. package/components/RadioButton/index.ts +0 -22
  205. package/components/RadioButton/utils.ts +0 -165
  206. package/components/TimePickerField/TimePickerField.tsx +0 -152
  207. package/components/TimePickerField/index.tsx +0 -10
  208. package/components/TimePickerField/utils.ts +0 -94
  209. package/components/TimePickerModal/TimePickerModal.tsx +0 -115
  210. package/components/TimePickerModal/index.tsx +0 -10
  211. package/components/TimePickerModal/utils.ts +0 -47
  212. package/components/VerticalDivider/VerticalDivider.tsx +0 -100
  213. package/components/VerticalDivider/index.tsx +0 -9
  214. package/context-bridge/index.tsx +0 -87
  215. package/fast-context/index.tsx +0 -190
  216. package/hocs/typedMemo.tsx +0 -5
  217. package/hooks/useControlledValue.tsx +0 -68
  218. package/hooks/useLatest.tsx +0 -9
  219. package/hooks/useMergedRefs.ts +0 -14
  220. package/hooks/usePrevious.ts +0 -13
  221. package/hooks/useSearchable.tsx +0 -74
  222. package/hooks/useSubcomponents.tsx +0 -59
  223. package/hooks/useToggle.tsx +0 -24
  224. package/utils/color.ts +0 -22
  225. package/utils/compare/index.ts +0 -54
  226. package/utils/lodash.ts +0 -49
  227. package/utils/repository.ts +0 -53
@@ -1,9 +1,8 @@
1
+ import { noop } from '@react-native-molecules/utils/helpers/lodash';
2
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
3
  import {
2
- Children,
3
4
  cloneElement,
4
- type FC,
5
- isValidElement,
6
- memo,
5
+ type ComponentType,
7
6
  type ReactElement,
8
7
  useCallback,
9
8
  useEffect,
@@ -24,21 +23,20 @@ import {
24
23
  type ViewStyle,
25
24
  } from 'react-native';
26
25
 
27
- import { useControlledValue } from '../../hooks';
28
- import { noop } from '../../utils/lodash';
29
- import { HorizontalDivider, type HorizontalDividerProps } from '../HorizontalDivider';
26
+ import { typedMemo } from '../../hocs';
27
+ import { extractSubcomponents } from '../../utils/extractSubcomponents';
30
28
  import type { TabItemProps } from './TabItem';
31
29
  import { tabsStyles } from './utils';
32
30
 
33
- export type TabsProps = ViewProps & {
31
+ export type TabsProps<T extends string | number> = ViewProps & {
34
32
  /**
35
33
  * child tab name
36
34
  * */
37
- value?: string;
35
+ value?: T;
38
36
  /**
39
37
  * defaultValue to preselected for uncontrolled mode
40
38
  * */
41
- defaultValue?: string;
39
+ defaultValue?: T;
42
40
  /**
43
41
  * to enable scroll
44
42
  * */
@@ -46,7 +44,7 @@ export type TabsProps = ViewProps & {
46
44
  /**
47
45
  * on name change callback.
48
46
  * */
49
- onChange?: (value: string) => void;
47
+ onChange?: (value: T) => void;
50
48
  /**
51
49
  * Disable the active indicator below.
52
50
  * */
@@ -58,10 +56,6 @@ export type TabsProps = ViewProps & {
58
56
 
59
57
  indicatorProps?: Omit<ViewStyle, 'style'>;
60
58
 
61
- dividerStyle?: ViewStyle;
62
-
63
- dividerProps?: Omit<HorizontalDividerProps, 'style'>;
64
-
65
59
  /** Define the background Variant. */
66
60
  variant?: 'primary' | 'secondary';
67
61
  activeColor?: string;
@@ -69,7 +63,7 @@ export type TabsProps = ViewProps & {
69
63
 
70
64
  const emptyObj = {};
71
65
 
72
- export const TabBase = ({
66
+ export const TabBase = <T extends string | number>({
73
67
  children,
74
68
  value: valueProp,
75
69
  defaultValue,
@@ -80,84 +74,75 @@ export const TabBase = ({
80
74
  style,
81
75
  variant = 'primary',
82
76
  indicatorProps,
83
- dividerStyle: dividerStyleProp = emptyObj,
84
- dividerProps,
85
77
  activeColor: activeColorProp,
86
78
  testID,
87
79
  ...rest
88
- }: TabsProps) => {
80
+ }: TabsProps<T>) => {
89
81
  tabsStyles.useVariants({
90
82
  variant,
91
83
  });
92
84
 
93
- const validChildren = useMemo(
94
- () =>
95
- Children.toArray(children).filter(
96
- child => isValidElement(child) && (child?.type as FC).displayName === 'Tabs_Item',
97
- ),
98
- [children],
99
- );
100
-
101
- const nameToIndexMap = useMemo(
102
- () =>
103
- validChildren.reduce((acc, child, currentIndex) => {
104
- acc[(child as ReactElement<TabItemProps>).props?.name] = currentIndex;
85
+ const { Tabs_Item: tabItems } = extractSubcomponents({
86
+ children,
87
+ allowedChildren: ['Tabs_Item'],
88
+ });
105
89
 
106
- return acc;
107
- }, {} as Record<string, number>),
108
- [validChildren],
90
+ // Get ordered list of tab names for current children
91
+ const tabNames = useMemo(
92
+ () => tabItems.map(child => (child as ReactElement<TabItemProps<T>>).props?.name),
93
+ [tabItems],
109
94
  );
110
95
 
111
96
  const [value, onChange] = useControlledValue({
112
97
  value: valueProp,
113
98
  onChange: onChangeProp,
114
- defaultValue: defaultValue || (validChildren[0] as ReactElement<TabItemProps>)?.props?.name,
99
+ defaultValue: defaultValue || tabNames[0],
115
100
  });
116
101
 
117
- const valueIndex = nameToIndexMap[value];
102
+ const valueIndex = useMemo(() => tabNames.indexOf(value), [value, tabNames]);
103
+ const previousTabCountRef = useRef(tabNames.length);
118
104
 
119
105
  const positionAnimationRef = useRef(new Animated.Value(0));
120
106
  const widthAnimationRef = useRef(new Animated.Value(0));
121
107
  const scrollViewRef = useRef<RNScrollView>(null);
122
108
  const scrollViewPosition = useRef(0);
123
109
 
124
- const tabItemPositions = useRef<Array<{ width: number; contentWidth: number }>>([]);
110
+ const tabItemPositions = useRef<Map<T, { width: number; contentWidth: number }>>(new Map());
125
111
  const [tabContainerWidth, setTabContainerWidth] = useState(0);
112
+ const [layoutVersion, setLayoutVersion] = useState(0);
126
113
 
127
114
  const itemPositionsMap = useMemo(() => {
128
- return tabItemPositions.current.reduce((acc, item, index) => {
129
- const previousItemsWidth = tabItemPositions.current
130
- .slice(0, index)
131
- .reduce((totalWidth, _item) => {
132
- totalWidth += _item.width || 0;
133
-
134
- return totalWidth;
135
- }, 0);
115
+ // Build positions based on current render order
116
+ let accumulatedWidth = 0;
117
+ return tabNames.reduce((acc, name, index) => {
118
+ const itemData = tabItemPositions.current.get(name);
119
+ if (!itemData) return acc;
136
120
 
137
121
  acc[index] =
138
122
  variant === 'primary'
139
- ? previousItemsWidth + (item.width - item.contentWidth) / 2
140
- : previousItemsWidth;
123
+ ? accumulatedWidth + (itemData.width - itemData.contentWidth) / 2
124
+ : accumulatedWidth;
141
125
 
126
+ accumulatedWidth += itemData.width || 0;
142
127
  return acc;
143
128
  }, {} as Record<number, number>);
144
- // to make useMemo in sync with the ref
145
129
  // eslint-disable-next-line react-hooks/exhaustive-deps
146
- }, [variant, tabItemPositions.current.length]);
130
+ }, [variant, tabNames, layoutVersion]);
147
131
 
148
132
  const itemWidthsMap = useMemo(() => {
149
- return tabItemPositions.current.reduce((acc, item, index) => {
150
- acc[index] = variant === 'primary' ? item.contentWidth : item.width;
133
+ return tabNames.reduce((acc, name, index) => {
134
+ const itemData = tabItemPositions.current.get(name);
135
+ if (!itemData) return acc;
151
136
 
137
+ acc[index] = variant === 'primary' ? itemData.contentWidth : itemData.width;
152
138
  return acc;
153
139
  }, {} as Record<number, number>);
154
- // to make useMemo in sync with the ref
155
140
  // eslint-disable-next-line react-hooks/exhaustive-deps
156
- }, [variant, tabItemPositions.current.length]);
141
+ }, [variant, tabNames, layoutVersion]);
157
142
 
158
143
  const scrollHandler = useCallback(
159
144
  (currValue: number) => {
160
- if (tabItemPositions.current.length > currValue) {
145
+ if (tabItemPositions.current.size > currValue) {
161
146
  const itemStartPosition = currValue === 0 ? 0 : itemPositionsMap[currValue - 1];
162
147
  const itemEndPosition = itemPositionsMap[currValue];
163
148
 
@@ -182,45 +167,76 @@ export const TabBase = ({
182
167
  [itemPositionsMap, tabContainerWidth],
183
168
  );
184
169
 
170
+ // Animate indicator position and width when value changes
171
+ useEffect(() => {
172
+ Animated.parallel([
173
+ Animated.timing(positionAnimationRef.current, {
174
+ toValue: valueIndex,
175
+ useNativeDriver: false,
176
+ duration: 170,
177
+ }),
178
+ Animated.timing(widthAnimationRef.current, {
179
+ toValue: valueIndex,
180
+ useNativeDriver: false,
181
+ duration: 170,
182
+ }),
183
+ ]).start();
184
+
185
+ if (scrollable) {
186
+ requestAnimationFrame(() => scrollHandler(valueIndex));
187
+ }
188
+ }, [scrollHandler, valueIndex, scrollable]);
189
+
190
+ // Handle tab count changes
185
191
  useEffect(() => {
186
- Animated.timing(positionAnimationRef.current, {
187
- toValue: valueIndex,
188
- useNativeDriver: false,
189
- duration: 170,
190
- }).start();
192
+ const currentTabCount = tabNames.length;
193
+
194
+ if (currentTabCount !== previousTabCountRef.current) {
195
+ // Clean up positions for removed tabs
196
+ const currentTabNamesSet = new Set(tabNames);
197
+ tabItemPositions.current.forEach((_, name) => {
198
+ if (!currentTabNamesSet.has(name)) {
199
+ tabItemPositions.current.delete(name);
200
+ }
201
+ });
191
202
 
192
- scrollable && requestAnimationFrame(() => scrollHandler(valueIndex));
193
- }, [positionAnimationRef, scrollHandler, valueIndex, scrollable]);
203
+ // Trigger re-calculation
204
+ setLayoutVersion(v => v + 1);
194
205
 
195
- useEffect(() => {
196
- Animated.timing(widthAnimationRef.current, {
197
- toValue: valueIndex,
198
- useNativeDriver: false,
199
- duration: 170,
200
- }).start();
201
- }, [positionAnimationRef, scrollHandler, valueIndex]);
206
+ // Clamp animated values when tabs are removed
207
+ if (currentTabCount < previousTabCountRef.current) {
208
+ const maxValidIndex = Math.max(0, currentTabCount - 1);
209
+ positionAnimationRef.current.setValue(Math.min(valueIndex, maxValidIndex));
210
+ widthAnimationRef.current.setValue(Math.min(valueIndex, maxValidIndex));
211
+ }
212
+ }
213
+
214
+ previousTabCountRef.current = currentTabCount;
215
+ }, [tabNames, valueIndex]);
202
216
 
203
217
  const onScrollHandler = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
204
218
  scrollViewPosition.current = event.nativeEvent.contentOffset.x;
205
219
  }, []);
206
220
 
207
- const transitionInterpolateWithMap = useCallback(
208
- (obj: Record<any, number>) => {
209
- const countItems = validChildren.length;
210
- if (countItems < 2 || !tabItemPositions.current.length) {
211
- // if there's only one item, use the value of that
212
- return Object.values(obj)[0] || 0;
213
- }
214
- const inputRange = Array.from(Array(countItems).keys());
215
- const outputRange = Object.values(obj);
221
+ const transitionInterpolateWithMap = useCallback((obj: Record<number, number>) => {
222
+ const entries = Object.entries(obj);
223
+ const countItems = entries.length;
216
224
 
217
- return positionAnimationRef.current.interpolate({
218
- inputRange,
219
- outputRange,
220
- });
221
- },
222
- [validChildren.length],
223
- );
225
+ if (countItems < 2 || !tabItemPositions.current.size) {
226
+ // If there's only one item or no layout data, use the first value
227
+ return Object.values(obj)[0] || 0;
228
+ }
229
+
230
+ // Use indices from the map entries to ensure inputRange matches outputRange
231
+ const inputRange = entries.map(([key]) => Number(key));
232
+ const outputRange = entries.map(([, val]) => val);
233
+
234
+ return positionAnimationRef.current.interpolate({
235
+ inputRange,
236
+ outputRange,
237
+ extrapolate: 'clamp',
238
+ });
239
+ }, []);
224
240
 
225
241
  const indicatorTransitionInterpolate = useMemo(() => {
226
242
  return transitionInterpolateWithMap(itemPositionsMap);
@@ -230,14 +246,12 @@ export const TabBase = ({
230
246
  return transitionInterpolateWithMap(itemWidthsMap);
231
247
  }, [transitionInterpolateWithMap, itemWidthsMap]);
232
248
 
233
- const { containerStyle, itemsContainerStyle, dividerStyle, indicatorStyle } = useMemo(() => {
234
- const { indicator, itemsContainer, divider } = tabsStyles;
249
+ const { containerStyle, indicatorStyle } = useMemo(() => {
250
+ const { indicator, itemsContainer } = tabsStyles;
235
251
  const { activeColor, ...restStyle } = tabsStyles.root;
236
252
 
237
253
  return {
238
- containerStyle: [restStyle, style],
239
- itemsContainerStyle: itemsContainer,
240
- dividerStyle: [divider, dividerStyleProp],
254
+ containerStyle: [restStyle, itemsContainer, style],
241
255
  indicatorStyle: [
242
256
  indicator,
243
257
  {
@@ -254,7 +268,6 @@ export const TabBase = ({
254
268
  };
255
269
  }, [
256
270
  style,
257
- dividerStyleProp,
258
271
  activeColorProp,
259
272
  indicatorTransitionInterpolate,
260
273
  widthTransitionInterpolate,
@@ -268,7 +281,6 @@ export const TabBase = ({
268
281
  ref: scrollViewRef,
269
282
  onScroll: onScrollHandler,
270
283
  showsHorizontalScrollIndicator: false,
271
- style: itemsContainerStyle,
272
284
  }
273
285
  : {};
274
286
 
@@ -276,95 +288,88 @@ export const TabBase = ({
276
288
  setTabContainerWidth(layout.width);
277
289
  }, []);
278
290
 
279
- const onLayoutItem = useCallback((event: LayoutChangeEvent, index: number) => {
291
+ const onLayoutItem = useCallback((event: LayoutChangeEvent, name: T) => {
280
292
  const { width } = event.nativeEvent.layout;
281
293
 
282
- const currentItemPosition = tabItemPositions.current[index];
294
+ const currentItemPosition = tabItemPositions.current.get(name) || {
295
+ width: 0,
296
+ contentWidth: 0,
297
+ };
283
298
 
284
- tabItemPositions.current[index] = {
299
+ tabItemPositions.current.set(name, {
285
300
  ...currentItemPosition,
286
301
  width: width,
287
- };
302
+ });
303
+ setLayoutVersion(v => v + 1);
288
304
  }, []);
289
305
 
290
- const onLayoutText = useCallback((event: LayoutChangeEvent, index: number) => {
306
+ const onLayoutText = useCallback((event: LayoutChangeEvent, name: T) => {
291
307
  const { width } = event.nativeEvent.layout;
292
308
 
293
- const currentItemPosition = tabItemPositions.current[index];
309
+ const currentItemPosition = tabItemPositions.current.get(name) || {
310
+ width: 0,
311
+ contentWidth: 0,
312
+ };
294
313
 
295
- tabItemPositions.current[index] = {
314
+ tabItemPositions.current.set(name, {
296
315
  ...currentItemPosition,
297
316
  contentWidth: width,
298
- };
317
+ });
318
+ setLayoutVersion(v => v + 1);
299
319
  }, []);
300
320
 
301
321
  return (
302
- <View
322
+ <Container
303
323
  {...rest}
304
324
  testID={testID}
325
+ {...containerProps}
305
326
  style={containerStyle}
306
327
  accessibilityRole="tablist"
307
328
  onLayout={onLayout}>
308
- <>
309
- <Container
310
- testID={testID && `${testID}--inner-container`}
311
- {...containerProps}
312
- style={itemsContainerStyle}>
313
- {validChildren.map((child, index) => (
314
- <ChildItem
315
- key={(child as ReactElement<TabItemProps>).props?.name}
316
- testID={testID && `${testID}--tab-item`}
317
- index={index}
318
- value={value}
319
- child={child as ReactElement<TabItemProps>}
320
- onChange={onChange}
321
- onLayout={onLayoutItem}
322
- onLayoutContent={onLayoutText}
323
- variant={variant}
324
- />
325
- ))}
326
-
327
- {!disableIndicator && (
328
- <Animated.View
329
- testID={testID && `${testID}--active-indicator`}
330
- {...indicatorProps}
331
- style={indicatorStyle}
332
- />
333
- )}
334
- </Container>
335
-
336
- <HorizontalDivider
337
- testID={testID && `${testID}--divider`}
338
- {...dividerProps}
339
- style={dividerStyle}
329
+ {tabItems.map(child => (
330
+ <ChildItem
331
+ key={(child as ReactElement<TabItemProps<T>>).props?.name}
332
+ testID={testID && `${testID}--tab-item`}
333
+ value={value}
334
+ child={child as ReactElement<TabItemProps<T>>}
335
+ onChange={onChange}
336
+ onLayout={onLayoutItem}
337
+ onLayoutContent={onLayoutText}
338
+ variant={variant}
339
+ />
340
+ ))}
341
+
342
+ {!disableIndicator && (
343
+ <Animated.View
344
+ testID={testID && `${testID}--active-indicator`}
345
+ {...indicatorProps}
346
+ style={indicatorStyle}
340
347
  />
341
- </>
342
- </View>
348
+ )}
349
+ </Container>
343
350
  );
344
351
  };
345
352
 
346
- type ChildItemProps = {
347
- variant: TabsProps['variant'];
348
- child: ReactElement<TabItemProps>;
349
- onChange: TabsProps['onChange'];
350
- onLayout: (event: LayoutChangeEvent, index: number) => void;
351
- onLayoutContent: (event: LayoutChangeEvent, index: number) => void;
352
- value: string;
353
- index: number;
353
+ type ChildItemProps<T extends string | number> = {
354
+ variant: TabsProps<T>['variant'];
355
+ child: ReactElement<TabItemProps<T>>;
356
+ onChange: TabsProps<T>['onChange'];
357
+ onLayout: (event: LayoutChangeEvent, name: T) => void;
358
+ onLayoutContent: (event: LayoutChangeEvent, name: T) => void;
359
+ value: string | number;
354
360
  testID?: string;
355
361
  };
356
362
 
357
- const ChildItem = memo(
358
- ({
363
+ const ChildItem = typedMemo(
364
+ <T extends string | number>({
359
365
  value,
360
366
  child,
361
367
  onChange,
362
368
  onLayout: onLayoutProp,
363
369
  onLayoutContent: onLayoutContentProp,
364
370
  variant,
365
- index,
366
371
  testID,
367
- }: ChildItemProps) => {
372
+ }: ChildItemProps<T>) => {
368
373
  const name = child.props?.name;
369
374
 
370
375
  const onPress = useCallback(() => {
@@ -373,16 +378,16 @@ const ChildItem = memo(
373
378
 
374
379
  const onLayout = useCallback(
375
380
  (e: LayoutChangeEvent) => {
376
- onLayoutProp(e, index);
381
+ onLayoutProp(e, name);
377
382
  },
378
- [index, onLayoutProp],
383
+ [name, onLayoutProp],
379
384
  );
380
385
 
381
386
  const onLayoutContent = useCallback(
382
387
  (e: LayoutChangeEvent) => {
383
- onLayoutContentProp(e, index);
388
+ onLayoutContentProp(e, name);
384
389
  },
385
- [index, onLayoutContentProp],
390
+ [name, onLayoutContentProp],
386
391
  );
387
392
 
388
393
  return cloneElement(child, {
@@ -396,4 +401,5 @@ const ChildItem = memo(
396
401
  },
397
402
  );
398
403
 
404
+ (ChildItem as ComponentType).displayName = 'Tabs_ChildItem';
399
405
  TabBase.displayName = 'Tabs';
@@ -1,10 +1,25 @@
1
+ import { createContext } from 'react';
1
2
  import { StyleSheet } from 'react-native-unistyles';
2
3
 
3
4
  import { getRegisteredComponentStylesWithFallback } from '../../core';
4
5
 
6
+ export type TabItemContextType = {
7
+ active: boolean;
8
+ hovered: boolean;
9
+ variant: 'primary' | 'secondary';
10
+ };
11
+
12
+ export const TabItemContext = createContext<TabItemContextType>({
13
+ active: false,
14
+ hovered: false,
15
+ variant: 'primary',
16
+ });
17
+
5
18
  const tabsStylesDefault = StyleSheet.create(theme => ({
6
19
  root: {
7
20
  activeColor: theme.colors.primary,
21
+ borderBottomWidth: 1,
22
+ borderBottomColor: theme.colors.outlineVariant,
8
23
  } as any,
9
24
 
10
25
  itemsContainer: {
@@ -30,8 +45,6 @@ const tabsStylesDefault = StyleSheet.create(theme => ({
30
45
  },
31
46
  },
32
47
  },
33
-
34
- divider: {},
35
48
  }));
36
49
 
37
50
  const tabsItemStylesDefault = StyleSheet.create(theme => ({
@@ -1,6 +1,8 @@
1
1
  import { type ComponentType, createContext, forwardRef, memo, useContext } from 'react';
2
2
  import { Text, type TextProps } from 'react-native';
3
- import { StyleSheet } from 'react-native-unistyles';
3
+ import { StyleSheet, useUnistyles } from 'react-native-unistyles';
4
+
5
+ import { MD3TypescaleKey } from '../../types/theme';
4
6
 
5
7
  const HasAncestorContext = createContext(false);
6
8
 
@@ -8,18 +10,28 @@ const defaultStyles = StyleSheet.create(theme => ({
8
10
  root: { color: theme.colors.onSurface, ...theme.typescale.bodyMedium },
9
11
  }));
10
12
 
13
+ export type TypescaleKey = `${MD3TypescaleKey}`;
14
+
15
+ export type TextFactoryProps = TextProps & {
16
+ typescale?: TypescaleKey;
17
+ };
18
+
11
19
  export const textFactory = (
12
20
  componentStyles: typeof defaultStyles = defaultStyles,
13
21
  isBlockLevelElement = false,
14
22
  DefaultComponent: ComponentType<any> = Text,
15
23
  ) => {
16
24
  return memo(
17
- forwardRef((props: TextProps, ref: any) => {
18
- const { style, ...rest } = props;
25
+ forwardRef((props: TextFactoryProps, ref: any) => {
26
+ const { style, typescale, ...rest } = props;
19
27
  const hasAncestorText = useContext(HasAncestorContext);
28
+ const { theme } = useUnistyles();
29
+
30
+ const typescaleStyle = typescale ? theme.typescale[typescale] : undefined;
20
31
 
21
- const styles =
22
- hasAncestorText && !isBlockLevelElement ? style : [componentStyles?.root, style];
32
+ const baseStyle =
33
+ hasAncestorText && !isBlockLevelElement ? null : componentStyles?.root;
34
+ const styles = [baseStyle, typescaleStyle, style];
23
35
 
24
36
  return hasAncestorText ? (
25
37
  <DefaultComponent ref={ref} style={styles} {...rest} />