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,90 @@
1
+ import { useCallback, useEffect, useLayoutEffect } from 'react';
2
+ import { AppState, Dimensions, Platform } from 'react-native';
3
+
4
+ import { popoverDefaultStyles } from './common';
5
+ import type { UsePlatformMeasureArgs, UsePlatformMeasureResult } from './usePlatformMeasure';
6
+
7
+ export const usePlatformMeasure = ({
8
+ triggerRef,
9
+ isOpen,
10
+ calculatedPosition,
11
+ calculateAndSetPosition,
12
+ targetLayoutRef,
13
+ triggerDimensions,
14
+ }: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
15
+ const measureTarget = useCallback(() => {
16
+ if (triggerRef?.current) {
17
+ triggerRef.current.measure(
18
+ (
19
+ _fx: number,
20
+ _fy: number,
21
+ width: number,
22
+ height: number,
23
+ px: number,
24
+ py: number,
25
+ ) => {
26
+ if (width !== 0 || height !== 0) {
27
+ const newLayout = { x: px, y: py, width, height };
28
+ const changed =
29
+ !targetLayoutRef.current ||
30
+ targetLayoutRef.current.x !== newLayout.x ||
31
+ targetLayoutRef.current.y !== newLayout.y ||
32
+ targetLayoutRef.current.width !== newLayout.width ||
33
+ targetLayoutRef.current.height !== newLayout.height;
34
+
35
+ if (changed) {
36
+ targetLayoutRef.current = newLayout;
37
+ calculateAndSetPosition();
38
+ }
39
+ } else {
40
+ targetLayoutRef.current = null;
41
+ calculateAndSetPosition();
42
+ }
43
+ },
44
+ () => {
45
+ console.error('Failed to measure target element for Popover.');
46
+ targetLayoutRef.current = null;
47
+ calculateAndSetPosition();
48
+ },
49
+ );
50
+ } else {
51
+ targetLayoutRef.current = null;
52
+ calculateAndSetPosition();
53
+ }
54
+ }, [triggerRef, calculateAndSetPosition, targetLayoutRef]);
55
+
56
+ useLayoutEffect(() => {
57
+ if (isOpen) {
58
+ measureTarget();
59
+ }
60
+ }, [isOpen, measureTarget, triggerDimensions]);
61
+
62
+ useEffect(() => {
63
+ if (!isOpen) return;
64
+ const subscription = Dimensions.addEventListener('change', measureTarget);
65
+ return () => {
66
+ if (typeof subscription?.remove === 'function') {
67
+ subscription.remove();
68
+ }
69
+ };
70
+ }, [isOpen, measureTarget]);
71
+
72
+ useEffect(() => {
73
+ if (!isOpen || Platform.OS === 'web') return;
74
+ const handleAppStateChange = (nextAppState: string) => {
75
+ if (nextAppState === 'active') {
76
+ setTimeout(measureTarget, 50);
77
+ }
78
+ };
79
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
80
+ return () => {
81
+ if (typeof subscription?.remove === 'function') {
82
+ subscription.remove();
83
+ }
84
+ };
85
+ }, [isOpen, measureTarget]);
86
+
87
+ return {
88
+ popoverStyle: (calculatedPosition ?? popoverDefaultStyles) as any,
89
+ };
90
+ };
@@ -0,0 +1,120 @@
1
+ import { type RefObject, useCallback, useEffect, useLayoutEffect, useMemo } from 'react';
2
+ import type { LayoutRectangle, View, ViewStyle } from 'react-native';
3
+
4
+ import { popoverDefaultStyles } from './common';
5
+
6
+ export type UsePlatformMeasureArgs = {
7
+ triggerRef: RefObject<View | any> | undefined;
8
+ isOpen: boolean;
9
+ onClose?: () => void;
10
+ dismissOnClickOutside?: boolean;
11
+ calculatedPosition: ViewStyle | null;
12
+ calculateAndSetPosition: () => void;
13
+ targetLayoutRef: RefObject<LayoutRectangle | null>;
14
+ popoverRef: RefObject<View | null>;
15
+ triggerDimensions?: { width: number; height: number } | null;
16
+ };
17
+
18
+ export type UsePlatformMeasureResult = {
19
+ /** Platform-adjusted popover position (includes scroll offset on web) */
20
+ popoverStyle: ViewStyle;
21
+ };
22
+
23
+ export const usePlatformMeasure = ({
24
+ triggerRef,
25
+ isOpen,
26
+ onClose,
27
+ dismissOnClickOutside = true,
28
+ calculatedPosition,
29
+ calculateAndSetPosition,
30
+ targetLayoutRef,
31
+ popoverRef,
32
+ triggerDimensions,
33
+ }: UsePlatformMeasureArgs): UsePlatformMeasureResult => {
34
+ const measureTarget = useCallback(() => {
35
+ if (triggerRef?.current) {
36
+ triggerRef.current.measureInWindow(
37
+ (x: number, y: number, width: number, height: number) => {
38
+ if (width !== 0 || height !== 0) {
39
+ const newLayout = { x, y, width, height };
40
+ const changed =
41
+ !targetLayoutRef.current ||
42
+ targetLayoutRef.current.x !== newLayout.x ||
43
+ targetLayoutRef.current.y !== newLayout.y ||
44
+ targetLayoutRef.current.width !== newLayout.width ||
45
+ targetLayoutRef.current.height !== newLayout.height;
46
+
47
+ if (changed) {
48
+ targetLayoutRef.current = newLayout;
49
+ calculateAndSetPosition();
50
+ }
51
+ } else {
52
+ targetLayoutRef.current = null;
53
+ calculateAndSetPosition();
54
+ }
55
+ },
56
+ );
57
+ } else {
58
+ targetLayoutRef.current = null;
59
+ calculateAndSetPosition();
60
+ }
61
+ }, [triggerRef, calculateAndSetPosition, targetLayoutRef]);
62
+
63
+ useLayoutEffect(() => {
64
+ if (isOpen) {
65
+ const timeoutId = setTimeout(measureTarget, 0);
66
+ return () => clearTimeout(timeoutId);
67
+ }
68
+ return;
69
+ }, [isOpen, measureTarget, triggerDimensions]);
70
+
71
+ useLayoutEffect(() => {
72
+ if (!isOpen) return;
73
+ const handleResize = () => {
74
+ if (triggerRef?.current && isOpen) {
75
+ window.requestAnimationFrame(measureTarget);
76
+ }
77
+ };
78
+ window.addEventListener('resize', handleResize);
79
+ window.addEventListener('scroll', handleResize, true);
80
+ return () => {
81
+ window.removeEventListener('resize', handleResize);
82
+ window.removeEventListener('scroll', handleResize, true);
83
+ };
84
+ }, [isOpen, measureTarget, triggerRef]);
85
+
86
+ useEffect(() => {
87
+ if (!isOpen || !onClose || !dismissOnClickOutside) return;
88
+ const handleClickOutside = (event: MouseEvent) => {
89
+ const popoverElement = popoverRef.current as any as HTMLElement;
90
+ const targetElement = triggerRef?.current as any as HTMLElement;
91
+ if (
92
+ popoverElement &&
93
+ !popoverElement.contains(event.target as Node) &&
94
+ targetElement &&
95
+ !targetElement.contains(event.target as Node)
96
+ ) {
97
+ onClose();
98
+ }
99
+ };
100
+ document.addEventListener('mousedown', handleClickOutside, { capture: true });
101
+ return () => {
102
+ document.removeEventListener('mousedown', handleClickOutside, { capture: true });
103
+ };
104
+ }, [dismissOnClickOutside, isOpen, onClose, popoverRef, triggerRef]);
105
+
106
+ const popoverStyle = useMemo(() => {
107
+ if (!calculatedPosition) return popoverDefaultStyles;
108
+
109
+ const scrollX = window.scrollX ?? window.pageXOffset ?? 0;
110
+ const scrollY = window.scrollY ?? window.pageYOffset ?? 0;
111
+
112
+ return {
113
+ ...calculatedPosition,
114
+ left: (calculatedPosition.left as number) + scrollX,
115
+ top: (calculatedPosition.top as number) + scrollY,
116
+ };
117
+ }, [calculatedPosition]);
118
+
119
+ return { popoverStyle };
120
+ };
@@ -0,0 +1,34 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
+ import { popoverDefaultStyles } from './common';
5
+
6
+ const popoverStylesDefault = StyleSheet.create(theme => ({
7
+ popoverContainer: {
8
+ ...popoverDefaultStyles,
9
+ backgroundColor: theme.colors.surface,
10
+ borderRadius: 4,
11
+ shadowColor: 'rgba(0, 0, 0, 1)',
12
+ shadowOffset: { width: 0, height: 2 },
13
+ shadowOpacity: theme.dark ? 0.7 : 0.3,
14
+ shadowRadius: 10,
15
+ elevation: 5,
16
+ zIndex: 100,
17
+ },
18
+ overlay: {
19
+ position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ right: 0,
23
+ bottom: 0,
24
+ backgroundColor: 'transparent',
25
+ _web: {
26
+ cursor: 'default',
27
+ },
28
+ },
29
+ }));
30
+
31
+ export const popoverStyles = getRegisteredComponentStylesWithFallback(
32
+ 'Popover',
33
+ popoverStylesDefault,
34
+ );
@@ -1,8 +1,7 @@
1
1
  import { Portal as GorhomPortal } from '@gorhom/portal';
