react-native-molecules 0.5.0-beta.1 → 0.5.0-beta.11

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