@idealyst/components 1.2.29 → 1.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +3 -3
  2. package/package.json +3 -3
  3. package/plugin/__tests__/web.test.ts +2 -2
  4. package/src/Accordion/Accordion.native.tsx +3 -2
  5. package/src/ActivityIndicator/ActivityIndicator.native.tsx +3 -2
  6. package/src/ActivityIndicator/ActivityIndicator.styles.tsx +25 -26
  7. package/src/ActivityIndicator/ActivityIndicator.web.tsx +2 -1
  8. package/src/Alert/Alert.native.tsx +20 -10
  9. package/src/Alert/Alert.styles.tsx +148 -86
  10. package/src/Alert/Alert.web.tsx +10 -5
  11. package/src/Alert/types.ts +53 -3
  12. package/src/Avatar/Avatar.native.tsx +3 -2
  13. package/src/Avatar/Avatar.web.tsx +2 -1
  14. package/src/Avatar/types.ts +1 -1
  15. package/src/Badge/Badge.native.tsx +18 -6
  16. package/src/Badge/Badge.styles.tsx +22 -5
  17. package/src/Badge/Badge.web.tsx +12 -4
  18. package/src/Badge/types.ts +14 -2
  19. package/src/Breadcrumb/Breadcrumb.native.tsx +3 -2
  20. package/src/Button/Button.native.tsx +16 -6
  21. package/src/Button/Button.styles.tsx +2 -2
  22. package/src/Button/Button.web.tsx +19 -15
  23. package/src/Button/types.ts +6 -10
  24. package/src/Card/Card.native.tsx +27 -3
  25. package/src/Card/Card.web.tsx +30 -4
  26. package/src/Card/types.ts +15 -0
  27. package/src/Checkbox/Checkbox.native.tsx +5 -4
  28. package/src/Checkbox/Checkbox.styles.tsx +62 -52
  29. package/src/Checkbox/Checkbox.web.tsx +4 -3
  30. package/src/Checkbox/types.ts +1 -1
  31. package/src/Chip/Chip.native.tsx +30 -7
  32. package/src/Chip/Chip.styles.tsx +142 -124
  33. package/src/Chip/Chip.web.tsx +28 -5
  34. package/src/Chip/types.ts +15 -0
  35. package/src/Dialog/Dialog.native.tsx +6 -6
  36. package/src/Dialog/Dialog.web.tsx +5 -5
  37. package/src/Dialog/types.ts +2 -2
  38. package/src/Divider/Divider.native.tsx +20 -17
  39. package/src/Divider/Divider.styles.tsx +51 -29
  40. package/src/Divider/Divider.web.tsx +5 -4
  41. package/src/Divider/types.ts +3 -3
  42. package/src/Icon/Icon.native.tsx +3 -2
  43. package/src/Icon/Icon.web.tsx +2 -1
  44. package/src/Icon/IconSvg/IconSvg.native.tsx +3 -2
  45. package/src/Image/Image.native.tsx +3 -2
  46. package/src/Input/Input.native.tsx +42 -290
  47. package/src/Input/Input.styles.tsx +1 -1
  48. package/src/Input/Input.web.tsx +37 -288
  49. package/src/Input/index.native.ts +9 -2
  50. package/src/Input/index.ts +8 -1
  51. package/src/Input/index.web.ts +8 -1
  52. package/src/Input/types.ts +1 -1
  53. package/src/List/List.native.tsx +3 -2
  54. package/src/List/ListItem.native.tsx +3 -2
  55. package/src/List/ListSection.native.tsx +3 -2
  56. package/src/Menu/Menu.native.tsx +2 -1
  57. package/src/Menu/Menu.styles.tsx +79 -29
  58. package/src/Menu/Menu.web.tsx +2 -1
  59. package/src/Menu/MenuItem.native.tsx +4 -3
  60. package/src/Menu/MenuItem.styles.tsx +81 -32
  61. package/src/Menu/MenuItem.web.tsx +2 -1
  62. package/src/Menu/docs.ts +1 -1
  63. package/src/Popover/Popover.native.tsx +2 -1
  64. package/src/Popover/Popover.web.tsx +2 -1
  65. package/src/Popover/types.ts +15 -4
  66. package/src/Pressable/Pressable.native.tsx +3 -2
  67. package/src/Pressable/Pressable.web.tsx +3 -5
  68. package/src/Progress/Progress.native.tsx +5 -4
  69. package/src/Progress/Progress.web.tsx +3 -3
  70. package/src/Progress/types.ts +3 -3
  71. package/src/RadioButton/RadioButton.native.tsx +4 -3
  72. package/src/RadioButton/RadioButton.styles.tsx +53 -33
  73. package/src/RadioButton/RadioGroup.native.tsx +3 -2
  74. package/src/SVGImage/SVGImage.native.tsx +5 -4
  75. package/src/SVGImage/SVGImage.styles.tsx +44 -10
  76. package/src/SVGImage/SVGImage.web.tsx +2 -1
  77. package/src/Screen/Screen.native.tsx +2 -1
  78. package/src/Screen/Screen.web.tsx +2 -1
  79. package/src/Select/Select.native.tsx +6 -5
  80. package/src/Select/Select.styles.tsx +1 -1
  81. package/src/Select/Select.web.tsx +4 -3
  82. package/src/Select/types.ts +1 -1
  83. package/src/Skeleton/Skeleton.native.tsx +2 -1
  84. package/src/Slider/Slider.native.tsx +9 -8
  85. package/src/Slider/Slider.web.tsx +10 -9
  86. package/src/Slider/types.ts +9 -2
  87. package/src/Switch/Switch.native.tsx +7 -6
  88. package/src/Switch/Switch.styles.tsx +35 -17
  89. package/src/Switch/Switch.web.tsx +8 -7
  90. package/src/Switch/types.ts +44 -4
  91. package/src/TabBar/TabBar.native.tsx +3 -2
  92. package/src/Text/Text.native.tsx +3 -2
  93. package/src/Text/Text.web.tsx +2 -1
  94. package/src/TextArea/TextArea.native.tsx +3 -2
  95. package/src/TextArea/TextArea.styles.tsx +2 -2
  96. package/src/TextArea/TextArea.web.tsx +2 -1
  97. package/src/TextInput/TextInput.native.tsx +300 -0
  98. package/src/TextInput/TextInput.styles.tsx +207 -0
  99. package/src/TextInput/TextInput.web.tsx +301 -0
  100. package/src/TextInput/index.native.ts +3 -0
  101. package/src/TextInput/index.ts +5 -0
  102. package/src/TextInput/index.web.ts +5 -0
  103. package/src/TextInput/types.ts +163 -0
  104. package/src/Tooltip/Tooltip.native.tsx +3 -2
  105. package/src/Video/Video.native.tsx +4 -3
  106. package/src/View/View.native.tsx +2 -1
  107. package/src/View/View.web.tsx +2 -1
  108. package/src/examples/AlertExamples.tsx +5 -5
  109. package/src/examples/ButtonExamples.tsx +12 -12
  110. package/src/examples/CardExamples.tsx +1 -1
  111. package/src/examples/CheckboxExamples.tsx +2 -2
  112. package/src/examples/ChipExamples.tsx +6 -6
  113. package/src/examples/DialogExamples.tsx +1 -1
  114. package/src/examples/DividerExamples.tsx +1 -1
  115. package/src/examples/InputExamples.tsx +1 -1
  116. package/src/examples/LinkExamples.tsx +1 -1
  117. package/src/examples/ListExamples.tsx +1 -1
  118. package/src/examples/MenuExamples.tsx +2 -2
  119. package/src/examples/ProgressExamples.tsx +1 -1
  120. package/src/examples/RadioButtonExamples.tsx +5 -5
  121. package/src/examples/SVGImageExamples.tsx +1 -1
  122. package/src/examples/SelectExamples.tsx +1 -1
  123. package/src/examples/SliderExamples.tsx +5 -5
  124. package/src/examples/SwitchExamples.tsx +2 -2
  125. package/src/examples/TableExamples.tsx +1 -1
  126. package/src/examples/TooltipExamples.tsx +2 -2
  127. package/src/extensions/index.ts +1 -0
  128. package/src/extensions/types.ts +10 -3
  129. package/src/index.ts +23 -2
  130. package/src/utils/index.ts +12 -0
  131. package/src/utils/refTypes.ts +50 -0
