@idealyst/components 1.2.29 → 1.2.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.
- package/README.md +3 -3
- package/package.json +4 -4
- package/plugin/__tests__/web.test.ts +2 -2
- package/plugin/web.js +2 -0
- package/src/Accordion/Accordion.native.tsx +3 -2
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +4 -2
- package/src/ActivityIndicator/ActivityIndicator.styles.tsx +22 -27
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +17 -29
- package/src/Alert/Alert.native.tsx +20 -10
- package/src/Alert/Alert.styles.tsx +173 -86
- package/src/Alert/Alert.web.tsx +34 -30
- package/src/Alert/types.ts +53 -3
- package/src/Avatar/Avatar.native.tsx +3 -2
- package/src/Avatar/Avatar.web.tsx +2 -1
- package/src/Avatar/types.ts +1 -1
- package/src/Badge/Badge.native.tsx +18 -6
- package/src/Badge/Badge.styles.tsx +22 -5
- package/src/Badge/Badge.web.tsx +12 -4
- package/src/Badge/types.ts +14 -2
- package/src/Breadcrumb/Breadcrumb.native.tsx +3 -2
- package/src/Button/Button.native.tsx +16 -6
- package/src/Button/Button.styles.tsx +2 -2
- package/src/Button/Button.web.tsx +19 -15
- package/src/Button/types.ts +6 -10
- package/src/Card/Card.native.tsx +27 -3
- package/src/Card/Card.web.tsx +30 -4
- package/src/Card/types.ts +15 -0
- package/src/Checkbox/Checkbox.native.tsx +5 -4
- package/src/Checkbox/Checkbox.styles.tsx +62 -52
- package/src/Checkbox/Checkbox.web.tsx +4 -3
- package/src/Checkbox/types.ts +1 -1
- package/src/Chip/Chip.native.tsx +30 -7
- package/src/Chip/Chip.web.tsx +28 -5
- package/src/Chip/types.ts +15 -0
- package/src/Dialog/Dialog.native.tsx +6 -6
- package/src/Dialog/Dialog.web.tsx +5 -5
- package/src/Dialog/types.ts +2 -2
- package/src/Divider/Divider.native.tsx +20 -17
- package/src/Divider/Divider.styles.tsx +51 -29
- package/src/Divider/Divider.web.tsx +5 -4
- package/src/Divider/types.ts +3 -3
- package/src/Icon/Icon.native.tsx +3 -2
- package/src/Icon/Icon.web.tsx +2 -1
- package/src/Icon/IconSvg/IconSvg.native.tsx +3 -2
- package/src/IconButton/IconButton.native.tsx +219 -0
- package/src/IconButton/IconButton.styles.tsx +127 -0
- package/src/IconButton/IconButton.web.tsx +198 -0
- package/src/IconButton/index.native.ts +5 -0
- package/src/IconButton/index.ts +5 -0
- package/src/IconButton/index.web.ts +5 -0
- package/src/IconButton/types.ts +84 -0
- package/src/Image/Image.native.tsx +3 -2
- package/src/Input/Input.native.tsx +42 -290
- package/src/Input/Input.styles.tsx +1 -1
- package/src/Input/Input.web.tsx +37 -288
- package/src/Input/index.native.ts +9 -2
- package/src/Input/index.ts +8 -1
- package/src/Input/index.web.ts +8 -1
- package/src/Input/types.ts +1 -1
- package/src/List/List.native.tsx +3 -2
- package/src/List/ListItem.native.tsx +3 -2
- package/src/List/ListSection.native.tsx +3 -2
- package/src/Menu/Menu.native.tsx +2 -1
- package/src/Menu/Menu.styles.tsx +79 -29
- package/src/Menu/Menu.web.tsx +2 -1
- package/src/Menu/MenuItem.native.tsx +4 -3
- package/src/Menu/MenuItem.styles.tsx +81 -32
- package/src/Menu/MenuItem.web.tsx +2 -1
- package/src/Menu/docs.ts +1 -1
- package/src/Popover/Popover.native.tsx +2 -1
- package/src/Popover/Popover.web.tsx +2 -1
- package/src/Popover/types.ts +15 -4
- package/src/Pressable/Pressable.native.tsx +3 -2
- package/src/Pressable/Pressable.web.tsx +3 -5
- package/src/Progress/Progress.native.tsx +5 -4
- package/src/Progress/Progress.web.tsx +3 -3
- package/src/Progress/types.ts +3 -3
- package/src/RadioButton/RadioButton.native.tsx +4 -3
- package/src/RadioButton/RadioButton.styles.tsx +53 -33
- package/src/RadioButton/RadioGroup.native.tsx +3 -2
- package/src/SVGImage/SVGImage.native.tsx +5 -4
- package/src/SVGImage/SVGImage.styles.tsx +44 -10
- package/src/SVGImage/SVGImage.web.tsx +2 -1
- package/src/Screen/Screen.native.tsx +2 -1
- package/src/Screen/Screen.web.tsx +2 -1
- package/src/Select/Select.native.tsx +6 -5
- package/src/Select/Select.styles.tsx +1 -1
- package/src/Select/Select.web.tsx +4 -3
- package/src/Select/types.ts +1 -1
- package/src/Skeleton/Skeleton.native.tsx +2 -1
- package/src/Skeleton/Skeleton.web.tsx +1 -1
- package/src/Slider/Slider.native.tsx +9 -8
- package/src/Slider/Slider.web.tsx +10 -9
- package/src/Slider/types.ts +9 -2
- package/src/Switch/Switch.native.tsx +7 -6
- package/src/Switch/Switch.styles.tsx +52 -17
- package/src/Switch/Switch.web.tsx +15 -16
- package/src/Switch/types.ts +44 -4
- package/src/TabBar/TabBar.native.tsx +3 -2
- package/src/Text/Text.native.tsx +3 -2
- package/src/Text/Text.web.tsx +2 -1
- package/src/TextArea/TextArea.native.tsx +3 -2
- package/src/TextArea/TextArea.styles.tsx +2 -2
- package/src/TextArea/TextArea.web.tsx +2 -1
- package/src/TextInput/TextInput.native.tsx +300 -0
- package/src/TextInput/TextInput.styles.tsx +207 -0
- package/src/TextInput/TextInput.web.tsx +301 -0
- package/src/TextInput/index.native.ts +3 -0
- package/src/TextInput/index.ts +5 -0
- package/src/TextInput/index.web.ts +5 -0
- package/src/TextInput/types.ts +163 -0
- package/src/Tooltip/Tooltip.native.tsx +3 -2
- package/src/Video/Video.native.tsx +4 -3
- package/src/View/View.native.tsx +2 -1
- package/src/View/View.styles.tsx +1 -0
- package/src/View/View.web.tsx +9 -2
- package/src/examples/ActivityIndicatorExamples.tsx +177 -0
- package/src/examples/AlertExamples.tsx +5 -5
- package/src/examples/ButtonExamples.tsx +12 -12
- package/src/examples/CardExamples.tsx +1 -1
- package/src/examples/CheckboxExamples.tsx +2 -2
- package/src/examples/ChipExamples.tsx +6 -6
- package/src/examples/DialogExamples.tsx +1 -1
- package/src/examples/DividerExamples.tsx +1 -1
- package/src/examples/InputExamples.tsx +1 -1
- package/src/examples/LinkExamples.tsx +1 -1
- package/src/examples/ListExamples.tsx +1 -1
- package/src/examples/MenuExamples.tsx +2 -2
- package/src/examples/ProgressExamples.tsx +1 -1
- package/src/examples/RadioButtonExamples.tsx +5 -5
- package/src/examples/SVGImageExamples.tsx +1 -1
- package/src/examples/SelectExamples.tsx +1 -1
- package/src/examples/SliderExamples.tsx +5 -5
- package/src/examples/SwitchExamples.tsx +26 -26
- package/src/examples/TableExamples.tsx +1 -1
- package/src/examples/TooltipExamples.tsx +2 -2
- package/src/examples/index.ts +1 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/types.ts +22 -3
- package/src/index.native.ts +4 -0
- package/src/index.ts +27 -2
- package/src/utils/index.ts +12 -0
- package/src/utils/refTypes.ts +50 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextInput styles using defineStyle with $iterator expansion.
|
|
3
|
+
*/
|
|
4
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
5
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
6
|
+
import type { Theme as BaseTheme } from '@idealyst/theme';
|
|
7
|
+
import { ViewStyleSize } from '../utils/viewStyleProps';
|
|
8
|
+
|
|
9
|
+
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
10
|
+
void StyleSheet;
|
|
11
|
+
|
|
12
|
+
// Wrap theme for $iterator support
|
|
13
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
14
|
+
|
|
15
|
+
type TextInputType = 'outlined' | 'filled' | 'bare';
|
|
16
|
+
|
|
17
|
+
export type TextInputDynamicProps = {
|
|
18
|
+
type?: TextInputType;
|
|
19
|
+
focused?: boolean;
|
|
20
|
+
hasError?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
margin?: ViewStyleSize;
|
|
23
|
+
marginVertical?: ViewStyleSize;
|
|
24
|
+
marginHorizontal?: ViewStyleSize;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* TextInput styles with type/state handling.
|
|
29
|
+
*/
|
|
30
|
+
export const textInputStyles = defineStyle('TextInput', (theme: Theme) => ({
|
|
31
|
+
container: ({ type = 'outlined', focused = false, hasError = false, disabled = false }: TextInputDynamicProps) => ({
|
|
32
|
+
display: 'flex' as const,
|
|
33
|
+
flexDirection: 'row' as const,
|
|
34
|
+
alignItems: 'center' as const,
|
|
35
|
+
width: '100%',
|
|
36
|
+
minWidth: 0,
|
|
37
|
+
borderRadius: theme.radii.md,
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderStyle: 'solid' as const,
|
|
40
|
+
borderColor: theme.colors.border.primary,
|
|
41
|
+
variants: {
|
|
42
|
+
type: {
|
|
43
|
+
outlined: {
|
|
44
|
+
backgroundColor: theme.colors.surface.primary,
|
|
45
|
+
borderColor: theme.colors.border.primary,
|
|
46
|
+
},
|
|
47
|
+
filled: {
|
|
48
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
49
|
+
borderColor: 'transparent',
|
|
50
|
+
},
|
|
51
|
+
bare: {
|
|
52
|
+
backgroundColor: 'transparent',
|
|
53
|
+
borderWidth: 0,
|
|
54
|
+
borderColor: 'transparent',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
focused: {
|
|
58
|
+
true: {
|
|
59
|
+
borderColor: theme.intents.primary.primary,
|
|
60
|
+
},
|
|
61
|
+
false: {
|
|
62
|
+
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
disabled: {
|
|
66
|
+
true: { opacity: 0.8, _web: { cursor: 'not-allowed' } },
|
|
67
|
+
false: { opacity: 1 }
|
|
68
|
+
},
|
|
69
|
+
hasError: {
|
|
70
|
+
true: { borderColor: theme.intents.danger.primary },
|
|
71
|
+
false: { borderColor: theme.colors.border.primary },
|
|
72
|
+
},
|
|
73
|
+
// $iterator expands for each input size
|
|
74
|
+
size: {
|
|
75
|
+
height: theme.sizes.$input.height,
|
|
76
|
+
paddingHorizontal: theme.sizes.$input.paddingHorizontal,
|
|
77
|
+
},
|
|
78
|
+
margin: {
|
|
79
|
+
margin: theme.sizes.$view.padding,
|
|
80
|
+
},
|
|
81
|
+
marginVertical: {
|
|
82
|
+
marginVertical: theme.sizes.$view.padding,
|
|
83
|
+
},
|
|
84
|
+
marginHorizontal: {
|
|
85
|
+
marginHorizontal: theme.sizes.$view.padding,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
_web: {
|
|
89
|
+
boxSizing: 'border-box',
|
|
90
|
+
transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
|
|
94
|
+
input: (_props: TextInputDynamicProps) => ({
|
|
95
|
+
flex: 1,
|
|
96
|
+
minWidth: 0,
|
|
97
|
+
backgroundColor: 'transparent' as const,
|
|
98
|
+
color: theme.colors.text.primary,
|
|
99
|
+
fontWeight: '400' as const,
|
|
100
|
+
variants: {
|
|
101
|
+
size: {
|
|
102
|
+
fontSize: theme.sizes.$input.fontSize,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
_web: {
|
|
106
|
+
border: 'none',
|
|
107
|
+
outline: 'none',
|
|
108
|
+
fontFamily: 'inherit',
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
leftIconContainer: (_props: TextInputDynamicProps) => ({
|
|
113
|
+
display: 'flex' as const,
|
|
114
|
+
alignItems: 'center' as const,
|
|
115
|
+
justifyContent: 'center' as const,
|
|
116
|
+
flexShrink: 0,
|
|
117
|
+
variants: {
|
|
118
|
+
size: {
|
|
119
|
+
width: theme.sizes.$input.iconSize,
|
|
120
|
+
marginRight: theme.sizes.$input.iconMargin,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
rightIconContainer: (_props: TextInputDynamicProps) => ({
|
|
126
|
+
display: 'flex' as const,
|
|
127
|
+
alignItems: 'center' as const,
|
|
128
|
+
justifyContent: 'center' as const,
|
|
129
|
+
flexShrink: 0,
|
|
130
|
+
variants: {
|
|
131
|
+
size: {
|
|
132
|
+
width: theme.sizes.$input.iconSize,
|
|
133
|
+
marginLeft: theme.sizes.$input.iconMargin,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
leftIcon: (_props: TextInputDynamicProps) => ({
|
|
139
|
+
color: theme.colors.text.secondary,
|
|
140
|
+
variants: {
|
|
141
|
+
size: {
|
|
142
|
+
fontSize: theme.sizes.$input.iconSize,
|
|
143
|
+
width: theme.sizes.$input.iconSize,
|
|
144
|
+
height: theme.sizes.$input.iconSize,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
rightIcon: (_props: TextInputDynamicProps) => ({
|
|
150
|
+
display: 'flex' as const,
|
|
151
|
+
alignItems: 'center' as const,
|
|
152
|
+
justifyContent: 'center' as const,
|
|
153
|
+
flexShrink: 0,
|
|
154
|
+
color: theme.colors.text.secondary,
|
|
155
|
+
variants: {
|
|
156
|
+
size: {
|
|
157
|
+
fontSize: theme.sizes.$input.iconSize,
|
|
158
|
+
width: theme.sizes.$input.iconSize,
|
|
159
|
+
height: theme.sizes.$input.iconSize,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
passwordToggle: (_props: TextInputDynamicProps) => ({
|
|
165
|
+
display: 'flex' as const,
|
|
166
|
+
alignItems: 'center' as const,
|
|
167
|
+
justifyContent: 'center' as const,
|
|
168
|
+
flexShrink: 0,
|
|
169
|
+
padding: 0,
|
|
170
|
+
variants: {
|
|
171
|
+
size: {
|
|
172
|
+
marginLeft: theme.sizes.$input.iconMargin,
|
|
173
|
+
width: theme.sizes.$input.iconSize,
|
|
174
|
+
height: theme.sizes.$input.iconSize,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
_web: {
|
|
178
|
+
appearance: 'none',
|
|
179
|
+
background: 'none',
|
|
180
|
+
backgroundColor: 'transparent',
|
|
181
|
+
border: 'none',
|
|
182
|
+
outline: 'none',
|
|
183
|
+
cursor: 'pointer',
|
|
184
|
+
margin: 0,
|
|
185
|
+
_hover: { opacity: 0.7 },
|
|
186
|
+
_active: { opacity: 0.5 },
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
passwordToggleIcon: (_props: TextInputDynamicProps) => ({
|
|
191
|
+
display: 'flex' as const,
|
|
192
|
+
alignItems: 'center' as const,
|
|
193
|
+
justifyContent: 'center' as const,
|
|
194
|
+
flexShrink: 0,
|
|
195
|
+
color: theme.colors.text.secondary,
|
|
196
|
+
variants: {
|
|
197
|
+
size: {
|
|
198
|
+
fontSize: theme.sizes.$input.iconSize,
|
|
199
|
+
width: theme.sizes.$input.iconSize,
|
|
200
|
+
height: theme.sizes.$input.iconSize,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
// Legacy export for backwards compatibility
|
|
207
|
+
export { textInputStyles as inputStyles };
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import React, { isValidElement, useState, useMemo, useRef } from 'react';
|
|
2
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
|
+
import { useUnistyles } from 'react-native-unistyles';
|
|
4
|
+
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
5
|
+
import { isIconName } from '../Icon/icon-resolver';
|
|
6
|
+
import useMergeRefs from '../hooks/useMergeRefs';
|
|
7
|
+
import { textInputStyles } from './TextInput.styles';
|
|
8
|
+
import { TextInputProps } from './types';
|
|
9
|
+
import { getWebFormAriaProps } from '../utils/accessibility';
|
|
10
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Single-line text input field with support for icons, password visibility toggle, and validation states.
|
|
14
|
+
* Available in outlined and filled variants with multiple sizes.
|
|
15
|
+
*/
|
|
16
|
+
const TextInput = React.forwardRef<IdealystElement, TextInputProps>(({
|
|
17
|
+
value,
|
|
18
|
+
onChangeText,
|
|
19
|
+
onFocus,
|
|
20
|
+
onBlur,
|
|
21
|
+
onPress,
|
|
22
|
+
placeholder,
|
|
23
|
+
disabled = false,
|
|
24
|
+
inputMode = 'text',
|
|
25
|
+
secureTextEntry = false,
|
|
26
|
+
leftIcon,
|
|
27
|
+
rightIcon,
|
|
28
|
+
showPasswordToggle,
|
|
29
|
+
autoCapitalize = 'sentences',
|
|
30
|
+
size = 'md',
|
|
31
|
+
type = 'outlined',
|
|
32
|
+
hasError = false,
|
|
33
|
+
// Spacing variants from FormInputStyleProps
|
|
34
|
+
margin,
|
|
35
|
+
marginVertical,
|
|
36
|
+
marginHorizontal,
|
|
37
|
+
style,
|
|
38
|
+
testID,
|
|
39
|
+
id,
|
|
40
|
+
// Accessibility props
|
|
41
|
+
accessibilityLabel,
|
|
42
|
+
accessibilityHint,
|
|
43
|
+
accessibilityDisabled,
|
|
44
|
+
accessibilityHidden,
|
|
45
|
+
accessibilityRole,
|
|
46
|
+
accessibilityLabelledBy,
|
|
47
|
+
accessibilityDescribedBy,
|
|
48
|
+
accessibilityControls,
|
|
49
|
+
accessibilityExpanded,
|
|
50
|
+
accessibilityPressed,
|
|
51
|
+
accessibilityOwns,
|
|
52
|
+
accessibilityHasPopup,
|
|
53
|
+
accessibilityRequired,
|
|
54
|
+
accessibilityInvalid,
|
|
55
|
+
accessibilityErrorMessage,
|
|
56
|
+
accessibilityAutoComplete,
|
|
57
|
+
}, ref) => {
|
|
58
|
+
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
59
|
+
|
|
60
|
+
// Determine if we should show password toggle
|
|
61
|
+
const isPasswordField = inputMode === 'password' || secureTextEntry;
|
|
62
|
+
const shouldShowPasswordToggle = isPasswordField && (showPasswordToggle !== false);
|
|
63
|
+
|
|
64
|
+
// Get theme for icon sizes and colors
|
|
65
|
+
const { theme } = useUnistyles();
|
|
66
|
+
const iconSize = theme.sizes.input[size].iconSize;
|
|
67
|
+
const iconColor = theme.colors.text.secondary;
|
|
68
|
+
|
|
69
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
70
|
+
|
|
71
|
+
const getInputType = () => {
|
|
72
|
+
// Handle password visibility
|
|
73
|
+
if (isPasswordField && !isPasswordVisible) {
|
|
74
|
+
return 'password';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
switch (inputMode) {
|
|
78
|
+
case 'email':
|
|
79
|
+
return 'email';
|
|
80
|
+
case 'number':
|
|
81
|
+
return 'number';
|
|
82
|
+
case 'password':
|
|
83
|
+
return 'text'; // When visible
|
|
84
|
+
case 'text':
|
|
85
|
+
default:
|
|
86
|
+
return 'text';
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
91
|
+
if (onChangeText) {
|
|
92
|
+
onChangeText(e.target.value);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handlePress = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
97
|
+
// For web compatibility, we can trigger onFocus when pressed
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
if (onPress) {
|
|
101
|
+
onPress();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
setIsFocused(true);
|
|
109
|
+
if (onFocus) {
|
|
110
|
+
onFocus();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleBlur = () => {
|
|
115
|
+
setIsFocused(false);
|
|
116
|
+
if (onBlur) {
|
|
117
|
+
onBlur();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const togglePasswordVisibility = () => {
|
|
122
|
+
setIsPasswordVisible(!isPasswordVisible);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Apply variants (for size and spacing)
|
|
126
|
+
textInputStyles.useVariants({
|
|
127
|
+
size,
|
|
128
|
+
margin,
|
|
129
|
+
marginVertical,
|
|
130
|
+
marginHorizontal,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Get web props for all styled elements (all styles are dynamic functions)
|
|
134
|
+
const dynamicContainerStyle = (textInputStyles.container as any)({ type, focused: isFocused, hasError, disabled });
|
|
135
|
+
const {ref: containerStyleRef, ...containerProps} = getWebProps([dynamicContainerStyle, style]);
|
|
136
|
+
const leftIconContainerProps = getWebProps([(textInputStyles.leftIconContainer as any)({})]);
|
|
137
|
+
const rightIconContainerProps = getWebProps([(textInputStyles.rightIconContainer as any)({})]);
|
|
138
|
+
const passwordToggleProps = getWebProps([(textInputStyles.passwordToggle as any)({})]);
|
|
139
|
+
|
|
140
|
+
// Get input props
|
|
141
|
+
const inputWebProps = getWebProps([(textInputStyles.input as any)({})]);
|
|
142
|
+
|
|
143
|
+
// Generate accessibility props
|
|
144
|
+
const ariaProps = useMemo(() => {
|
|
145
|
+
// Derive invalid state from hasError or explicit accessibilityInvalid
|
|
146
|
+
const isInvalid = accessibilityInvalid ?? hasError;
|
|
147
|
+
|
|
148
|
+
return getWebFormAriaProps({
|
|
149
|
+
accessibilityLabel,
|
|
150
|
+
accessibilityHint,
|
|
151
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
152
|
+
accessibilityHidden,
|
|
153
|
+
accessibilityRole: accessibilityRole ?? 'textbox',
|
|
154
|
+
accessibilityLabelledBy,
|
|
155
|
+
accessibilityDescribedBy,
|
|
156
|
+
accessibilityControls,
|
|
157
|
+
accessibilityExpanded,
|
|
158
|
+
accessibilityPressed,
|
|
159
|
+
accessibilityOwns,
|
|
160
|
+
accessibilityHasPopup,
|
|
161
|
+
accessibilityRequired,
|
|
162
|
+
accessibilityInvalid: isInvalid,
|
|
163
|
+
accessibilityErrorMessage,
|
|
164
|
+
accessibilityAutoComplete,
|
|
165
|
+
});
|
|
166
|
+
}, [
|
|
167
|
+
accessibilityLabel,
|
|
168
|
+
accessibilityHint,
|
|
169
|
+
accessibilityDisabled,
|
|
170
|
+
disabled,
|
|
171
|
+
accessibilityHidden,
|
|
172
|
+
accessibilityRole,
|
|
173
|
+
accessibilityLabelledBy,
|
|
174
|
+
accessibilityDescribedBy,
|
|
175
|
+
accessibilityControls,
|
|
176
|
+
accessibilityExpanded,
|
|
177
|
+
accessibilityPressed,
|
|
178
|
+
accessibilityOwns,
|
|
179
|
+
accessibilityHasPopup,
|
|
180
|
+
accessibilityRequired,
|
|
181
|
+
accessibilityInvalid,
|
|
182
|
+
hasError,
|
|
183
|
+
accessibilityErrorMessage,
|
|
184
|
+
accessibilityAutoComplete,
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
// Merge the forwarded ref with unistyles ref for the input
|
|
188
|
+
const mergedInputRef = useMergeRefs(ref, inputWebProps.ref);
|
|
189
|
+
|
|
190
|
+
// Helper to render left icon
|
|
191
|
+
const renderLeftIcon = () => {
|
|
192
|
+
if (!leftIcon) return null;
|
|
193
|
+
|
|
194
|
+
if (isIconName(leftIcon)) {
|
|
195
|
+
return (
|
|
196
|
+
<IconSvg
|
|
197
|
+
name={leftIcon}
|
|
198
|
+
size={iconSize}
|
|
199
|
+
color={iconColor}
|
|
200
|
+
aria-label={leftIcon}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
} else if (isValidElement(leftIcon)) {
|
|
204
|
+
return <span {...leftIconContainerProps}>{leftIcon}</span>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Helper to render right icon (not password toggle)
|
|
211
|
+
const renderRightIcon = () => {
|
|
212
|
+
if (!rightIcon) return null;
|
|
213
|
+
|
|
214
|
+
if (isIconName(rightIcon)) {
|
|
215
|
+
return (
|
|
216
|
+
<IconSvg
|
|
217
|
+
name={rightIcon}
|
|
218
|
+
size={iconSize}
|
|
219
|
+
color={iconColor}
|
|
220
|
+
aria-label={rightIcon}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
} else if (isValidElement(rightIcon)) {
|
|
224
|
+
return <span {...rightIconContainerProps}>{rightIcon}</span>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Helper to render password toggle icon
|
|
231
|
+
const renderPasswordToggleIcon = () => {
|
|
232
|
+
const iconName = isPasswordVisible ? 'eye-off' : 'eye';
|
|
233
|
+
return (
|
|
234
|
+
<IconSvg
|
|
235
|
+
name={iconName}
|
|
236
|
+
size={iconSize}
|
|
237
|
+
color={iconColor}
|
|
238
|
+
aria-label={iconName}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
244
|
+
|
|
245
|
+
const handleContainerPress = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
e.stopPropagation();
|
|
248
|
+
containerRef.current?.focus();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const mergedContainerRef = useMergeRefs(containerRef, containerStyleRef);
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div onClick={handleContainerPress} ref={mergedContainerRef} {...containerProps} id={id} data-testid={testID}>
|
|
255
|
+
{/* Left Icon */}
|
|
256
|
+
{leftIcon && (
|
|
257
|
+
<span {...leftIconContainerProps}>
|
|
258
|
+
{renderLeftIcon()}
|
|
259
|
+
</span>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{/* Input */}
|
|
263
|
+
<input
|
|
264
|
+
{...inputWebProps}
|
|
265
|
+
{...ariaProps}
|
|
266
|
+
ref={mergedInputRef}
|
|
267
|
+
type={getInputType()}
|
|
268
|
+
value={value}
|
|
269
|
+
onClick={handlePress}
|
|
270
|
+
onChange={handleChange}
|
|
271
|
+
onFocus={handleFocus}
|
|
272
|
+
onBlur={handleBlur}
|
|
273
|
+
placeholder={placeholder}
|
|
274
|
+
disabled={disabled}
|
|
275
|
+
autoCapitalize={autoCapitalize}
|
|
276
|
+
/>
|
|
277
|
+
|
|
278
|
+
{/* Right Icon or Password Toggle */}
|
|
279
|
+
{shouldShowPasswordToggle ? (
|
|
280
|
+
<button
|
|
281
|
+
{...passwordToggleProps}
|
|
282
|
+
onClick={togglePasswordVisibility}
|
|
283
|
+
disabled={disabled}
|
|
284
|
+
aria-label={isPasswordVisible ? 'Hide password' : 'Show password'}
|
|
285
|
+
type="button"
|
|
286
|
+
tabIndex={-1}
|
|
287
|
+
>
|
|
288
|
+
{renderPasswordToggleIcon()}
|
|
289
|
+
</button>
|
|
290
|
+
) : rightIcon ? (
|
|
291
|
+
<span {...rightIconContainerProps}>
|
|
292
|
+
{renderRightIcon()}
|
|
293
|
+
</span>
|
|
294
|
+
) : null}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
TextInput.displayName = 'TextInput';
|
|
300
|
+
|
|
301
|
+
export default TextInput;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { IconName } from '../Icon/icon-types';
|
|
3
|
+
import { Intent, Size } from '@idealyst/theme';
|
|
4
|
+
import { FormInputStyleProps } from '../utils/viewStyleProps';
|
|
5
|
+
import { FormAccessibilityProps } from '../utils/accessibility';
|
|
6
|
+
|
|
7
|
+
// Component-specific type aliases for future extensibility
|
|
8
|
+
export type TextInputIntent = Intent;
|
|
9
|
+
export type TextInputSize = Size;
|
|
10
|
+
export type TextInputType = 'outlined' | 'filled' | 'bare';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Input mode for keyboard type on mobile platforms.
|
|
14
|
+
* This prop only affects React Native - on web, browser handles keyboard automatically.
|
|
15
|
+
* @platform native
|
|
16
|
+
*/
|
|
17
|
+
export type TextInputMode = 'text' | 'email' | 'password' | 'number';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Single-line text input field with support for icons, validation states, and multiple visual styles.
|
|
21
|
+
* Includes built-in password visibility toggle and platform-specific keyboard handling.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // Basic usage
|
|
26
|
+
* <TextInput
|
|
27
|
+
* value={email}
|
|
28
|
+
* onChangeText={setEmail}
|
|
29
|
+
* placeholder="Enter email"
|
|
30
|
+
* inputMode="email"
|
|
31
|
+
* />
|
|
32
|
+
*
|
|
33
|
+
* // Password with toggle
|
|
34
|
+
* <TextInput
|
|
35
|
+
* value={password}
|
|
36
|
+
* onChangeText={setPassword}
|
|
37
|
+
* inputMode="password"
|
|
38
|
+
* secureTextEntry
|
|
39
|
+
* />
|
|
40
|
+
*
|
|
41
|
+
* // With icons and validation
|
|
42
|
+
* <TextInput
|
|
43
|
+
* value={search}
|
|
44
|
+
* onChangeText={setSearch}
|
|
45
|
+
* leftIcon="magnify"
|
|
46
|
+
* intent="danger"
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface TextInputProps extends FormInputStyleProps, FormAccessibilityProps {
|
|
51
|
+
/**
|
|
52
|
+
* The current value of the input
|
|
53
|
+
*/
|
|
54
|
+
value?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Called when the text changes
|
|
58
|
+
*/
|
|
59
|
+
onChangeText?: (text: string) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Called when the input receives focus
|
|
63
|
+
*/
|
|
64
|
+
onFocus?: () => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Called when the input loses focus
|
|
68
|
+
*/
|
|
69
|
+
onBlur?: () => void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Called when the input is pressed
|
|
73
|
+
*/
|
|
74
|
+
onPress?: () => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Placeholder text shown when the input is empty
|
|
78
|
+
*/
|
|
79
|
+
placeholder?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether the input is disabled
|
|
83
|
+
*/
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The type of input keyboard to show on mobile platforms.
|
|
88
|
+
* This prop only affects React Native - on web, browser handles keyboard automatically.
|
|
89
|
+
* @platform native
|
|
90
|
+
*/
|
|
91
|
+
inputMode?: TextInputMode;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether to hide the text (for passwords)
|
|
95
|
+
*/
|
|
96
|
+
secureTextEntry?: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Icon to display on the left side of the input.
|
|
100
|
+
* Can be an icon name string or a custom React node.
|
|
101
|
+
*/
|
|
102
|
+
leftIcon?: IconName | React.ReactNode;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Icon to display on the right side of the input.
|
|
106
|
+
* Can be an icon name string or a custom React node.
|
|
107
|
+
*/
|
|
108
|
+
rightIcon?: IconName | React.ReactNode;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Show password visibility toggle for password inputs.
|
|
112
|
+
* Defaults to true when inputMode="password" or secureTextEntry is true.
|
|
113
|
+
*/
|
|
114
|
+
showPasswordToggle?: boolean;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Auto-capitalization behavior
|
|
118
|
+
*/
|
|
119
|
+
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Size variant of the input
|
|
123
|
+
*/
|
|
124
|
+
size?: TextInputSize;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Visual style type of the input
|
|
128
|
+
*/
|
|
129
|
+
type?: TextInputType;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The intent/color scheme of the input (for focus states, validation, etc.)
|
|
133
|
+
*/
|
|
134
|
+
intent?: TextInputIntent;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether the input has an error state
|
|
138
|
+
* @deprecated Use intent="danger" instead
|
|
139
|
+
*/
|
|
140
|
+
hasError?: boolean;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Additional styles (platform-specific)
|
|
144
|
+
*/
|
|
145
|
+
style?: StyleProp<ViewStyle>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Test ID for testing
|
|
149
|
+
*/
|
|
150
|
+
testID?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Legacy type aliases for backwards compatibility
|
|
154
|
+
/** @deprecated Use TextInputIntent instead */
|
|
155
|
+
export type InputIntent = TextInputIntent;
|
|
156
|
+
/** @deprecated Use TextInputSize instead */
|
|
157
|
+
export type InputSize = TextInputSize;
|
|
158
|
+
/** @deprecated Use TextInputType instead */
|
|
159
|
+
export type InputType = TextInputType;
|
|
160
|
+
/** @deprecated Use TextInputMode instead */
|
|
161
|
+
export type InputInputType = TextInputMode;
|
|
162
|
+
/** @deprecated Use TextInputProps instead */
|
|
163
|
+
export type InputProps = TextInputProps;
|
|
@@ -3,8 +3,9 @@ import { View, Modal, Text, Pressable } from 'react-native';
|
|
|
3
3
|
import { tooltipStyles } from './Tooltip.styles';
|
|
4
4
|
import type { TooltipProps } from './types';
|
|
5
5
|
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
6
|
+
import type { IdealystElement } from '../utils/refTypes';
|
|
6
7
|
|
|
7
|
-
const Tooltip = forwardRef<
|
|
8
|
+
const Tooltip = forwardRef<IdealystElement, TooltipProps>(({
|
|
8
9
|
content,
|
|
9
10
|
children,
|
|
10
11
|
placement = 'top',
|
|
@@ -144,7 +145,7 @@ const Tooltip = forwardRef<View, TooltipProps>(({
|
|
|
144
145
|
|
|
145
146
|
return (
|
|
146
147
|
<>
|
|
147
|
-
<View ref={ref} nativeID={id} collapsable={false} style={style} {...nativeA11yProps}>
|
|
148
|
+
<View ref={ref as any} nativeID={id} collapsable={false} style={style} {...nativeA11yProps}>
|
|
148
149
|
{trigger}
|
|
149
150
|
</View>
|
|
150
151
|
|