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,9 @@
1
+ import { useControlledValue, useLatest } from '@react-native-molecules/utils/hooks';
1
2
  import React, {
2
- forwardRef,
3
3
  memo,
4
4
  type PropsWithoutRef,
5
5
  type ReactNode,
6
- type RefObject,
6
+ type Ref,
7
7
  useCallback,
8
8
  useContext,
9
9
  useEffect,
@@ -14,28 +14,52 @@ import React, {
14
14
  } from 'react';
15
15
  import type {
16
16
  BlurEvent,
17
+ ColorValue,
17
18
  FocusEvent,
19
+ GestureResponderEvent,
18
20
  LayoutChangeEvent,
19
21
  StyleProp,
20
- TextInputProps,
21
- TextStyle,
22
+ TextInputProps as NativeTextInputProps,
22
23
  ViewProps,
23
24
  ViewStyle,
24
25
  } from 'react-native';
25
- import { Animated, I18nManager, Platform, TextInput as NativeTextInput, View } from 'react-native';
26
- import { StyleSheet } from 'react-native-unistyles';
27
-
28
- import { useActionState } from '../../hooks/useActionState';
29
- import useControlledValue from '../../hooks/useControlledValue';
30
- import useLatest from '../../hooks/useLatest';
31
- import type { WithElements } from '../../types';
32
- import { BackgroundContext } from '../../utils';
26
+ import {
27
+ Animated,
28
+ I18nManager,
29
+ Platform,
30
+ Pressable,
31
+ TextInput as NativeTextInput,
32
+ View,
33
+ } from 'react-native';
34
+
35
+ import { useActionState } from '../../hooks';
33
36
  import { createSyntheticEvent, resolveStateVariant } from '../../utils';
37
+ import { extractSubcomponents } from '../../utils/extractSubcomponents';
34
38
  import { HelperText } from '../HelperText';
39
+ import { Icon } from '../Icon';
35
40
  import { StateLayer } from '../StateLayer';
36
41
  import InputLabel from './InputLabel';
37
- import type { RenderProps, TextInputLabelProp, TextInputSize, TextInputVariant } from './types';
38
- import { getInputMinHeight, styles } from './utils';
42
+ import type {
43
+ RenderProps,
44
+ TextInputElementCompoundProps,
45
+ TextInputIconCompoundProps,
46
+ TextInputLabelCompoundProps,
47
+ TextInputOutlineCompoundProps,
48
+ TextInputSize,
49
+ TextInputSupportingTextCompoundProps,
50
+ TextInputVariant,
51
+ } from './types';
52
+ import {
53
+ getInputMinHeight,
54
+ styles,
55
+ TextInputContext,
56
+ textInputIconStyles,
57
+ textInputLabelStyles,
58
+ textInputLeftStyles,
59
+ textInputOutlineStyles,
60
+ textInputRightStyles,
61
+ textInputSupportingTextStyles,
62
+ } from './utils';
39
63
 
40
64
  const BLUR_ANIMATION_DURATION = 180;
41
65
  const FOCUS_ANIMATION_DURATION = 150;
@@ -46,138 +70,100 @@ export type ElementProps = {
46
70
  focused: boolean;
47
71
  };
48
72
 
49
- type Element = ReactNode | ((props: ElementProps) => ReactNode);
50
-
51
- export type Props = Omit<TextInputProps, 'ref'> &
52
- WithElements<Element> & {
53
- ref?: RefObject<TextInputHandles | null>;
54
- /**
55
- * Variant of the TextInput.
56
- * - `flat` - flat input with an underline.
57
- * - `outlined` - input with an outline.
58
- *
59
- * In `outlined` variant, the background color of the label is derived from `colors?.background` in theme or the `backgroundColor` style.
60
- * This component render TextInputOutlined or TextInputFlat based on that props
61
- */
62
- variant?: TextInputVariant;
63
- /**
64
- * If true, user won't be able to interact with the component.
65
- */
66
- disabled?: boolean;
67
- /**
68
- * The text or component to use for the floating label.
69
- */
70
- label?: TextInputLabelProp;
71
- /**
72
- * Placeholder for the input.
73
- */
74
- placeholder?: string;
75
- /**
76
- * Whether to style the TextInput with error style.
77
- */
78
- error?: boolean;
79
- /**
80
- * Callback that is called when the text input's text changes. Changed text is passed as an argument to the callback handler.
81
- */
82
- onChangeText?: Function;
83
- /**
84
- * Selection color of the input
85
- */
86
- selectionColor?: string;
87
- /**
88
- * Inactive underline color of the input.
89
- */
90
- underlineColor?: string;
91
- /**
92
- * Active underline color of the input.
93
- */
94
- activeUnderlineColor?: string;
95
- /**
96
- * Inactive outline color of the input.
97
- */
98
- outlineColor?: string;
99
- /**
100
- * Active outline color of the input.
101
- */
102
- activeOutlineColor?: string;
103
- /**
104
- * Sets min height with densed layout. For `TextInput` in `flat` mode
105
- * height is `64dp` or in dense layout - `52dp` with label or `40dp` without label.
106
- * For `TextInput` in `outlined` mode
107
- * height is `56dp` or in dense layout - `40dp` regardless of label.
108
- * When you apply `height` prop in style the `dense` prop affects only `paddingVertical` inside `TextInput`
109
- */
110
- size?: TextInputSize;
111
- /**
112
- * Whether the input can have multiple lines.
113
- */
114
- multiline?: boolean;
115
- /**
116
- * The number of lines to show in the input (Android only).
117
- */
118
- numberOfLines?: number;
119
- /**
120
- * The Supporting Text below the TextInput
121
- */
122
- supportingText?: string;
123
- /**
124
- * To display the required indicator in Supporting Text and in the Label
125
- */
126
- required?: boolean;
127
- /**
128
- *
129
- * Callback to render a custom input component such as `react-native-text-input-mask`
130
- * instead of the default `TextInput` component from `react-native`.
131
- *
132
- * Example:
133
- * ```js
134
- * <TextInput
135
- * label="Phone number"
136
- * render={props =>
137
- * <TextInputMask
138
- * {...props}
139
- * mask="+[00] [000] [000] [000]"
140
- * />
141
- * }
142
- * />
143
- * ```
144
- */
145
- render?: (props: RenderProps) => ReactNode;
146
- /**
147
- * Value of the text input.
148
- */
149
- value?: string;
150
- /**
151
- * Pass `fontSize` prop to modify the font size inside `TextInput`.
152
- * Pass `height` prop to set `TextInput` height.
153
- * Pass `backgroundColor` prop to set `TextInput` backgroundColor.
154
- */
155
- style?: StyleProp<TextStyle>;
156
- /**
157
- * Style of the Input Container
158
- */
159
- inputContainerStyle?: StyleProp<ViewStyle>;
160
- /**
161
- * Style of the Input
162
- */
163
- inputStyle?: StyleProp<TextStyle>;
164
- /**
165
- * Style of the rightElement
166
- */
167
- rightElementStyle?: StyleProp<TextStyle>;
168
- /**
169
- * Style of the leftElement
170
- */
171
- leftElementStyle?: StyleProp<TextStyle>;
172
- /**
173
- * props for the stateLayer
174
- */
175
- stateLayerProps?: PropsWithoutRef<ViewProps>;
176
- /**
177
- * testID to be used on tests.
178
- */
179
- testID?: string;
180
- };
73
+ export type Props = Omit<NativeTextInputProps, 'style' | 'ref'> & {
74
+ ref?: Ref<TextInputHandles | null>;
75
+ /**
76
+ * Variant of the TextInput.
77
+ * - `flat` - flat input with an underline.
78
+ * - `outlined` - input with an outline.
79
+ * - `plain` - plain input without any decoration.
80
+ */
81
+ variant?: TextInputVariant;
82
+ /**
83
+ * Size of the TextInput.
84
+ */
85
+ size?: TextInputSize;
86
+ /**
87
+ * If true, user won't be able to interact with the component.
88
+ */
89
+ disabled?: boolean;
90
+ /**
91
+ * Whether to style the TextInput with error style.
92
+ */
93
+ error?: boolean;
94
+ /**
95
+ * Whether the input can have multiple lines.
96
+ */
97
+ multiline?: boolean;
98
+ /**
99
+ * To display the required indicator in the Label
100
+ */
101
+ required?: boolean;
102
+ /**
103
+ * Value of the text input (for controlled usage).
104
+ */
105
+ value?: string;
106
+ /**
107
+ * Default value for uncontrolled usage.
108
+ */
109
+ defaultValue?: string;
110
+ /**
111
+ * Callback that is called when the text input's text changes.
112
+ */
113
+ onChangeText?: (text: string) => void;
114
+ /**
115
+ * Callback when input is focused.
116
+ */
117
+ onFocus?: (args: FocusEvent) => void;
118
+ /**
119
+ * Callback when input loses focus.
120
+ */
121
+ onBlur?: (args: BlurEvent) => void;
122
+ /**
123
+ * Selection color of the input
124
+ */
125
+ selectionColor?: ColorValue;
126
+
127
+ /**
128
+ * Placeholder text color.
129
+ */
130
+ placeholderTextColor?: ColorValue;
131
+
132
+ /**
133
+ * If false, text is not editable.
134
+ */
135
+ editable?: boolean;
136
+ /**
137
+ * Style of the container.
138
+ */
139
+ style?: StyleProp<ViewStyle>;
140
+ /**
141
+ * Props for the container that is directly inside the context container which is horizontal layout
142
+ */
143
+ containerProps?: ViewProps;
144
+ /**
145
+ * Style of the Input Container
146
+ */
147
+ inputWrapperProps?: ViewProps;
148
+ /**
149
+ * props for the stateLayer (flat variant)
150
+ */
151
+ stateLayerProps?: PropsWithoutRef<ViewProps>;
152
+ /**
153
+ * testID to be used on tests.
154
+ */
155
+ testID?: string;
156
+ /**
157
+ * Children for composable API. Use TextInput.Label, TextInput.Left, TextInput.Right, etc.
158
+ */
159
+ children?: ReactNode;
160
+ /**
161
+ * Render custom input component.
162
+ */
163
+ render?: (props: RenderProps) => ReactNode;
164
+ inputStyle?: NativeTextInputProps['style'];
165
+ placeholder?: string;
166
+ };
181
167
 
182
168
  export type TextInputHandles = Pick<
183
169
  NativeTextInput,
@@ -191,504 +177,602 @@ const labelWiggleXOffset = 4;
191
177
 
192
178
  const DefaultComponent = (props: RenderProps) => <NativeTextInput {...props} />;
193
179
 
194
- const TextInput = forwardRef<TextInputHandles, Props>(
195
- (
196
- {
197
- variant = 'flat',
198
- size = 'md',
199
- disabled = false,
200
- error: errorProp = false,
201
- multiline = false,
202
- editable = true,
203
- required = false,
204
- maxFontSizeMultiplier = 15,
205
- supportingText,
206
- selectionColor: selectionColorProp,
207
- underlineColor: underlineColorProp,
208
- activeUnderlineColor: activeUnderlineColorProp,
209
- outlineColor: outlineColorProp,
210
- activeOutlineColor: activeOutlineColorProp,
211
- placeholderTextColor: placeholderTextColorProp,
212
- style,
213
- inputContainerStyle,
214
- inputStyle,
215
- stateLayerProps = {},
216
- left,
217
- right,
218
- render = DefaultComponent,
219
- onBlur,
220
- leftElementStyle,
221
- rightElementStyle,
222
- ...rest
223
- }: Props,
224
- ref,
225
- ) => {
226
- const { hovered, actionsRef } = useActionState({ actionsToListen: ['hover'] });
227
- const isControlled = rest.value !== undefined;
228
- const validInputValue = isControlled ? rest.value : rest.defaultValue;
229
- const floatingLabelVerticalOffset = variant === 'flat' ? 16 : 0;
180
+ const TextInput = ({
181
+ ref,
182
+ variant = 'outlined',
183
+ size = 'sm',
184
+ disabled = false,
185
+ error: errorProp = false,
186
+ multiline = false,
187
+ editable = true,
188
+ required = false,
189
+
190
+ selectionColor,
191
+ placeholderTextColor,
192
+ containerProps,
193
+ style,
194
+ inputWrapperProps,
195
+ stateLayerProps,
196
+ onBlur,
197
+ onFocus,
198
+ testID,
199
+ value: valueProp,
200
+ defaultValue,
201
+ onChangeText,
202
+ render,
203
+ placeholder,
204
+ children,
205
+ inputStyle,
206
+ ...rest
207
+ }: Props) => {
208
+ const { hovered, actionsRef } = useActionState({ actionsToListen: ['hover'] });
209
+
210
+ const {
211
+ TextInput_Label,
212
+ TextInput_Left,
213
+ TextInput_Right,
214
+ TextInput_SupportingText,
215
+ TextInput_Outline,
216
+ rest: restChildren,
217
+ } = extractSubcomponents({
218
+ children,
219
+ allowedChildren: [
220
+ { name: 'TextInput_Label', allowMultiple: false },
221
+ { name: 'TextInput_Left', allowMultiple: false },
222
+ { name: 'TextInput_Right', allowMultiple: false },
223
+ { name: 'TextInput_SupportingText', allowMultiple: false },
224
+ { name: 'TextInput_Outline', allowMultiple: false },
225
+ ] as const,
226
+ includeRest: true,
227
+ });
228
+
229
+ const hasLabel = TextInput_Label.length > 0;
230
+
231
+ const [focused, setFocused] = useState<boolean>(false);
232
+ // Use value from props instead of local state when input is controlled
233
+ const [value, onChangeValue] = useControlledValue({
234
+ value: valueProp,
235
+ defaultValue: defaultValue,
236
+ onChange: onChangeText,
237
+ disabled: !editable || disabled,
238
+ });
239
+
240
+ const onBlurRef = useLatest(onBlur);
241
+
242
+ const state = resolveStateVariant({
243
+ errorDisabled: errorProp && disabled,
244
+ disabled,
245
+ errorFocusedAndHovered: errorProp && hovered && focused,
246
+ errorFocused: errorProp && focused,
247
+ errorHovered: errorProp && hovered,
248
+ hoveredAndFocused: hovered && focused,
249
+ hovered,
250
+ focused: focused,
251
+ error: !!errorProp,
252
+ }) as any;
253
+
254
+ styles.useVariants({
255
+ variant: variant as any,
256
+ state,
257
+ size,
258
+ });
259
+
260
+ const [labelLayout, setLabelLayout] = useState<{
261
+ measured: boolean;
262
+ width: number;
263
+ height: number;
264
+ }>({
265
+ measured: false,
266
+ width: 0,
267
+ height: 0,
268
+ });
269
+
270
+ const [leftElementLayout, setElementLayout] = useState<{
271
+ measured: boolean;
272
+ width: number;
273
+ height: number;
274
+ }>({
275
+ measured: false,
276
+ width: 0,
277
+ height: 0,
278
+ });
279
+
280
+ const inputRefLocal = useRef<NativeTextInput>(null);
281
+
282
+ useImperativeHandle(ref, () => inputRefLocal.current!);
283
+
284
+ const handleFocus = useCallback(
285
+ (args: FocusEvent) => {
286
+ if (disabled || !editable) {
287
+ return;
288
+ }
230
289
 
231
- const { current: labelAnimation } = useRef<Animated.Value>(
232
- new Animated.Value(validInputValue ? 0 : 1),
233
- );
234
- const { current: errorAnimation } = useRef<Animated.Value>(
235
- new Animated.Value(errorProp ? 1 : 0),
236
- );
237
- const [focused, setFocused] = useState<boolean>(false);
238
- const [placeholder, setPlaceholder] = useState<string | undefined>('');
239
- // Use value from props instead of local state when input is controlled
240
- const [value, onChangeValue] = useControlledValue({
241
- value: rest.value,
242
- defaultValue: rest.defaultValue,
243
- onChange: rest.onChangeText,
244
- disabled: !editable || disabled,
290
+ setFocused(true);
291
+
292
+ onFocus?.(args);
293
+ },
294
+ [disabled, editable, onFocus],
295
+ );
296
+
297
+ const handleBlur = useCallback(
298
+ (args: BlurEvent) => {
299
+ if (!editable) {
300
+ return;
301
+ }
302
+
303
+ setFocused(false);
304
+ onBlur?.(args);
305
+ },
306
+ [editable, onBlur],
307
+ );
308
+
309
+ const handleLayoutAnimatedText = useCallback((e: LayoutChangeEvent) => {
310
+ setLabelLayout({
311
+ width: e.nativeEvent.layout.width,
312
+ height: e.nativeEvent.layout.height,
313
+ measured: true,
245
314
  });
315
+ }, []);
246
316
 
247
- const onBlurRef = useLatest(onBlur);
317
+ const handleLayoutLeftElement = useCallback((e: LayoutChangeEvent) => {
318
+ setElementLayout({
319
+ width: e.nativeEvent.layout.width,
320
+ height: e.nativeEvent.layout.height,
321
+ measured: true,
322
+ });
323
+ }, []);
248
324
 
249
- const state = resolveStateVariant({
250
- errorDisabled: errorProp && disabled,
251
- disabled,
252
- errorFocusedAndHovered: errorProp && hovered && focused,
253
- errorFocused: errorProp && focused,
254
- errorHovered: errorProp && hovered,
255
- hoveredAndFocused: hovered && focused,
256
- hovered,
257
- focused: focused,
258
- error: !!errorProp,
259
- }) as any;
325
+ const forceFocus = useCallback(() => inputRefLocal.current?.focus(), []);
260
326
 
261
- styles.useVariants({
262
- variant: variant as any,
327
+ const inputMinHeight = getInputMinHeight(variant, size);
328
+
329
+ // Workaround for React bug where onBlur doesn't fire when a focused input unmounts
330
+ // Issue: https://github.com/facebook/react/issues/25194
331
+ // Only needed for React 18+ on web (issue still exists in React 19)
332
+ useEffect(() => {
333
+ const is_version_18_or_higher =
334
+ typeof React.version === 'string' ? +React.version.split('.')[0] >= 18 : false;
335
+
336
+ const _onBlurRef = onBlurRef;
337
+ const input = inputRefLocal.current;
338
+
339
+ return () => {
340
+ if (!is_version_18_or_higher || !focused || Platform.OS !== 'web') {
341
+ return;
342
+ }
343
+
344
+ // Manually fire blur event on unmount if input was focused
345
+ const event = new Event('blur', { bubbles: true });
346
+ Object.defineProperty(event, 'target', {
347
+ writable: false,
348
+ value: input,
349
+ });
350
+ const syntheticEvent = createSyntheticEvent(
351
+ event,
352
+ ) as React.ChangeEvent<HTMLInputElement>;
353
+ _onBlurRef.current?.(syntheticEvent as any);
354
+ };
355
+ }, [onBlurRef, focused]);
356
+
357
+ const renderFunc = render || DefaultComponent;
358
+ const showPlaceholder = !hasLabel || focused || !!value;
359
+ const placeholderText = showPlaceholder ? placeholder : undefined;
360
+
361
+ const labelHeight = labelLayout.height;
362
+ const finalHeight = +labelHeight;
363
+ const inputHeight = finalHeight < inputMinHeight ? inputMinHeight : finalHeight;
364
+
365
+ const computedStyles = useMemo(
366
+ () => ({
367
+ textInputStyle: [
368
+ styles.input,
369
+ !multiline ? { height: inputHeight || labelHeight } : {},
370
+ multiline && variant === 'outlined' && { paddingTop: 12 },
371
+ {
372
+ textAlignVertical: multiline ? ('top' as const) : ('center' as const),
373
+ textAlign: I18nManager.isRTL ? ('right' as const) : ('left' as const),
374
+ },
375
+ Platform.OS === 'ios' && !multiline
376
+ ? { lineHeight: undefined, textAlign: undefined }
377
+ : {},
378
+ inputStyle,
379
+ ],
380
+ stateLayerStyle: [styles.stateLayer, stateLayerProps?.style],
381
+ }),
382
+ // forcing useMemo to recompute when state, size or variant change
383
+ // eslint-disable-next-line react-hooks/exhaustive-deps
384
+ [
385
+ size,
263
386
  state,
387
+ variant,
388
+ multiline,
389
+ inputHeight,
390
+ labelHeight,
391
+ inputStyle,
392
+ stateLayerProps?.style,
393
+ ],
394
+ );
395
+ const hasValue = !!value;
396
+
397
+ const contextValue = useMemo(
398
+ () => ({
399
+ variant,
264
400
  size,
265
- });
401
+ state,
402
+ disabled,
403
+ hasValue,
404
+ error: errorProp,
405
+ focused,
406
+ hovered,
407
+ hasLabel,
408
+ required,
409
+ multiline,
410
+ labelLayout,
411
+ leftElementLayout,
412
+ onLayoutLabel: handleLayoutAnimatedText,
413
+ onLayoutLeftElement: handleLayoutLeftElement,
414
+ forceFocus,
415
+ }),
416
+ [
417
+ variant,
418
+ size,
419
+ state,
420
+ disabled,
421
+ errorProp,
422
+ focused,
423
+ hovered,
424
+ hasValue,
425
+ hasLabel,
426
+ required,
427
+ multiline,
428
+ labelLayout,
429
+ leftElementLayout,
430
+ handleLayoutAnimatedText,
431
+ handleLayoutLeftElement,
432
+ forceFocus,
433
+ ],
434
+ );
435
+
436
+ const outlineElement = TextInput_Outline.length > 0 ? TextInput_Outline : <TextInputOutline />;
437
+
438
+ return (
439
+ <TextInputContext value={contextValue}>
440
+ <View
441
+ ref={actionsRef}
442
+ {...containerProps}
443
+ style={[styles.root, style, containerProps?.style]}>
444
+ {outlineElement}
445
+ {variant === 'flat' && (
446
+ <StateLayer
447
+ testID={testID && `${testID}--stateLayer`}
448
+ {...stateLayerProps}
449
+ style={computedStyles.stateLayerStyle}
450
+ />
451
+ )}
266
452
 
267
- const [labelLayout, setLabelLayout] = useState<{
268
- measured: boolean;
269
- width: number;
270
- height: number;
271
- }>({
272
- measured: false,
273
- width: 0,
274
- height: 0,
275
- });
453
+ {TextInput_Left}
454
+ <View
455
+ {...inputWrapperProps}
456
+ style={[
457
+ styles.inputWrapper,
458
+ {
459
+ minHeight: labelHeight,
460
+ },
461
+ inputWrapperProps?.style,
462
+ ]}>
463
+ {TextInput_Label}
464
+ {renderFunc({
465
+ placeholder: placeholderText,
466
+ ref: inputRefLocal,
467
+ placeholderTextColor: placeholderTextColor,
468
+ selectionColor: selectionColor,
469
+ editable: !disabled && editable,
470
+ underlineColorAndroid: 'transparent' as const,
471
+ multiline,
472
+ size,
473
+ onFocus: handleFocus,
474
+ onBlur: handleBlur,
475
+ onChangeText: onChangeValue,
476
+ value: value,
477
+ ...rest,
478
+ style: computedStyles.textInputStyle,
479
+ })}
480
+ </View>
481
+ {TextInput_Right}
482
+ </View>
483
+ {TextInput_SupportingText}
484
+ {restChildren}
485
+ </TextInputContext>
486
+ );
487
+ };
276
488
 
277
- const [leftElementLayout, setElementLayout] = useState<{
278
- measured: boolean;
279
- width: number;
280
- height: number;
281
- }>({
282
- measured: false,
283
- width: 0,
284
- height: 0,
285
- });
489
+ export default memo(TextInput);
286
490
 
287
- const timer = useRef<NodeJS.Timeout | undefined>(undefined);
288
- const inputRefLocal = useRef<NativeTextInput>(null);
491
+ /**
492
+ * TextInput.Label - Renders the animated floating label
493
+ */
494
+ export const TextInputLabel = memo(
495
+ ({
496
+ children,
497
+ style,
498
+ floatingStyle,
499
+ maxFontSizeMultiplier = 15,
500
+ }: TextInputLabelCompoundProps) => {
501
+ const {
502
+ labelLayout,
503
+ leftElementLayout,
504
+ hasValue,
505
+ focused,
506
+ error,
507
+ required,
508
+ onLayoutLabel,
509
+ variant,
510
+ size,
511
+ state,
512
+ } = useContext(TextInputContext);
289
513
 
290
- useImperativeHandle(ref, () => inputRefLocal.current!);
514
+ textInputLabelStyles.useVariants({
515
+ variant: variant as any,
516
+ state: state as any,
517
+ size,
518
+ });
291
519
 
292
- const { backgroundColor: parentBackground } = useContext(BackgroundContext);
293
- const hasActiveOutline = focused || errorProp;
520
+ const { current: labelAnimation } = useRef<Animated.Value>(
521
+ new Animated.Value(hasValue ? 0 : 1),
522
+ );
523
+ const { current: errorAnimation } = useRef<Animated.Value>(
524
+ new Animated.Value(error ? 1 : 0),
525
+ );
294
526
 
295
527
  useEffect(() => {
296
- // When the input has an error, we wiggle the label and apply error styles
297
- if (errorProp) {
298
- // show error
528
+ if (error) {
299
529
  Animated.timing(errorAnimation, {
300
530
  toValue: 1,
301
531
  duration: FOCUS_ANIMATION_DURATION * animationScale,
302
- // To prevent this - https://github.com/callstack/react-native-paper/issues/941
303
532
  useNativeDriver: true,
304
533
  }).start();
305
-
306
534
  return;
307
535
  }
308
536
 
309
- // hide error
310
537
  Animated.timing(errorAnimation, {
311
538
  toValue: 0,
312
539
  duration: BLUR_ANIMATION_DURATION * animationScale,
313
- // To prevent this - https://github.com/callstack/react-native-paper/issues/941
314
540
  useNativeDriver: true,
315
541
  }).start();
316
- }, [errorProp, errorAnimation]);
542
+ }, [error, errorAnimation]);
317
543
 
318
- useEffect(() => {
319
- // Show placeholder text only if the input is focused, or there's no label
320
- // We don't show placeholder if there's a label because the label acts as placeholder
321
- // When focused, the label moves up, so we can show a placeholder
322
- if (focused || !rest.label) {
323
- // Set the placeholder in a delay to offset the label animation
324
- // If we show it immediately, they'll overlap and look ugly
325
- timer.current = setTimeout(
326
- () => setPlaceholder(rest.placeholder),
327
- 50,
328
- ) as unknown as NodeJS.Timeout;
329
- } else {
330
- // hidePlaceholder
331
- setPlaceholder('');
332
- }
333
-
334
- return () => {
335
- if (timer.current) {
336
- clearTimeout(timer.current);
337
- }
338
- };
339
- }, [focused, rest.label, rest.placeholder]);
340
-
341
- const hasValue = !!value || focused;
544
+ const shouldMinimize = hasValue || focused;
342
545
 
343
546
  useEffect(() => {
344
- // The label should be minimized if the text input is focused, or has text
345
- // In minimized mode, the label moves up and becomes small
346
- // workaround for animated regression for react native > 0.61
347
- // https://github.com/callstack/react-native-paper/pull/1440
348
- if (hasValue) {
349
- // minimize label
547
+ if (shouldMinimize) {
350
548
  Animated.timing(labelAnimation, {
351
549
  toValue: 0,
352
550
  duration: BLUR_ANIMATION_DURATION * animationScale,
353
- // To prevent this - https://github.com/callstack/react-native-paper/issues/941
354
551
  useNativeDriver: true,
355
552
  }).start();
356
553
  } else {
357
- // restore label
358
554
  Animated.timing(labelAnimation, {
359
555
  toValue: 1,
360
556
  duration: FOCUS_ANIMATION_DURATION * animationScale,
361
- // To prevent this - https://github.com/callstack/react-native-paper/issues/941
362
557
  useNativeDriver: true,
363
558
  }).start();
364
559
  }
365
- }, [focused, hasValue, labelAnimation]);
560
+ }, [focused, shouldMinimize, labelAnimation]);
366
561
 
367
- const handleFocus = useCallback(
368
- (args: FocusEvent) => {
369
- if (disabled || !editable) {
370
- return;
371
- }
562
+ if (variant === 'plain') {
563
+ return null;
564
+ }
565
+
566
+ const labelScale = minimizedLabelFontSize / maximizedLabelFontSize;
567
+ const floatingLabelVerticalOffset = variant === 'flat' ? 16 : 0;
568
+ const labelWidth = labelLayout.width;
569
+ const labelHalfWidth = labelWidth / 2;
570
+ const baseLabelTranslateX =
571
+ (I18nManager.isRTL ? 1 : -1) *
572
+ (labelScale - 1 + labelHalfWidth - (labelScale * labelWidth) / 2);
573
+ const resolvedBaseLabelTranslateX =
574
+ variant === 'outlined'
575
+ ? baseLabelTranslateX - leftElementLayout.width
576
+ : baseLabelTranslateX;
372
577
 
373
- setFocused(true);
578
+ return (
579
+ <InputLabel
580
+ hasValue={hasValue}
581
+ focused={focused}
582
+ labelAnimation={labelAnimation}
583
+ errorAnimation={errorAnimation}
584
+ labelLayout={labelLayout}
585
+ label={children}
586
+ floatingStyle={[textInputLabelStyles.floatingLabel, floatingStyle]}
587
+ floatingLabelVerticalOffset={floatingLabelVerticalOffset}
588
+ required={required}
589
+ onLayoutAnimatedText={onLayoutLabel}
590
+ error={error}
591
+ baseLabelTranslateX={resolvedBaseLabelTranslateX}
592
+ labelScale={labelScale}
593
+ wiggleOffsetX={labelWiggleXOffset}
594
+ maxFontSizeMultiplier={maxFontSizeMultiplier}
595
+ style={[textInputLabelStyles.labelText, style]}
596
+ />
597
+ );
598
+ },
599
+ );
374
600
 
375
- rest.onFocus?.(args);
601
+ TextInputLabel.displayName = 'TextInput_Label';
602
+
603
+ /**
604
+ * TextInput.Left - Container for left-positioned elements
605
+ */
606
+ export const TextInputLeft = memo(
607
+ ({
608
+ children,
609
+ style,
610
+ onLayout,
611
+ onPress: onPressProp,
612
+ ...rest
613
+ }: TextInputElementCompoundProps) => {
614
+ const { forceFocus, onLayoutLeftElement, state } = useContext(TextInputContext);
615
+
616
+ textInputLeftStyles.useVariants({
617
+ state: state as any,
618
+ });
619
+
620
+ const handleLayout = useCallback(
621
+ (e: LayoutChangeEvent) => {
622
+ onLayoutLeftElement(e);
623
+ onLayout?.(e);
376
624
  },
377
- [disabled, editable, rest],
625
+ [onLayoutLeftElement, onLayout],
378
626
  );
379
627
 
380
- const handleBlur = useCallback(
381
- (args: BlurEvent) => {
382
- if (!editable) {
628
+ const onPress = useCallback(
629
+ (e: GestureResponderEvent) => {
630
+ if (onPressProp) {
631
+ onPressProp(e, forceFocus);
632
+
383
633
  return;
384
634
  }
385
635
 
386
- setFocused(false);
387
- onBlur?.(args);
636
+ forceFocus();
388
637
  },
389
- [editable, onBlur],
638
+ [forceFocus, onPressProp],
390
639
  );
391
640
 
392
- const handleLayoutAnimatedText = useCallback((e: LayoutChangeEvent) => {
393
- setLabelLayout({
394
- width: e.nativeEvent.layout.width,
395
- height: e.nativeEvent.layout.height,
396
- measured: true,
397
- });
398
- }, []);
641
+ return (
642
+ <Pressable
643
+ onPress={onPress}
644
+ style={[textInputLeftStyles.leftElement, style]}
645
+ onLayout={handleLayout}
646
+ accessibilityRole="none"
647
+ tabIndex={-1}
648
+ {...rest}>
649
+ {children}
650
+ </Pressable>
651
+ );
652
+ },
653
+ );
399
654
 
400
- const handleLayoutLeftElement = useCallback((e: LayoutChangeEvent) => {
401
- setElementLayout({
402
- width: e.nativeEvent.layout.width,
403
- height: e.nativeEvent.layout.height,
404
- measured: true,
405
- });
406
- }, []);
655
+ TextInputLeft.displayName = 'TextInput_Left';
407
656
 
408
- const forceFocus = useCallback(() => inputRefLocal.current?.focus(), []);
657
+ /**
658
+ * TextInput.Right - Container for right-positioned elements
659
+ */
660
+ export const TextInputRight = memo(
661
+ ({ children, style, onPress: onPressProp, ...rest }: TextInputElementCompoundProps) => {
662
+ const { forceFocus, state } = useContext(TextInputContext);
409
663
 
410
- const inputMinHeight = getInputMinHeight(variant, size);
664
+ textInputRightStyles.useVariants({
665
+ state: state as any,
666
+ });
411
667
 
412
- // This is because of a bug in react 18 doesn't trigger onBlur when the component is unmounted // we can remove it when it's fixed
413
- useEffect(() => {
414
- const isVersion18 =
415
- typeof React.version === 'string' ? +React.version.split('.')[0] >= 18 : false;
416
-
417
- const _onBlurRef = onBlurRef;
418
- const input = inputRefLocal.current;
419
-
420
- return () => {
421
- if (!isVersion18 || !input?.isFocused() || Platform.OS !== 'web') return;
422
-
423
- const event = new Event('blur', { bubbles: true });
424
- Object.defineProperty(event, 'target', {
425
- writable: false,
426
- value: input,
427
- });
428
- const syntheticEvent = createSyntheticEvent(
429
- event,
430
- ) as React.ChangeEvent<HTMLInputElement>;
431
- _onBlurRef.current?.(syntheticEvent as any);
432
- };
433
- }, [onBlurRef]);
434
-
435
- // @ts-ignore // TODO - fix this
436
- const componentStyles = styles.root;
668
+ const onPress = useCallback(
669
+ (e: GestureResponderEvent) => {
670
+ if (onPressProp) {
671
+ onPressProp(e, forceFocus);
437
672
 
438
- const labelWidth = labelLayout.width;
439
- const labelHeight = labelLayout.height;
440
- const labelHalfWidth = labelWidth / 2;
441
- const labelScale =
442
- minimizedLabelFontSize / (componentStyles.fontSize || maximizedLabelFontSize);
443
- const baseLabelTranslateX =
444
- (I18nManager.isRTL ? 1 : -1) *
445
- (labelScale - 1 + labelHalfWidth - (labelScale * labelWidth) / 2);
673
+ return;
674
+ }
446
675
 
447
- // const normalizedLeftElementMarginRight = normalizeSpacings(
448
- // styles.leftElement,
449
- // 'marginRight',
450
- // );
451
-
452
- const baseLabelTranslateXOutline =
453
- baseLabelTranslateX - leftElementLayout.width - (left ? 0 : 0);
454
-
455
- const backgroundColor =
456
- styles.container?.backgroundColor || componentStyles.backgroundColor;
457
- // const viableRadiuses = normalizeBorderRadiuses(componentStyles);
458
- const finalHeight =
459
- (+(componentStyles.height ?? 0) > 0 ? componentStyles.height : +labelHeight) ?? 0;
460
- const inputHeight = finalHeight < inputMinHeight ? inputMinHeight : finalHeight;
461
- const hasLabel = !!rest.label;
462
-
463
- const computedStyles = useMemo(
464
- () => ({
465
- activeIndicator: styles.activeIndicator,
466
- fontSize: componentStyles.fontSize,
467
- fontWeight: componentStyles.fontWeight,
468
- height: componentStyles.height,
469
- textAlign: componentStyles.textAlign,
470
- backgroundColor,
471
- activeColor: (styles as any).root.activeColor,
472
- baseLabelTranslateX:
473
- variant === 'outlined' ? baseLabelTranslateXOutline : baseLabelTranslateX,
474
- labelScale,
475
- selectionColor: selectionColorProp || (styles as any).root.activeColor,
476
- underlineColor: underlineColorProp,
477
- activeUnderlineColor: activeUnderlineColorProp,
478
- outlineColor: outlineColorProp,
479
- activeOutlineColor: activeOutlineColorProp,
480
- placeholderTextColor: placeholderTextColorProp || styles.placeholder?.color,
481
- floatingLabelVerticalOffset,
482
- labelWiggleXOffset,
483
- textInputStyle: [
484
- styles.inputText,
485
- variant === 'flat' && hasLabel && { paddingTop: 12 },
486
- !multiline || (multiline && componentStyles.height)
487
- ? { height: inputHeight || labelHeight }
488
- : {},
489
- multiline && variant === 'outlined' && { paddingTop: 12 },
490
- {
491
- textAlignVertical: multiline ? 'top' : 'center',
492
- textAlign: componentStyles.textAlign
493
- ? componentStyles.textAlign
494
- : I18nManager.isRTL
495
- ? 'right'
496
- : 'left',
497
- },
498
- Platform.OS === 'ios' && !multiline
499
- ? { lineHeight: undefined, textAlign: undefined }
500
- : {},
501
- Platform.OS === 'web' && { outline: 'none' },
502
- inputStyle,
503
- ],
504
- inputContainerStyle: [
505
- styles.labelContainer,
506
- {
507
- minHeight: componentStyles.height || labelHeight,
508
- },
509
- inputContainerStyle,
510
- ],
511
- underlineStyle: [
512
- styles.underline,
513
- styles.activeIndicator,
514
- hasActiveOutline && activeOutlineColorProp
515
- ? {
516
- backgroundColor: hasActiveOutline
517
- ? activeUnderlineColorProp
518
- : underlineColorProp,
519
- }
520
- : {},
521
- ],
522
- outlineStyle: [
523
- styles.outline,
524
- hasActiveOutline && activeOutlineColorProp
525
- ? {
526
- borderColor: hasActiveOutline
527
- ? activeOutlineColorProp
528
- : outlineColorProp,
529
- }
530
- : {},
531
- // viableRadiuses,
532
- {},
533
- ],
534
- patchContainer: [
535
- StyleSheet.absoluteFill,
536
- {
537
- backgroundColor,
538
- },
539
- styles.patchContainer,
540
- ],
541
- stateLayerStyle: [styles.stateLayer, stateLayerProps?.style],
542
- }),
543
- // forcing useMemo to recompute when state, size or variant change
544
- // eslint-disable-next-line
545
- [
546
- hasLabel,
547
- state,
548
- size,
549
- componentStyles,
550
- backgroundColor,
551
- variant,
552
- parentBackground,
553
- baseLabelTranslateXOutline,
554
- baseLabelTranslateX,
555
- labelScale,
556
- selectionColorProp,
557
- underlineColorProp,
558
- activeUnderlineColorProp,
559
- outlineColorProp,
560
- activeOutlineColorProp,
561
- placeholderTextColorProp,
562
- floatingLabelVerticalOffset,
563
- multiline,
564
- inputHeight,
565
- labelHeight,
566
- inputStyle,
567
- inputContainerStyle,
568
- hasActiveOutline,
569
- stateLayerProps?.style,
570
- ],
676
+ forceFocus();
677
+ },
678
+ [forceFocus, onPressProp],
571
679
  );
572
680
 
573
681
  return (
574
- <>
575
- <View ref={actionsRef} style={[styles.container, style]}>
576
- {variant === 'flat' && (
577
- <>
578
- <Animated.View
579
- testID={rest.testID && `${rest.testID}--text-input-underline`}
580
- style={computedStyles.underlineStyle}
581
- />
582
-
583
- <StateLayer
584
- testID={rest.testID && `${rest.testID}--stateLayer`}
585
- {...stateLayerProps}
586
- style={computedStyles.stateLayerStyle}
587
- />
588
- </>
589
- )}
590
- {variant === 'outlined' && (
591
- <Animated.View
592
- testID="text-input-outline"
593
- pointerEvents="none"
594
- style={computedStyles.outlineStyle}
595
- />
596
- )}
597
-
598
- {left && (
599
- <View
600
- style={[styles.leftElement, leftElementStyle]}
601
- onLayout={handleLayoutLeftElement}
602
- testID={rest.testID && `${rest.testID}--text-input-left-element`}>
603
- {typeof left === 'function'
604
- ? left?.({ color: computedStyles.activeColor, forceFocus, focused })
605
- : left}
606
- </View>
607
- )}
608
-
609
- <View
610
- style={computedStyles.inputContainerStyle}
611
- testID={rest.testID && `${rest.testID}-${variant}`}>
612
- {Platform.OS !== 'android' &&
613
- multiline &&
614
- !!rest.label &&
615
- variant === 'flat' && (
616
- // Workaround for: https://github.com/callstack/react-native-paper/issues/2799
617
- // Patch for a multiline TextInput with fixed height, which allow to avoid covering input label with its value.
618
- <View
619
- testID={rest.testID && `${rest.testID}--patch-container`}
620
- pointerEvents="none"
621
- style={computedStyles.patchContainer}
622
- />
623
- )}
624
-
625
- {variant !== 'plain' && (
626
- <InputLabel
627
- hasValue={!!value}
628
- focused={focused}
629
- labelAnimation={labelAnimation}
630
- errorAnimation={errorAnimation}
631
- labelLayout={labelLayout}
632
- label={rest.label}
633
- floatingStyle={styles.floatingLabel}
634
- floatingLabelVerticalOffset={
635
- computedStyles.floatingLabelVerticalOffset
636
- }
637
- required={required}
638
- onLayoutAnimatedText={handleLayoutAnimatedText}
639
- error={errorProp}
640
- baseLabelTranslateX={computedStyles.baseLabelTranslateX}
641
- labelScale={computedStyles.labelScale}
642
- wiggleOffsetX={computedStyles.labelWiggleXOffset}
643
- maxFontSizeMultiplier={maxFontSizeMultiplier}
644
- testID={rest.testID}
645
- style={styles.labelText}
646
- />
647
- )}
648
-
649
- {render({
650
- testID: rest.testID,
651
- ...rest,
652
- style: computedStyles.textInputStyle,
653
- ref: inputRefLocal,
654
- onChangeText: onChangeValue,
655
- placeholder: rest.label ? placeholder : rest.placeholder,
656
- placeholderTextColor: computedStyles.placeholderTextColor,
657
- editable: !disabled && editable,
658
- selectionColor: computedStyles.selectionColor,
659
- onFocus: handleFocus,
660
- onBlur: handleBlur,
661
- underlineColorAndroid: 'transparent',
662
- multiline,
663
- size,
664
- })}
665
- </View>
666
-
667
- {right && (
668
- <View
669
- style={[styles.rightElement, rightElementStyle]}
670
- testID={rest.testID && `${rest.testID}--text-input-right-element`}>
671
- {typeof right === 'function'
672
- ? right?.({
673
- color: computedStyles.activeColor,
674
- forceFocus,
675
- focused,
676
- })
677
- : right}
678
- </View>
679
- )}
680
- </View>
682
+ <Pressable
683
+ onPress={onPress}
684
+ style={[textInputRightStyles.rightElement, style]}
685
+ accessibilityRole="none"
686
+ tabIndex={-1}
687
+ {...rest}>
688
+ {children}
689
+ </Pressable>
690
+ );
691
+ },
692
+ );
681
693
 
682
- {supportingText && (
683
- <HelperText
684
- variant={errorProp ? 'error' : 'info'}
685
- style={styles.supportingText}>
686
- {supportingText}
687
- </HelperText>
688
- )}
689
- </>
694
+ TextInputRight.displayName = 'TextInput_Right';
695
+
696
+ /**
697
+ * TextInput.Icon - Convenience component for icons within TextInput
698
+ */
699
+ export const TextInputIcon = memo(
700
+ ({ size: sizeProp, color: colorProp, style, ...rest }: TextInputIconCompoundProps) => {
701
+ const { state } = useContext(TextInputContext);
702
+
703
+ textInputIconStyles.useVariants({
704
+ // @ts-ignore - state includes 'default' which is valid but not in style variants
705
+ state,
706
+ });
707
+
708
+ const colorResolved = colorProp;
709
+
710
+ return (
711
+ <Icon
712
+ size={sizeProp ?? 20}
713
+ color={colorResolved}
714
+ style={[textInputIconStyles.root, style]}
715
+ {...rest}
716
+ />
690
717
  );
691
718
  },
692
719
  );
693
720
 
694
- export default memo(TextInput);
721
+ TextInputIcon.displayName = 'TextInput_Icon';
722
+
723
+ /**
724
+ * TextInput.SupportingText - Helper/error text below the input
725
+ */
726
+ export const TextInputSupportingText = memo(
727
+ ({ children, style }: TextInputSupportingTextCompoundProps) => {
728
+ const { error, state } = useContext(TextInputContext);
729
+
730
+ textInputSupportingTextStyles.useVariants({
731
+ state: state as any,
732
+ });
733
+
734
+ return (
735
+ <HelperText
736
+ variant={error ? 'error' : 'info'}
737
+ style={[textInputSupportingTextStyles.supportingText, style]}>
738
+ {children}
739
+ </HelperText>
740
+ );
741
+ },
742
+ );
743
+
744
+ TextInputSupportingText.displayName = 'TextInput_SupportingText';
745
+
746
+ /**
747
+ * TextInput.Outline - Renders the border overlay for outlined/flat variants.
748
+ * Rendered automatically if not provided. Use this to customize border styles.
749
+ */
750
+ export const TextInputOutline = memo(({ style }: TextInputOutlineCompoundProps) => {
751
+ const { variant, state } = useContext(TextInputContext);
752
+
753
+ textInputOutlineStyles.useVariants({
754
+ variant: variant as any,
755
+ state: state as any,
756
+ });
757
+
758
+ if (variant === 'plain') {
759
+ return null;
760
+ }
761
+
762
+ if (variant === 'flat') {
763
+ return (
764
+ <View
765
+ style={[
766
+ textInputOutlineStyles.underline,
767
+ textInputOutlineStyles.activeIndicator,
768
+ style,
769
+ ]}
770
+ />
771
+ );
772
+ }
773
+
774
+ // outlined
775
+ return <View pointerEvents="none" style={[textInputOutlineStyles.outline, style]} />;
776
+ });
777
+
778
+ TextInputOutline.displayName = 'TextInput_Outline';