@@ -6,21 +6,22 @@ import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
6
  import { isIconName } from '../Icon/icon-resolver';
7
7
  import useMergeRefs from '../hooks/useMergeRefs';
8
8
  import { getWebSelectionAriaProps, generateAccessibilityId } from '../utils/accessibility';
9
+ import type { IdealystElement } from '../utils/refTypes';
9
10
 
10
11
  /**
11
12
  * Toggle switch for binary on/off states with optional label and icons.
12
13
  * Supports custom enabled/disabled icons and multiple sizes.
13
14
  */
14
- const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
15
+ const Switch = forwardRef<IdealystElement, SwitchProps>(({
15
16
  checked = false,
16
- onCheckedChange,
17
+ onChange,
17
18
  disabled = false,
18
19
  label,
19
20
  labelPosition = 'right',
20
21
  intent = 'primary',
21
22
  size = 'md',
22
- enabledIcon,
23
- disabledIcon,
23
+ onIcon,
24
+ offIcon,
24
25
  // Spacing variants from FormInputStyleProps
25
26
  margin,
26
27
  marginVertical,
@@ -39,8 +40,8 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
39
40
  accessibilityChecked,
40
41
  }, ref) => {
41
42
  const handleClick = () => {
42
- if (!disabled && onCheckedChange) {
43
- onCheckedChange(!checked);
43
+ if (!disabled && onChange) {
44
+ onChange(!checked);
44
45
  }
45
46
  };
46
47
 
@@ -93,7 +94,7 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
93
94
 
94
95
  // Helper to render icon
95
96
  const renderIcon = () => {
96
- const iconToRender = checked ? enabledIcon : disabledIcon;
97
+ const iconToRender = checked ? onIcon : offIcon;
97
98
  if (!iconToRender) return null;
98
99
 
99
100
  if (isIconName(iconToRender)) {
@@ -10,21 +10,61 @@ export type SwitchSizeVariant = Size;
10
10
 
11
11
  /**
12
12
  * Toggle switch component for binary on/off states.
13
- * Supports custom icons for enabled/disabled states and optional label positioning.
13
+ * Supports custom icons for on/off states and optional label positioning.
14
14
  */
15
15
  export interface SwitchProps extends FormInputStyleProps, SelectionAccessibilityProps {
16
16
  /**
17
17
  * Whether the switch is on
18
18
  */
19
19
  checked?: boolean;
20
- onCheckedChange?: (checked: boolean) => void;
20
+
21
+ /**
22
+ * Called when the switch state changes
23
+ */
24
+ onChange?: (checked: boolean) => void;
25
+
26
+ /**
27
+ * Whether the switch is disabled
28
+ */
21
29
  disabled?: boolean;
30
+
31
+ /**
32
+ * Label text to display next to the switch
33
+ */
22
34
  label?: string;
35
+
36
+ /**
37
+ * Position of the label relative to the switch
38
+ */
23
39
  labelPosition?: 'left' | 'right';
40
+
41
+ /**
42
+ * The intent/color scheme of the switch
43
+ */
24
44
  intent?: SwitchIntentVariant;
45
+
46
+ /**
47
+ * Size of the switch
48
+ */
25
49
  size?: SwitchSizeVariant;
26
- enabledIcon?: IconName | React.ReactNode;
27
- disabledIcon?: IconName | React.ReactNode;
50
+
51
+ /**
52
+ * Icon to display in the thumb when the switch is ON
53
+ */
54
+ onIcon?: IconName | React.ReactNode;
55
+
56
+ /**
57
+ * Icon to display in the thumb when the switch is OFF
58
+ */
59
+ offIcon?: IconName | React.ReactNode;
60
+
61
+ /**
62
+ * Additional styles (platform-specific)
63
+ */
28
64
  style?: StyleProp<ViewStyle>;
65
+
66
+ /**
67
+ * Test ID for testing
68
+ */
29
69
  testID?: string;
30
70
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from './TabBar.styles';
11
11
  import type { TabBarProps, TabBarItem } from './types';
12
12
  import { getNativeAccessibilityProps } from '../utils/accessibility';
13
+ import type { IdealystElement } from '../utils/refTypes';
13
14
 
14
15
  // Icon size mapping based on size variant
15
16
  const ICON_SIZES: Record<string, number> = {
@@ -33,7 +34,7 @@ function renderIcon(
33
34
  return icon;
34
35
  }
35
36
 
36
- const TabBar = forwardRef<View, TabBarProps>(({
37
+ const TabBar = forwardRef<IdealystElement, TabBarProps>(({
37
38
  items,
38
39
  value: controlledValue,
39
40
  defaultValue,
@@ -154,7 +155,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
154
155
  }}
155
156
  style={{ width: '100%' }}
156
157
  >
157
- <View ref={ref} nativeID={id} style={[containerStyle, style]} testID={testID} {...nativeA11yProps}>
158
+ <View ref={ref as any} nativeID={id} style={[containerStyle, style]} testID={testID} {...nativeA11yProps}>
158
159
  {/* Animated indicator - render first so it's behind */}
159
160
  <Animated.View
160
161
  style={[
@@ -2,8 +2,9 @@ import { forwardRef } from 'react';
2
2
  import { Text as RNText } from 'react-native';
3
3
  import { TextProps } from './types';
4
4
  import { textStyles } from './Text.styles';
5
+ import type { IdealystElement } from '../utils/refTypes';
5
6
 
6
- const Text = forwardRef<RNText, TextProps>(({
7
+ const Text = forwardRef<IdealystElement, TextProps>(({
7
8
  children,
8
9
  typography = 'body1',
9
10
  weight,
@@ -30,7 +31,7 @@ const Text = forwardRef<RNText, TextProps>(({
30
31
 
31
32
  return (
32
33
  <RNText
33
- ref={ref}
34
+ ref={ref as any}
34
35
  nativeID={id}
35
36
  style={[textStyles.text({ color, typography, weight, align }), style]}
36
37
  testID={testID}
@@ -3,12 +3,13 @@ import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { TextProps } from './types';
4
4
  import { textStyles } from './Text.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import type { IdealystElement } from '../utils/refTypes';
6
7
 
7
8
  /**
8
9
  * Typography component for displaying text with predefined styles and semantic variants.
9
10
  * Supports multiple typography scales, colors, weights, and alignments.
10
11
  */
11
- const Text = forwardRef<HTMLSpanElement, TextProps>(({
12
+ const Text = forwardRef<IdealystElement, TextProps>(({
12
13
  children,
13
14
  typography,
14
15
  weight,
@@ -4,8 +4,9 @@ import { textAreaStyles } from './TextArea.styles';
4
4
  import Text from '../Text';
5
5
  import type { TextAreaProps } from './types';
6
6
  import { getNativeFormAccessibilityProps } from '../utils/accessibility';
7
+ import type { IdealystElement } from '../utils/refTypes';
7
8
 
8
- const TextArea = forwardRef<TextInput, TextAreaProps>(({
9
+ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
9
10
  value: controlledValue,
10
11
  defaultValue = '',
11
12
  onChange,
@@ -149,7 +150,7 @@ const TextArea = forwardRef<TextInput, TextAreaProps>(({
149
150
 
150
151
  <View style={textareaContainerStyleComputed}>
151
152
  <TextInput
152
- ref={ref}
153
+ ref={ref as any}
153
154
  {...nativeA11yProps}
154
155
  style={[
155
156
  textareaStyleComputed,
@@ -117,7 +117,7 @@ export const textAreaStyles = defineStyle('TextArea', (theme: Theme) => ({
117
117
  fontSize: 12,
118
118
  variants: {
119
119
  hasError: {
120
- true: { color: theme.intents.error.primary },
120
+ true: { color: theme.intents.danger.primary },
121
121
  false: { color: theme.colors.text.secondary },
122
122
  },
123
123
  },
@@ -136,7 +136,7 @@ export const textAreaStyles = defineStyle('TextArea', (theme: Theme) => ({
136
136
  color: theme.colors.text.secondary,
137
137
  variants: {
138
138
  isAtLimit: {
139
- true: { color: theme.intents.error.primary },
139
+ true: { color: theme.intents.danger.primary },
140
140
  false: {},
141
141
  },
142
142
  isNearLimit: {
@@ -4,12 +4,13 @@ import { textAreaStyles } from './TextArea.styles';
4
4
  import type { TextAreaProps } from './types';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
6
  import { getWebFormAriaProps, combineIds, generateAccessibilityId } from '../utils/accessibility';
7
+ import type { IdealystElement } from '../utils/refTypes';
7
8
 
8
9
  /**
9
10
  * Multi-line text input with auto-grow, character counting, and validation support.
10
11
  * Includes label, helper text, and error message display.
11
12
  */
12
- const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
13
+ const TextArea = forwardRef<IdealystElement, TextAreaProps>(({
13
14
  value: controlledValue,
14
15
  defaultValue = '',
15
16
  onChange,
@@ -0,0 +1,300 @@
1
+ import React, { useState, isValidElement, useMemo, useEffect, useRef, useCallback } from 'react';
2
+ import { View, TextInput as RNTextInput, TouchableOpacity, Platform, TextInputProps as RNTextInputProps } from 'react-native';
3
+ import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons';
4
+ import { useUnistyles } from 'react-native-unistyles';
5
+ import { TextInputProps } from './types';
6
+ import { textInputStyles } from './TextInput.styles';
7
+ import { getNativeFormAccessibilityProps } from '../utils/accessibility';
8
+ import type { IdealystElement } from '../utils/refTypes';
9
+
10
+ // Inner TextInput component that can be memoized to prevent re-renders
11
+ // for Android secure text entry
12
+ type InnerTextInputProps = {
13
+ inputRef: React.ForwardedRef<RNTextInput>;
14
+ value: string | undefined;
15
+ onChangeText: ((text: string) => void) | undefined;
16
+ isAndroidSecure: boolean;
17
+ textInputProps: Omit<RNTextInputProps, 'value' | 'defaultValue' | 'onChangeText'>;
18
+ inputStyle: any;
19
+ };
20
+
21
+ const InnerRNTextInput = React.memo<InnerTextInputProps>(
22
+ ({ inputRef, value, onChangeText, isAndroidSecure, textInputProps, inputStyle }) => {
23
+ return (
24
+ <RNTextInput
25
+ ref={inputRef as any}
26
+ // For Android secure text entry, don't pass value prop at all
27
+ // Let TextInput manage its own state to preserve character reveal animation
28
+ {...(isAndroidSecure ? {} : { value })}
29
+ onChangeText={onChangeText}
30
+ style={inputStyle}
31
+ {...textInputProps}
32
+ />
33
+ );
34
+ },
35
+ (prevProps, nextProps) => {
36
+ // For Android secure text entry, skip re-renders when only value changes
37
+ if (nextProps.isAndroidSecure) {
38
+ // Only re-render if non-value props change
39
+ const valueChanged = prevProps.value !== nextProps.value;
40
+ const otherPropsChanged =
41
+ prevProps.onChangeText !== nextProps.onChangeText ||
42
+ prevProps.isAndroidSecure !== nextProps.isAndroidSecure ||
43
+ prevProps.textInputProps !== nextProps.textInputProps ||
44
+ prevProps.inputStyle !== nextProps.inputStyle;
45
+
46
+ if (valueChanged && !otherPropsChanged) {
47
+ return true; // Skip re-render
48
+ }
49
+ }
50
+ return false; // Allow re-render
51
+ }
52
+ );
53
+
54
+ const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
55
+ value,
56
+ onChangeText,
57
+ onFocus,
58
+ onBlur,
59
+ onPress,
60
+ placeholder,
61
+ disabled = false,
62
+ inputMode = 'text',
63
+ secureTextEntry = false,
64
+ leftIcon,
65
+ rightIcon,
66
+ showPasswordToggle,
67
+ autoCapitalize = 'sentences',
68
+ size = 'md',
69
+ type = 'outlined',
70
+ hasError = false,
71
+ // Spacing variants from FormInputStyleProps
72
+ margin,
73
+ marginVertical,
74
+ marginHorizontal,
75
+ style,
76
+ testID,
77
+ id,
78
+ // Accessibility props
79
+ accessibilityLabel,
80
+ accessibilityHint,
81
+ accessibilityDisabled,
82
+ accessibilityHidden,
83
+ accessibilityRole,
84
+ accessibilityRequired,
85
+ accessibilityInvalid,
86
+ }, ref) => {
87
+ const [isFocused, setIsFocused] = useState(false);
88
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
89
+
90
+ // Track if this is a secure field that needs Android workaround
91
+ const isSecureField = inputMode === 'password' || secureTextEntry;
92
+ const needsAndroidSecureWorkaround = Platform.OS === 'android' && isSecureField && !isPasswordVisible;
93
+
94
+ // For Android secure text entry, we use an internal ref to track value
95
+ const internalValueRef = useRef(value ?? '');
96
+
97
+ // Sync external value changes to internal ref (for programmatic updates)
98
+ useEffect(() => {
99
+ if (value !== undefined) {
100
+ internalValueRef.current = value;
101
+ }
102
+ }, [value]);
103
+
104
+ // Get theme for icon sizes and colors
105
+ const { theme } = useUnistyles();
106
+ const iconSize = theme.sizes.input[size].iconSize;
107
+ const iconColor = theme.colors.text.secondary;
108
+
109
+ // Determine if we should show password toggle
110
+ const isPasswordField = inputMode === 'password' || secureTextEntry;
111
+ const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
112
+
113
+ const getKeyboardType = useCallback((): 'default' | 'email-address' | 'numeric' => {
114
+ switch (inputMode) {
115
+ case 'email':
116
+ return 'email-address';
117
+ case 'number':
118
+ return 'numeric';
119
+ case 'password':
120
+ case 'text':
121
+ default:
122
+ return 'default';
123
+ }
124
+ }, [inputMode]);
125
+
126
+ const handleFocus = useCallback(() => {
127
+ setIsFocused(true);
128
+ onFocus?.();
129
+ }, [onFocus]);
130
+
131
+ const handlePress = useCallback(() => {
132
+ onPress?.();
133
+ }, [onPress]);
134
+
135
+ const handleBlur = useCallback(() => {
136
+ setIsFocused(false);
137
+ onBlur?.();
138
+ }, [onBlur]);
139
+
140
+ const togglePasswordVisibility = () => {
141
+ setIsPasswordVisible(!isPasswordVisible);
142
+ };
143
+
144
+ // Memoized change handler for InnerTextInput
145
+ const handleChangeText = useCallback((text: string) => {
146
+ internalValueRef.current = text;
147
+ onChangeText?.(text);
148
+ }, [onChangeText]);
149
+
150
+ // Memoized input style
151
+ const inputStyle = useMemo(() => (textInputStyles.input as any)({}), []);
152
+
153
+ // Generate native accessibility props
154
+ const nativeA11yProps = useMemo(() => {
155
+ // Derive invalid state from hasError or explicit accessibilityInvalid
156
+ const isInvalid = accessibilityInvalid ?? hasError;
157
+
158
+ return getNativeFormAccessibilityProps({
159
+ accessibilityLabel,
160
+ accessibilityHint,
161
+ accessibilityDisabled: accessibilityDisabled ?? disabled,
162
+ accessibilityHidden,
163
+ accessibilityRole: accessibilityRole ?? 'textbox',
164
+ accessibilityRequired,
165
+ accessibilityInvalid: isInvalid,
166
+ });
167
+ }, [
168
+ accessibilityLabel,
169
+ accessibilityHint,
170
+ accessibilityDisabled,
171
+ disabled,
172
+ accessibilityHidden,
173
+ accessibilityRole,
174
+ accessibilityRequired,
175
+ accessibilityInvalid,
176
+ hasError,
177
+ ]);
178
+
179
+ // Memoized TextInput props (everything except value/onChangeText)
180
+ const textInputProps = useMemo(() => ({
181
+ onPress: handlePress,
182
+ placeholder,
183
+ editable: !disabled,
184
+ keyboardType: getKeyboardType(),
185
+ secureTextEntry: isSecureField && !isPasswordVisible,
186
+ autoCapitalize,
187
+ onFocus: handleFocus,
188
+ onBlur: handleBlur,
189
+ placeholderTextColor: '#999999',
190
+ ...nativeA11yProps,
191
+ }), [
192
+ handlePress,
193
+ placeholder,
194
+ disabled,
195
+ getKeyboardType,
196
+ isSecureField,
197
+ isPasswordVisible,
198
+ autoCapitalize,
199
+ handleFocus,
200
+ handleBlur,
201
+ nativeA11yProps,
202
+ ]);
203
+
204
+ // Apply variants to the stylesheet (for size and spacing)
205
+ textInputStyles.useVariants({
206
+ size,
207
+ margin,
208
+ marginVertical,
209
+ marginHorizontal,
210
+ });
211
+
212
+ // Compute dynamic styles - call as functions for theme reactivity
213
+ const containerStyle = (textInputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
214
+ const leftIconContainerStyle = (textInputStyles.leftIconContainer as any)({});
215
+ const rightIconContainerStyle = (textInputStyles.rightIconContainer as any)({});
216
+ const passwordToggleStyle = (textInputStyles.passwordToggle as any)({});
217
+
218
+ // Helper to render left icon
219
+ const renderLeftIcon = () => {
220
+ if (!leftIcon) return null;
221
+
222
+ if (typeof leftIcon === 'string') {
223
+ return (
224
+ <MaterialDesignIcons
225
+ name={leftIcon}
226
+ size={iconSize}
227
+ color={iconColor}
228
+ />
229
+ );
230
+ } else if (isValidElement(leftIcon)) {
231
+ return leftIcon;
232
+ }
233
+
234
+ return null;
235
+ };
236
+
237
+ // Helper to render right icon (not password toggle)
238
+ const renderRightIcon = () => {
239
+ if (!rightIcon) return null;
240
+
241
+ if (typeof rightIcon === 'string') {
242
+ return (
243
+ <MaterialDesignIcons
244
+ name={rightIcon}
245
+ size={iconSize}
246
+ color={iconColor}
247
+ />
248
+ );
249
+ } else if (isValidElement(rightIcon)) {
250
+ return rightIcon;
251
+ }
252
+
253
+ return null;
254
+ };
255
+
256
+ return (
257
+ <View style={[containerStyle, style]} testID={testID} nativeID={id}>
258
+ {/* Left Icon */}
259
+ {leftIcon && (
260
+ <View style={leftIconContainerStyle}>
261
+ {renderLeftIcon()}
262
+ </View>
263
+ )}
264
+
265
+ {/* Input */}
266
+ <InnerRNTextInput
267
+ inputRef={ref}
268
+ value={value}
269
+ onChangeText={handleChangeText}
270
+ isAndroidSecure={needsAndroidSecureWorkaround}
271
+ inputStyle={inputStyle}
272
+ textInputProps={textInputProps}
273
+ />
274
+
275
+ {/* Right Icon or Password Toggle */}
276
+ {shouldShowPasswordToggle ? (
277
+ <TouchableOpacity
278
+ style={passwordToggleStyle}
279
+ onPress={togglePasswordVisibility}
280
+ disabled={disabled}
281
+ accessibilityLabel={isPasswordVisible ? 'Hide password' : 'Show password'}
282
+ >
283
+ <MaterialDesignIcons
284
+ name={isPasswordVisible ? 'eye-off' : 'eye'}
285
+ size={iconSize}
286
+ color={iconColor}
287
+ />
288
+ </TouchableOpacity>
289
+ ) : rightIcon ? (
290
+ <View style={rightIconContainerStyle}>
291
+ {renderRightIcon()}
292
+ </View>
293
+ ) : null}
294
+ </View>
295
+ );
296
+ });
297
+
298
+ TextInput.displayName = 'TextInput';
299
+
300
+ export default TextInput;