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

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 +244 -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 +20 -20
  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
@@ -0,0 +1,62 @@
1
+ import * as React from 'react';
2
+
3
+ type PossibleRef<T> = React.Ref<T> | undefined;
4
+
5
+ /**
6
+ * Set a given ref to a given value
7
+ * This utility takes care of different types of refs: callback refs and RefObject(s)
8
+ */
9
+ function setRef<T>(ref: PossibleRef<T>, value: T) {
10
+ if (typeof ref === 'function') {
11
+ return ref(value);
12
+ } else if (ref !== null && ref !== undefined) {
13
+ ref.current = value;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * A utility to compose multiple refs together
19
+ * Accepts callback refs and RefObject(s)
20
+ */
21
+ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
22
+ return node => {
23
+ let hasCleanup = false;
24
+ const cleanups = refs.map(ref => {
25
+ const cleanup = setRef(ref, node);
26
+ if (!hasCleanup && typeof cleanup === 'function') {
27
+ hasCleanup = true;
28
+ }
29
+ return cleanup;
30
+ });
31
+
32
+ // React <19 will log an error to the console if a callback ref returns a
33
+ // value. We don't use ref cleanups internally so this will only happen if a
34
+ // user's ref callback returns a value, which we only expect if they are
35
+ // using the cleanup functionality added in React 19.
36
+ if (hasCleanup) {
37
+ return () => {
38
+ for (let i = 0; i < cleanups.length; i++) {
39
+ const cleanup = cleanups[i];
40
+ if (typeof cleanup === 'function') {
41
+ cleanup();
42
+ } else {
43
+ setRef(refs[i], null);
44
+ }
45
+ }
46
+ };
47
+ }
48
+
49
+ return undefined;
50
+ };
51
+ }
52
+
53
+ /**
54
+ * A custom hook that composes multiple refs
55
+ * Accepts callback refs and RefObject(s)
56
+ */
57
+ function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ return React.useCallback(composeRefs(...refs), refs);
60
+ }
61
+
62
+ export { composeRefs, useComposedRefs };
@@ -0,0 +1,8 @@
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
+ import { Slot as SlotComponent, Slottable } from './Slot';
3
+
4
+ const SlotDefault = Object.assign(SlotComponent, { Slottable, Root: SlotComponent });
5
+
6
+ export const Slot = getRegisteredComponentWithFallback('Slot', SlotDefault);
7
+
8
+ export { createSlot, createSlottable, type SlotProps } from './Slot';
@@ -2,11 +2,13 @@ import { forwardRef, memo, type ReactNode, useMemo } from 'react';
2
2
  import { Animated, type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
3
3
  import { useUnistyles } from 'react-native-unistyles';
4
4
 
5
- import { inputRange } from '../../styles/shadow';
6
5
  import type { MD3Elevation } from '../../types/theme';
7
6
  import { extractPropertiesFromStyles } from '../../utils/extractPropertiesFromStyles';
7
+ import { Slot } from '../Slot';
8
8
  import { BackgroundContextWrapper } from './BackgroundContextWrapper';
9
- import { defaultStyles, getElevationAndroid } from './utils';
9
+ import { defaultStyles } from './utils';
10
+
11
+ const AnimatedView = Animated.createAnimatedComponent(View);
10
12
 
11
13
  export type Props = ViewProps & {
12
14
  /**
@@ -20,11 +22,31 @@ export type Props = ViewProps & {
20
22
  * TestID used for testing purposes
21
23
  */
22
24
  testID?: string;
25
+ /**
26
+ * When `true`, the component will not render a wrapper element. Instead, it will
27
+ * merge its props (styles, elevation shadow, ref) onto its immediate child element.
28
+ * This follows the Radix UI "Slot" pattern for flexible component composition.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * // With asChild - merges elevation styles onto the child
33
+ * <Surface asChild elevation={2}>
34
+ * <Card><Text>Content</Text></Card>
35
+ * </Surface>
36
+ * ```
37
+ *
38
+ * @note When `asChild` is `true`, only a single child element is allowed.
39
+ * @default false
40
+ */
41
+ asChild?: boolean;
23
42
  };
24
43
 
25
44
  const elevationLevel = [0, 1, 2, 6, 8, 12];
26
45
 
27
- const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, ref: any) => {
46
+ const Surface = (
47
+ { elevation: _elevation = 1, style, children, testID, asChild = false, ...props }: Props,
48
+ ref: any,
49
+ ) => {
28
50
  const { theme } = useUnistyles();
29
51
 
30
52
  const backgroundColor = (() => {
@@ -33,6 +55,7 @@ const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, re
33
55
  })();
34
56
 
35
57
  const { memoizedStyles, surfaceBackground } = useMemo(() => {
58
+ const elevation = typeof _elevation === 'number' ? (_elevation > 5 ? 5 : _elevation) : 0;
36
59
  return {
37
60
  memoizedStyles: [
38
61
  {
@@ -41,7 +64,7 @@ const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, re
41
64
  defaultStyles.root,
42
65
  style,
43
66
  {
44
- elevation: getElevationAndroid(elevation, inputRange, elevationLevel),
67
+ elevation: elevationLevel[elevation],
45
68
  },
46
69
  ] as StyleProp<ViewStyle>,
47
70
  surfaceBackground: extractPropertiesFromStyles(
@@ -49,13 +72,15 @@ const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, re
49
72
  ['backgroundColor'],
50
73
  ).backgroundColor,
51
74
  };
52
- }, [backgroundColor, elevation, style]);
75
+ }, [backgroundColor, _elevation, style]);
76
+
77
+ const Component = asChild ? Slot : AnimatedView;
53
78
 
54
79
  return (
55
80
  <BackgroundContextWrapper backgroundColor={surfaceBackground}>
56
- <View ref={ref} {...props} testID={testID} style={memoizedStyles}>
81
+ <Component ref={ref} {...props} testID={testID} style={memoizedStyles}>
57
82
  {children}
58
- </View>
83
+ </Component>
59
84
  </BackgroundContextWrapper>
60
85
  );
61
86
  };