2
+ import { createContextBridge } from '@react-native-molecules/utils/context-bridge';
2
3
  import { type ComponentType, type ReactNode } from 'react';
3
4
 
4
- import { createContextBridge } from '../../context-bridge';
5
-
6
5
  const { BridgedComponent: Portal, registerContextToBridge: registerPortalContext } =
7
6
  createContextBridge<Omit<any, 'children'> & { children: ReactNode }>(
8
7
  'portal-context',
@@ -0,0 +1,188 @@
1
+ import { useControlledValue } from '@react-native-molecules/utils/hooks';
2
+ import { forwardRef, memo, useCallback, useContext, useId, useMemo } from 'react';
3
+ import { View } from 'react-native';
4
+
5
+ import { resolveStateVariant } from '../../utils';
6
+ import { Text } from '../Text';
7
+ import { RadioGroupContext, RadioItemContext } from './context';
8
+ import RadioBase from './RadioBase';
9
+ import type { RadioGroupProps, RadioLabelProps, RadioProps, RadioRowProps } from './types';
10
+ import { radioRowStyles } from './utils';
11
+
12
+ /**
13
+ * The radio control (the circle). Use inside a RadioRow, or standalone under a RadioGroup with `value`.
14
+ */
15
+ const Radio = (
16
+ {
17
+ value,
18
+ disabled: disabledProp,
19
+ size: sizeProp,
20
+ color,
21
+ uncheckedColor,
22
+ stateLayerProps,
23
+ testID,
24
+ ...rest
25
+ }: RadioProps,
26
+ ref: any,
27
+ ) => {
28
+ const item = useContext(RadioItemContext);
29
+ const group = useContext(RadioGroupContext);
30
+
31
+ // Inside a RadioRow the item context drives state; standalone (bare) mode uses group context.
32
+ const checked = item ? item.checked : group?.value === value;
33
+ const disabled = disabledProp ?? item?.disabled ?? group?.disabled;
34
+ const size = sizeProp ?? item?.size ?? group?.size ?? 'md';
35
+ const labelId = item?.labelId;
36
+
37
+ const onPress = useCallback(() => {
38
+ if (disabled) return;
39
+ if (item) {
40
+ item.onSelect();
41
+ } else if (value !== undefined) {
42
+ group?.onChange(value);
43
+ }
44
+ }, [disabled, item, group, value]);
45
+
46
+ const accessibilityState = useMemo(
47
+ () => ({ checked: !!checked, disabled: !!disabled }),
48
+ [checked, disabled],
49
+ );
50
+
51
+ return (
52
+ <RadioBase
53
+ {...rest}
54
+ ref={ref}
55
+ checked={!!checked}
56
+ disabled={disabled}
57
+ size={size}
58
+ color={color}
59
+ uncheckedColor={uncheckedColor}
60
+ onPress={onPress}
61
+ stateLayerProps={stateLayerProps}
62
+ testID={testID}
63
+ accessibilityRole="radio"
64
+ accessibilityState={accessibilityState}
65
+ accessibilityLabelledBy={labelId}
66
+ />
67
+ );
68
+ };
69
+
70
+ /**
71
+ * The label for a RadioRow. Pressing it selects the row's value, and it is wired to the control via
72
+ * `nativeID` / `accessibilityLabelledBy` (web `id` / `aria-labelledby`).
73
+ */
74
+ export const RadioLabel = memo(({ children, style, ...rest }: RadioLabelProps) => {
75
+ const item = useContext(RadioItemContext);
76
+
77
+ const state = resolveStateVariant({
78
+ disabled: !!item?.disabled,
79
+ checked: !!item?.checked,
80
+ });
81
+
82
+ radioRowStyles.useVariants({ state: state as any });
83
+
84
+ if (!item) {
85
+ return (
86
+ <Text style={style} {...rest}>
87
+ {children}
88
+ </Text>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <Text
94
+ nativeID={item.labelId}
95
+ onPress={item.disabled ? undefined : item.onSelect}
96
+ disabled={item.disabled}
97
+ selectable={false}
98
+ style={[radioRowStyles.label, style]}
99
+ {...rest}>
100
+ {children}
101
+ </Text>
102
+ );
103
+ });
104
+
105
+ RadioLabel.displayName = 'Radio_Label';
106
+
107
+ /**
108
+ * A row that binds a value to a Radio control and its Radio.Label. The row itself is not pressable —
109
+ * only the Radio and Radio.Label inside it are. Children may be in any order.
110
+ */
111
+ export const RadioRow = memo(
112
+ forwardRef(
113
+ ({ value, disabled: disabledProp, style, children, ...rest }: RadioRowProps, ref: any) => {
114
+ const group = useContext(RadioGroupContext);
115
+ const labelId = useId();
116
+
117
+ const disabled = disabledProp ?? group?.disabled;
118
+ const checked = group?.value === value;
119
+
120
+ const onSelect = useCallback(() => {
121
+ if (disabled) return;
122
+ group?.onChange(value);
123
+ }, [disabled, group, value]);
124
+
125
+ const contextValue = useMemo(
126
+ () => ({ value, checked, onSelect, disabled, size: group?.size, labelId }),
127
+ [value, checked, onSelect, disabled, group?.size, labelId],
128
+ );
129
+
130
+ return (
131
+ <RadioItemContext.Provider value={contextValue}>
132
+ <View ref={ref} style={[radioRowStyles.row, style]} {...rest}>
133
+ {children}
134
+ </View>
135
+ </RadioItemContext.Provider>
136
+ );
137
+ },
138
+ ),
139
+ );
140
+
141
+ RadioRow.displayName = 'Radio_Row';
142
+
143
+ /**
144
+ * Controls a group of radios, holding the selected value.
145
+ *
146
+ * ```tsx
147
+ * <RadioGroup value={value} onChange={setValue}>
148
+ * <RadioRow value="first">
149
+ * <Radio />
150
+ * <Radio.Label>First option</Radio.Label>
151
+ * </RadioRow>
152
+ * </RadioGroup>
153
+ * ```
154
+ */
155
+ export const RadioGroup = memo(
156
+ ({
157
+ value: valueProp,
158
+ defaultValue,
159
+ onChange,
160
+ disabled,
161
+ size,
162
+ children,
163
+ ...rest
164
+ }: RadioGroupProps) => {
165
+ const [value, setValue] = useControlledValue({
166
+ value: valueProp,
167
+ defaultValue,
168
+ onChange,
169
+ });
170
+
171
+ const contextValue = useMemo(
172
+ () => ({ value, onChange: setValue, disabled, size }),
173
+ [value, setValue, disabled, size],
174
+ );
175
+
176
+ return (
177
+ <RadioGroupContext.Provider value={contextValue}>
178
+ <View accessibilityRole="radiogroup" {...rest}>
179
+ {children}
180
+ </View>
181
+ </RadioGroupContext.Provider>
182
+ );
183
+ },
184
+ );
185
+
186
+ RadioGroup.displayName = 'Radio_Group';
187
+
188
+ export default memo(forwardRef(Radio));
@@ -0,0 +1,69 @@
1
+ import { forwardRef, memo, useMemo } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { resolveStateVariant } from '../../utils';
5
+ import { tokenStylesParser } from '../../utils/tokenStylesParser';
6
+ import { Icon } from '../Icon';
7
+ import { TouchableRipple } from '../TouchableRipple';
8
+ import type { RadioBaseProps } from './types';
9
+ import { iconSizeMap, radioStyles } from './utils';
10
+
11
+ const RadioBaseIOS = (
12
+ {
13
+ disabled = false,
14
+ size = 'md',
15
+ style,
16
+ color: colorProp,
17
+ checked,
18
+ onPress,
19
+ uncheckedColor: uncheckedColorProp,
20
+ testID,
21
+ ...rest
22
+ }: RadioBaseProps,
23
+ ref: any,
24
+ ) => {
25
+ const state = resolveStateVariant({
26
+ disabled,
27
+ checked,
28
+ });
29
+
30
+ radioStyles.useVariants({
31
+ state: state as any,
32
+ size: size as any,
33
+ });
34
+
35
+ const { containerStyle, iconContainerStyle, iconStyle } = useMemo(() => {
36
+ const _color = tokenStylesParser.getColor(checked ? colorProp : uncheckedColorProp);
37
+
38
+ return {
39
+ containerStyle: [radioStyles.container, radioStyles.root, style],
40
+ iconContainerStyle: { opacity: checked ? 1 : 0 },
41
+ iconStyle: [radioStyles.icon, _color],
42
+ };
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, [checked, colorProp, style, state, size, uncheckedColorProp]);
45
+
46
+ return (
47
+ <TouchableRipple
48
+ {...rest}
49
+ ref={ref}
50
+ onPress={onPress}
51
+ disabled={disabled}
52
+ borderless
53
+ style={containerStyle}
54
+ testID={testID}>
55
+ <View style={iconContainerStyle}>
56
+ <Icon
57
+ allowFontScaling={false}
58
+ name="check"
59
+ size={iconSizeMap[size]}
60
+ style={iconStyle}
61
+ />
62
+ </View>
63
+ </TouchableRipple>
64
+ );
65
+ };
66
+
67
+ RadioBaseIOS.displayName = 'Radio_Base';
68
+
69
+ export default memo(forwardRef(RadioBaseIOS));
@@ -0,0 +1,136 @@
1
+ import { forwardRef, memo, useEffect, useMemo, useRef } from 'react';
2
+ import { Animated, StyleSheet, View } from 'react-native';
3
+
4
+ import { useActionState } from '../../hooks';
5
+ import { resolveStateVariant } from '../../utils';
6
+ import { tokenStylesParser } from '../../utils/tokenStylesParser';
7
+ import { StateLayer } from '../StateLayer';
8
+ import { TouchableRipple } from '../TouchableRipple';
9
+ import type { RadioBaseProps } from './types';
10
+ import { ANIMATION_DURATION, radioStyles } from './utils';
11
+
12
+ const BORDER_WIDTH = 2;
13
+
14
+ const RadioBaseAndroid = (
15
+ {
16
+ disabled = false,
17
+ size = 'md',
18
+ testID,
19
+ color: colorProp,
20
+ uncheckedColor: uncheckedColorProp,
21
+ style,
22
+ checked,
23
+ onPress,
24
+ stateLayerProps = {},
25
+ ...rest
26
+ }: RadioBaseProps,
27
+ ref: any,
28
+ ) => {
29
+ const { actionsRef, hovered } = useActionState({ ref, actionsToListen: ['hover'] });
30
+ const { current: borderAnim } = useRef<Animated.Value>(new Animated.Value(BORDER_WIDTH));
31
+ const { current: radioAnim } = useRef<Animated.Value>(new Animated.Value(1));
32
+ const isFirstRendering = useRef<boolean>(true);
33
+
34
+ const state = resolveStateVariant({
35
+ disabled,
36
+ checkedAndHovered: checked && hovered,
37
+ checked,
38
+ hovered,
39
+ });
40
+
41
+ radioStyles.useVariants({
42
+ state: state as any,
43
+ size: size as any,
44
+ });
45
+
46
+ const { containerStyles, radioStyle, dotStyles, dotContainerStyles, stateLayerStyle } =
47
+ useMemo(() => {
48
+ return {
49
+ containerStyles: [radioStyles.container, radioStyles.root, style],
50
+ radioStyle: [
51
+ radioStyles.radio,
52
+ { borderWidth: borderAnim },
53
+ tokenStylesParser.getColor(
54
+ checked ? colorProp : uncheckedColorProp,
55
+ 'borderColor',
56
+ ),
57
+ ],
58
+ dotContainerStyles: [StyleSheet.absoluteFill, radioStyles.radioContainer],
59
+ dotStyles: [
60
+ radioStyles.dot,
61
+ { transform: [{ scale: radioAnim }] },
62
+ tokenStylesParser.getColor(
63
+ checked ? colorProp : uncheckedColorProp,
64
+ 'backgroundColor',
65
+ ),
66
+ ],
67
+ stateLayerStyle: [radioStyles.stateLayer, stateLayerProps?.style],
68
+ };
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [
71
+ borderAnim,
72
+ checked,
73
+ colorProp,
74
+ radioAnim,
75
+ stateLayerProps?.style,
76
+ uncheckedColorProp,
77
+ style,
78
+ state,
79
+ size,
80
+ ]);
81
+
82
+ useEffect(() => {
83
+ // Do not run animation on the very first render.
84
+ if (isFirstRendering.current) {
85
+ isFirstRendering.current = false;
86
+ return;
87
+ }
88
+
89
+ if (checked) {
90
+ radioAnim.setValue(1.2);
91
+ Animated.timing(radioAnim, {
92
+ toValue: 1,
93
+ duration: ANIMATION_DURATION,
94
+ useNativeDriver: true,
95
+ }).start();
96
+ } else {
97
+ borderAnim.setValue(10);
98
+ Animated.timing(borderAnim, {
99
+ toValue: BORDER_WIDTH,
100
+ duration: ANIMATION_DURATION,
101
+ useNativeDriver: false,
102
+ }).start();
103
+ }
104
+ }, [checked, borderAnim, radioAnim]);
105
+
106
+ return (
107
+ <TouchableRipple
108
+ {...rest}
109
+ ref={actionsRef}
110
+ onPress={onPress}
111
+ disabled={disabled}
112
+ borderless
113
+ style={containerStyles}
114
+ testID={testID}>
115
+ <>
116
+ <Animated.View style={radioStyle}>
117
+ {checked ? (
118
+ <View style={dotContainerStyles}>
119
+ <Animated.View style={dotStyles} />
120
+ </View>
121
+ ) : null}
122
+ </Animated.View>
123
+
124
+ <StateLayer
125
+ testID={testID ? `${testID}-stateLayer` : ''}
126
+ {...stateLayerProps}
127
+ style={stateLayerStyle}
128
+ />
129
+ </>
130
+ </TouchableRipple>
131
+ );
132
+ };
133
+
134
+ RadioBaseAndroid.displayName = 'Radio_Base';
135
+
136
+ export default memo(forwardRef(RadioBaseAndroid));
@@ -0,0 +1,23 @@
1
+ import { createContext } from 'react';
2
+
3
+ import type { Size } from './types';
4
+
5
+ export type RadioGroupContextType = {
6
+ value?: string;
7
+ onChange: (value: string) => void;
8
+ disabled?: boolean;
9
+ size?: Size;
10
+ };
11
+
12
+ export const RadioGroupContext = createContext<RadioGroupContextType | null>(null);
13
+
14
+ export type RadioItemContextType = {
15
+ value: string;
16
+ checked: boolean;
17
+ onSelect: () => void;
18
+ disabled?: boolean;
19
+ size?: Size;
20
+ labelId: string;
21
+ };
22
+
23
+ export const RadioItemContext = createContext<RadioItemContextType | null>(null);
@@ -0,0 +1,20 @@
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
+ // @component ./Radio.tsx
3
+ import RadioControl, {
4
+ RadioGroup as RadioGroupComponent,
5
+ RadioLabel,
6
+ RadioRow as RadioRowComponent,
7
+ } from './Radio';
8
+
9
+ const RadioDefault = Object.assign(RadioControl, {
10
+ Label: RadioLabel,
11
+ Group: RadioGroupComponent,
12
+ Row: RadioRowComponent,
13
+ });
14
+
15
+ export const Radio = getRegisteredComponentWithFallback('Radio', RadioDefault);
16
+ export const RadioGroup = RadioGroupComponent;
17
+ export const RadioRow = RadioRowComponent;
18
+
19
+ export type { RadioGroupProps, RadioLabelProps, RadioProps, RadioRowProps } from './types';
20
+ export { radioRowStyles, radioStyles } from './utils';