@@ -4,8 +4,11 @@ import { useUnistyles } from 'react-native-unistyles';
4
4
 
5
5
  import type { MD3Elevation } from '../../types/theme';
6
6
  import { extractPropertiesFromStyles } from '../../utils/extractPropertiesFromStyles';
7
+ import { Slot } from '../Slot';
7
8
  import { BackgroundContextWrapper } from './BackgroundContextWrapper';
8
- import { defaultStyles, getStyleForShadowLayer } from './utils';
9
+ import { defaultStyles, getCombinedShadowStyle } from './utils';
10
+
11
+ const AnimatedView = Animated.createAnimatedComponent(View);
9
12
 
10
13
  export type Props = ComponentPropsWithRef<typeof View> & {
11
14
  /**
@@ -27,6 +30,23 @@ export type Props = ComponentPropsWithRef<typeof View> & {
27
30
  * TestID used for testing purposes
28
31
  */
29
32
  testID?: string;
33
+ /**
34
+ * When `true`, the component will not render a wrapper element. Instead, it will
35
+ * merge its props (styles, elevation shadow, ref) onto its immediate child element.
36
+ * This follows the Radix UI "Slot" pattern for flexible component composition.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * // With asChild - merges elevation styles onto the child
41
+ * <Surface asChild elevation={2}>
42
+ * <Card><Text>Content</Text></Card>
43
+ * </Surface>
44
+ * ```
45
+ *
46
+ * @note When `asChild` is `true`, only a single child element is allowed.
47
+ * @default false
48
+ */
49
+ asChild?: boolean;
30
50
  };
31
51
 
32
52
  /**
@@ -71,53 +91,38 @@ export type Props = ComponentPropsWithRef<typeof View> & {
71
91
  * });
72
92
  * ```
73
93
  */
74
- const Surface = ({ elevation = 1, style, children, testID, ...props }: Props, ref: any) => {
94
+ const Surface = (
95
+ { elevation = 1, style, children, testID, asChild = false, ...props }: Props,
96
+ ref: any,
97
+ ) => {
75
98
  const { theme } = useUnistyles();
76
99
  const backgroundColor = (() => {
77
100
  // @ts-ignore
78
101
  return theme.colors.elevation?.[`level${elevation}`];
79
102
  })();
80
103
 
81
- const { surfaceBackground, sharedStyle, layer0Style, layer1Style } = useMemo(() => {
82
- const { position, alignSelf, top, left, right, bottom, borderRadius } =
83
- extractPropertiesFromStyles(
84
- [defaultStyles.root as ViewStyle, style],
85
- ['position', 'alignSelf', 'top', 'left', 'right', 'bottom', 'borderRadius'],
86
- );
87
- const absoluteStyle = { position, alignSelf, top, right, bottom, left };
88
-
104
+ const { surfaceBackground, combinedStyle } = useMemo(() => {
89
105
  return {
90
106
  surfaceBackground: extractPropertiesFromStyles(
91
107
  [defaultStyles.root as ViewStyle, style],
92
108
  ['backgroundColor'],
93
109
  ).backgroundColor,
94
- sharedStyle: [
95
- { backgroundColor, borderRadius },
110
+ combinedStyle: [
111
+ { backgroundColor },
112
+ getCombinedShadowStyle(elevation),
96
113
  defaultStyles.root,
97
114
  style,
98
- {
99
- position: undefined,
100
- alignSelf: undefined,
101
- top: undefined,
102
- left: undefined,
103
- right: undefined,
104
- bottom: undefined,
105
- },
106
115
  ],
107
- layer0Style: [getStyleForShadowLayer(0, elevation), absoluteStyle, { borderRadius }],
108
- layer1Style: [getStyleForShadowLayer(1, elevation), { borderRadius }],
109
116
  };
110
117
  }, [backgroundColor, elevation, style]);
111
118
 
119
+ const Component = asChild ? Slot : AnimatedView;
120
+
112
121
  return (
113
122
  <BackgroundContextWrapper backgroundColor={surfaceBackground}>
114
- <View ref={ref} style={layer0Style}>
115
- <View style={layer1Style}>
116
- <View {...props} testID={testID} style={sharedStyle}>
117
- {children}
118
- </View>
119
- </View>
120
- </View>
123
+ <Component ref={ref} {...props} testID={testID} style={combinedStyle}>
124
+ {children}
125
+ </Component>
121
126
  </BackgroundContextWrapper>
122
127
  );
123
128
  };
@@ -1,11 +1,14 @@
1
1
  import { forwardRef, memo, type ReactNode, useMemo } from 'react';
2
- import { type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
2
+ import { Animated, type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
3
3
 
4
4
  import shadow from '../../styles/shadow';
5
5
  import type { MD3Elevation } from '../../types/theme';
6
+ import { Slot } from '../Slot';
6
7
  import { BackgroundContextWrapper } from './BackgroundContextWrapper';
7
8
  import { defaultStyles } from './utils';
8
9
 
10
+ const AnimatedView = Animated.createAnimatedComponent(View);
11
+
9
12
  export type Props = ViewProps & {
10
13
  /**
11
14
  * Content of the `Surface`.
@@ -18,11 +21,33 @@ export type Props = ViewProps & {
18
21
  * TestID used for testing purposes
19
22
  */
20
23
  testID?: string;
24
+ /**
25
+ * When `true`, the component will not render a wrapper element. Instead, it will
26
+ * merge its props (styles, elevation shadow, ref) onto its immediate child element.
27
+ * This follows the Radix UI "Slot" pattern for flexible component composition.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * // Without asChild - renders an AnimatedView wrapper
32
+ * <Surface elevation={2}>
33
+ * <Card><Text>Content</Text></Card>
34
+ * </Surface>
35
+ *
36
+ * // With asChild - merges elevation styles onto the child
37
+ * <Surface asChild elevation={2}>
38
+ * <Card><Text>Content</Text></Card>
39
+ * </Surface>
40
+ * ```
41
+ *
42
+ * @note When `asChild` is `true`, only a single child element is allowed.
43
+ * @default false
44
+ */
45
+ asChild?: boolean;
21
46
  };
22
47
 
23
48
  // for Web
24
49
  const Surface = (
25
- { elevation = 1, style, children, testID, backgroundColor, ...props }: Props,
50
+ { elevation = 1, style, children, testID, backgroundColor, asChild = false, ...props }: Props,
26
51
  ref: any,
27
52
  ) => {
28
53
  const { surfaceStyle } = useMemo(() => {
@@ -36,11 +61,13 @@ const Surface = (
36
61
  };
37
62
  }, [backgroundColor, elevation, style]);
38
63
 
64
+ const Component = asChild ? Slot : AnimatedView;
65
+
39
66
  return (
40
67
  <BackgroundContextWrapper backgroundColor={backgroundColor!}>
41
- <View ref={ref} {...props} testID={testID} style={surfaceStyle}>
68
+ <Component ref={ref} {...props} testID={testID} style={surfaceStyle}>
42
69
  {children}
43
- </View>
70
+ </Component>
44
71
  </BackgroundContextWrapper>
45
72
  );
46
73
  };
@@ -72,10 +72,48 @@ export const getStyleForShadowLayer = (
72
72
  };
73
73
  };
74
74
 
75
- export const getElevationAndroid = (
76
- elevation: number,
77
- _inputRange: number[],
78
- elevationLevel: number[],
79
- ) => {
80
- return elevationLevel[elevation];
75
+ /**
76
+ * Combines the two shadow layers into a single shadow style.
77
+ * This approximates the two-layer shadow effect using a single shadow.
78
+ */
79
+ export const getCombinedShadowStyle = (elevation: number, shadowColor = _shadowColor) => {
80
+ if (elevation === 0) {
81
+ return {
82
+ shadowColor,
83
+ shadowOpacity: 0,
84
+ shadowOffset: { width: 0, height: 0 },
85
+ shadowRadius: 0,
86
+ };
87
+ }
88
+
89
+ const layer0 = iOSShadowOutputRanges[0];
90
+ const layer1 = iOSShadowOutputRanges[1];
91
+
92
+ // Use the larger shadow offset (from layer 0)
93
+ const shadowOffsetHeight = layer0.height[elevation];
94
+
95
+ // Use the larger shadow radius (from layer 0)
96
+ const shadowRadius = layer0.shadowRadius[elevation];
97
+
98
+ // Combine opacities (additive, capped at 1.0)
99
+ // This approximates the visual effect of two overlapping shadows
100
+ const shadowOpacity = Math.min(1.0, layer0.shadowOpacity + layer1.shadowOpacity);
101
+
102
+ return {
103
+ shadowColor,
104
+ shadowOpacity,
105
+ shadowOffset: {
106
+ width: 0,
107
+ height: shadowOffsetHeight,
108
+ },
109
+ shadowRadius,
110
+ };
81
111
  };
112
+
113
+ // export const getElevationAndroid = (
114
+ // elevation: number,
115
+ // _inputRange: number[],
116
+ // elevationLevel: number[],
117
+ // ) => {
118
+ // return elevationLevel[elevation];
119
+ // };
@@ -1,3 +1,4 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
1
2
  import { forwardRef, memo } from 'react';
2
3
  import {
3
4
  Switch as NativeSwitch,
@@ -6,7 +7,6 @@ import {
6
7
  type ViewStyle,
7
8
  } from 'react-native';
8
9
 
9
- import { useControlledValue } from '../../hooks';
10
10
  import type { IconType } from '../Icon';
11
11
 
12
12
  export type Props = SwitchProps & {
@@ -1,3 +1,4 @@
1
+ import { useControlledValue, useLatest } from '@react-native-molecules/utils/hooks';
1
2
  import { forwardRef, memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
3
  import {
3
4
  Animated,
@@ -9,7 +10,7 @@ import {
9
10
  type ViewStyle,
10
11
  } from 'react-native';
11
12
 
12
- import { useActionState, useControlledValue, useLatest } from '../../hooks';
13
+ import { useActionState } from '../../hooks';
13
14
  import { resolveStateVariant } from '../../utils';
14
15
  import { Icon, type IconType } from '../Icon';
15
16
  import { switchStyles, useSwitchColors } from './utils';
@@ -70,6 +71,7 @@ const Switch = (
70
71
  ref,
71
72
  actionsToListen: ['focus', 'hover', 'press'],
72
73
  });
74
+ const isFirstRender = useRef(true);
73
75
 
74
76
  const [value, onChange] = useControlledValue({
75
77
  value: valueProp,
@@ -98,8 +100,8 @@ const Switch = (
98
100
  state,
99
101
  });
100
102
 
101
- const toggleMarginAnimation = useRef(new Animated.Value(value ? 0 : 1)).current;
102
- const toggleSizeAnimation = useRef(new Animated.Value(value ? 0 : 1)).current;
103
+ const toggleMarginAnimation = useRef(new Animated.Value(value ? 1 : 0)).current;
104
+ const toggleSizeAnimation = useRef(new Animated.Value(value ? 1 : 0)).current;
103
105
 
104
106
  const thumbPosition = toggleMarginAnimation.interpolate({
105
107
  inputRange: [0, 1],
@@ -130,6 +132,11 @@ const Switch = (
130
132
  });
131
133
 
132
134
  useEffect(() => {
135
+ if (isFirstRender.current) {
136
+ isFirstRender.current = false;
137
+ return;
138
+ }
139
+
133
140
  Animated.timing(toggleMarginAnimation, {
134
141
  toValue: value ? 1 : 0,
135
142
  duration: 300,
@@ -1,29 +1,20 @@
1
- import {
2
- Children,
3
- cloneElement,
4
- forwardRef,
5
- isValidElement,
6
- type JSXElementConstructor,
7
- memo,
8
- type ReactElement,
9
- useCallback,
10
- useMemo,
11
- useRef,
12
- useState,
13
- } from 'react';
1
+ import { forwardRef, memo, type ReactNode, useCallback, useMemo, useRef, useState } from 'react';
14
2
  import { type LayoutChangeEvent, View, type ViewProps, type ViewStyle } from 'react-native';
15
3
 
16
4
  import { useActionState } from '../../hooks';
17
5
  import { resolveStateVariant } from '../../utils';
18
6
  import { StateLayer } from '../StateLayer';
19
7
  import { TouchableRipple, type TouchableRippleProps } from '../TouchableRipple';
20
- import { tabsItemStyles } from './utils';
8
+ import { TabItemContext, tabsItemStyles } from './utils';
21
9
 
22
- export type TabItemProps = Omit<TouchableRippleProps, 'children' | 'ref'> & {
10
+ export type TabItemProps<T extends string | number> = Omit<
11
+ TouchableRippleProps,
12
+ 'children' | 'ref'
13
+ > & {
23
14
  /**
24
15
  * name of the tab. This should be unique like a route name
25
16
  * */
26
- name: string;
17
+ name: T;
27
18
  /**
28
19
  * Allows to define if TabItem is active.
29
20
  * */
@@ -37,22 +28,11 @@ export type TabItemProps = Omit<TouchableRippleProps, 'children' | 'ref'> & {
37
28
  contentsContainerProps?: Omit<ViewProps, 'children' | 'style' | 'onLayout'>;
38
29
  onLayoutContent?: (e: LayoutChangeEvent) => void;
39
30
  accessibilityLabel?: string;
40
- children: ReactElement<
41
- {
42
- active: boolean;
43
- hovered: boolean;
44
- variant: 'primary' | 'secondary';
45
- },
46
- JSXElementConstructor<{
47
- active: boolean;
48
- hovered: boolean;
49
- variant: 'primary' | 'secondary';
50
- }>
51
- >;
31
+ children: ReactNode;
52
32
  stateLayerProps?: ViewProps;
53
33
  };
54
34
 
55
- const TabItem = (
35
+ const TabItem = <T extends string | number>(
56
36
  {
57
37
  active = false,
58
38
  variant = 'primary',
@@ -65,7 +45,7 @@ const TabItem = (
65
45
  children,
66
46
  stateLayerProps,
67
47
  ...rest
68
- }: TabItemProps,
48
+ }: TabItemProps<T>,
69
49
  ref: any,
70
50
  ) => {
71
51
  const { hovered, actionsRef } = useActionState({ ref, actionsToListen: ['hover'] });
@@ -110,36 +90,33 @@ const TabItem = (
110
90
  [active, accessibilityLabel, state],
111
91
  );
112
92
 
93
+ const contextValue = useMemo(() => ({ active, hovered, variant }), [active, hovered, variant]);
94
+
113
95
  return (
114
- <TouchableRipple
115
- style={containerStyle}
116
- accessibilityRole="tab"
117
- accessibilityState={accessibilityState}
118
- accessibilityValue={accessibilityValue}
119
- {...rest}
120
- ref={actionsRef}
121
- onLayout={onLayout}>
122
- <>
123
- <View
124
- style={[tabsItemStyles.contentsContainer, contentsContainerStyleProp]}
125
- {...contentsContainerProps}
126
- onLayout={onLayoutHandled}>
127
- {Children.map(children, child => {
128
- if (!isValidElement(child)) return null;
129
- return cloneElement(child, {
130
- active,
131
- hovered,
132
- variant,
133
- });
134
- })}
135
- </View>
96
+ <TabItemContext.Provider value={contextValue}>
97
+ <TouchableRipple
98
+ style={containerStyle}
99
+ accessibilityRole="tab"
100
+ accessibilityState={accessibilityState}
101
+ accessibilityValue={accessibilityValue}
102
+ {...rest}
103
+ ref={actionsRef}
104
+ onLayout={onLayout}>
105
+ <>
106
+ <View
107
+ style={[tabsItemStyles.contentsContainer, contentsContainerStyleProp]}
108
+ {...contentsContainerProps}
109
+ onLayout={onLayoutHandled}>
110
+ {children}
111
+ </View>
136
112
 
137
- <StateLayer
138
- {...stateLayerProps}
139
- style={[tabsItemStyles.stateLayer, stateLayerProps?.style]}
140
- />
141
- </>
142
- </TouchableRipple>
113
+ <StateLayer
114
+ {...stateLayerProps}
115
+ style={[tabsItemStyles.stateLayer, stateLayerProps?.style]}
116
+ />
117
+ </>
118
+ </TouchableRipple>
119
+ </TabItemContext.Provider>
143
120
  );
144
121
  };
145
122
 
@@ -1,9 +1,9 @@
1
- import { type FC, memo, useMemo } from 'react';
1
+ import { memo, useContext, useMemo } from 'react';
2
2
  import { type TextProps, type TextStyle } from 'react-native';
3
3
 
4
4
  import { Icon, type IconProps, type IconType } from '../Icon';
5
5
  import { Text } from '../Text';
6
- import { tabsLabelStyles } from './utils';
6
+ import { TabItemContext, tabsLabelStyles } from './utils';
7
7
 
8
8
  const DEFAULT_ICON_SIZE = 24;
9
9
 
@@ -23,11 +23,6 @@ export type TabLabelProps = {
23
23
  iconStyle?: TextStyle;
24
24
 
25
25
  activeColor?: string;
26
-
27
- active: boolean;
28
- hovered: boolean;
29
-
30
- variant: 'primary' | 'secondary';
31
26
  };
32
27
 
33
28
  const TabLabel = memo((props: TabLabelProps) => {
@@ -40,9 +35,10 @@ const TabLabel = memo((props: TabLabelProps) => {
40
35
  labelStyle: labelStyleProp,
41
36
  labelProps,
42
37
  label,
43
- active,
44
38
  } = props;
45
39
 
40
+ const { active } = useContext(TabItemContext);
41
+
46
42
  // tabsLabelStyles.useVariants({
47
43
  // variant,
48
44
  // state: resolveStateVariant({
@@ -81,4 +77,4 @@ const TabLabel = memo((props: TabLabelProps) => {
81
77
 
82
78
  TabLabel.displayName = 'TabLabel';
83
79
 
84
- export default TabLabel as unknown as FC<Omit<TabLabelProps, 'active' | 'hovered' | 'variant'>>;
80
+ export default TabLabel;