@hero-design/rn-work-uikit 1.1.0 → 1.2.0-alpha.0
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/.cursorrules +57 -0
- package/CHANGELOG.md +6 -0
- package/DEVELOPMENT.md +118 -0
- package/eslint.config.js +20 -0
- package/lib/index.js +871 -5
- package/package.json +4 -1
- package/src/__tests__/__snapshots__/index.spec.tsx.snap +90 -115
- package/src/__tests__/theme-export-override.spec.ts +6 -0
- package/src/components/TextInput/ErrorOrHelpText.tsx +58 -0
- package/src/components/TextInput/FloatingLabel.tsx +120 -0
- package/src/components/TextInput/InputComponent.tsx +61 -0
- package/src/components/TextInput/InputRow.tsx +103 -0
- package/src/components/TextInput/MaxLengthMessage.tsx +66 -0
- package/src/components/TextInput/PrefixComponent.tsx +77 -0
- package/src/components/TextInput/StyledTextInput.tsx +134 -0
- package/src/components/TextInput/SuffixComponent.tsx +73 -0
- package/src/components/TextInput/__tests__/ErrorOrHelpText.spec.tsx +20 -0
- package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +203 -0
- package/src/components/TextInput/__tests__/InputComponent.spec.tsx +39 -0
- package/src/components/TextInput/__tests__/InputRow.spec.tsx +275 -0
- package/src/components/TextInput/__tests__/MaxLengthMessage.spec.tsx +17 -0
- package/src/components/TextInput/__tests__/PrefixComponent.spec.tsx +14 -0
- package/src/components/TextInput/__tests__/StyledTextInput.spec.tsx +114 -0
- package/src/components/TextInput/__tests__/SuffixComponent.spec.tsx +20 -0
- package/src/components/TextInput/__tests__/__snapshots__/StyledTextInput.spec.tsx.snap +571 -0
- package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +5671 -0
- package/src/components/TextInput/__tests__/getState.spec.tsx +89 -0
- package/src/components/TextInput/__tests__/index.spec.tsx +699 -0
- package/src/components/TextInput/constants.ts +1 -0
- package/src/components/TextInput/index.tsx +327 -0
- package/src/components/TextInput/types.ts +95 -0
- package/src/emotion.d.ts +15 -0
- package/src/index.ts +3 -0
- package/src/jest.d.ts +24 -0
- package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +15 -8
- package/src/theme/components/textInput.ts +33 -0
- package/src/utils/__tests__/helpers.spec.ts +92 -0
- package/src/utils/helpers.ts +113 -0
- package/testUtils/renderWithTheme.tsx +6 -3
- package/stats/1.1.0/rn-work-uikit-stats.html +0 -4842
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hero-design/rn-work-uikit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-alpha.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.js",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"set-dev-env": "npm pkg set react-native=src/index.ts"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@emotion/native": "^11.9.3",
|
|
24
|
+
"@emotion/primitives-core": "11.0.0",
|
|
25
|
+
"@emotion/react": "^11.9.3",
|
|
23
26
|
"@hero-design/rn": "^8.101.1"
|
|
24
27
|
},
|
|
25
28
|
"peerDependencies": {
|
|
@@ -1,126 +1,120 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`first test to ensure configure correctly should render TextInput with props in snapshot 1`] = `
|
|
4
|
-
<
|
|
5
|
-
|
|
4
|
+
<Pressable
|
|
5
|
+
accessibilityState={
|
|
6
6
|
{
|
|
7
|
-
"
|
|
7
|
+
"disabled": false,
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
disabled={false}
|
|
11
|
+
onPress={[Function]}
|
|
12
|
+
style={
|
|
13
|
+
[
|
|
14
|
+
[
|
|
15
|
+
{
|
|
16
|
+
"flexDirection": "row",
|
|
17
|
+
"marginTop": 7.846153846153847,
|
|
18
|
+
"minHeight": 54.92307692307692,
|
|
19
|
+
"paddingHorizontal": 15.692307692307693,
|
|
20
|
+
"width": "100%",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
{},
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
testID="text-input"
|
|
10
27
|
>
|
|
11
28
|
<View
|
|
12
|
-
pointerEvents="
|
|
29
|
+
pointerEvents="none"
|
|
30
|
+
style={
|
|
31
|
+
[
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
"borderColor": "#e8e9ea",
|
|
35
|
+
"borderRadius": 8,
|
|
36
|
+
"borderWidth": 2,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
"backgroundColor": "#ffffff",
|
|
42
|
+
},
|
|
43
|
+
{},
|
|
44
|
+
],
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
testID="text-input-border"
|
|
48
|
+
themeFocused={false}
|
|
49
|
+
themeState="filled"
|
|
50
|
+
/>
|
|
51
|
+
<View
|
|
13
52
|
style={
|
|
14
53
|
[
|
|
15
54
|
[
|
|
16
55
|
{
|
|
56
|
+
"backgroundColor": "transparent",
|
|
57
|
+
"borderRadius": 8,
|
|
58
|
+
"flex": 1,
|
|
59
|
+
"flexDirection": "column",
|
|
60
|
+
"marginBottom": 7.846153846153847,
|
|
17
61
|
"marginTop": 7.846153846153847,
|
|
18
|
-
"
|
|
62
|
+
"overflow": "hidden",
|
|
19
63
|
},
|
|
20
64
|
],
|
|
21
65
|
undefined,
|
|
22
66
|
]
|
|
23
67
|
}
|
|
24
|
-
testID="text-input"
|
|
25
68
|
>
|
|
26
69
|
<View
|
|
27
|
-
onLayout={[Function]}
|
|
28
70
|
style={
|
|
29
71
|
[
|
|
30
72
|
[
|
|
31
73
|
{
|
|
32
74
|
"alignItems": "center",
|
|
33
|
-
"backgroundColor": "#ffffff",
|
|
34
|
-
"borderRadius": 8,
|
|
35
75
|
"flexDirection": "row",
|
|
36
|
-
"
|
|
76
|
+
"flexGrow": 2,
|
|
77
|
+
"flexShrink": 1,
|
|
78
|
+
"gap": 3.9230769230769234,
|
|
37
79
|
},
|
|
38
80
|
],
|
|
39
81
|
undefined,
|
|
40
82
|
]
|
|
41
83
|
}
|
|
42
84
|
>
|
|
43
|
-
<View
|
|
44
|
-
style={
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
{
|
|
48
|
-
"borderColor": "#001f23",
|
|
49
|
-
"borderRadius": 8,
|
|
50
|
-
"borderWidth": 1,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
[
|
|
54
|
-
{
|
|
55
|
-
"backgroundColor": "#ffffff",
|
|
56
|
-
},
|
|
57
|
-
undefined,
|
|
58
|
-
],
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
testID="text-input-border"
|
|
62
|
-
themeFocused={false}
|
|
63
|
-
themeState="filled"
|
|
64
|
-
/>
|
|
65
|
-
<View />
|
|
66
85
|
<AnimatedView
|
|
67
|
-
pointerEvents="none"
|
|
68
86
|
style={
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
],
|
|
81
|
-
[
|
|
82
|
-
{
|
|
83
|
-
"transformOrigin": "left top",
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"transform": [
|
|
87
|
-
{
|
|
88
|
-
"translateY": {
|
|
89
|
-
"addListener": [MockFunction],
|
|
90
|
-
"removeListener": [MockFunction],
|
|
91
|
-
"setValue": [MockFunction],
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
"scale": {
|
|
96
|
-
"addListener": [MockFunction],
|
|
97
|
-
"removeListener": [MockFunction],
|
|
98
|
-
"setValue": [MockFunction],
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
]
|
|
87
|
+
{
|
|
88
|
+
"opacity": {
|
|
89
|
+
"_offset": 0,
|
|
90
|
+
"_value": 0,
|
|
91
|
+
"addListener": [MockFunction],
|
|
92
|
+
"interpolate": [MockFunction],
|
|
93
|
+
"removeAllListeners": [MockFunction],
|
|
94
|
+
"removeListener": [MockFunction],
|
|
95
|
+
"setValue": [MockFunction],
|
|
96
|
+
},
|
|
97
|
+
}
|
|
105
98
|
}
|
|
106
|
-
themeHasPrefix={false}
|
|
107
|
-
themeVariant="text"
|
|
108
99
|
/>
|
|
109
|
-
<
|
|
100
|
+
<AnimatedView
|
|
101
|
+
accessibilityElementsHidden={false}
|
|
102
|
+
accessibilityLabel="Text input field"
|
|
110
103
|
style={
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
104
|
+
{
|
|
105
|
+
"flex": 1,
|
|
106
|
+
"opacity": {
|
|
107
|
+
"_offset": 0,
|
|
108
|
+
"_value": 0,
|
|
109
|
+
"addListener": [MockFunction],
|
|
110
|
+
"interpolate": [MockFunction],
|
|
111
|
+
"removeAllListeners": [MockFunction],
|
|
112
|
+
"removeListener": [MockFunction],
|
|
113
|
+
"setValue": [MockFunction],
|
|
114
|
+
},
|
|
115
|
+
}
|
|
123
116
|
}
|
|
117
|
+
testID="input-row-input-wrapper"
|
|
124
118
|
>
|
|
125
119
|
<TextInput
|
|
126
120
|
accessibilityState={
|
|
@@ -139,13 +133,12 @@ exports[`first test to ensure configure correctly should render TextInput with p
|
|
|
139
133
|
[
|
|
140
134
|
[
|
|
141
135
|
{
|
|
142
|
-
"alignSelf": "stretch",
|
|
143
136
|
"flexGrow": 2,
|
|
144
137
|
"fontFamily": "BeVietnamPro-Regular",
|
|
145
138
|
"fontSize": 15.692307692307693,
|
|
146
139
|
"height": undefined,
|
|
147
|
-
"marginHorizontal": 7.846153846153847,
|
|
148
140
|
"maxHeight": 141.23076923076923,
|
|
141
|
+
"minHeight": 23.53846153846154,
|
|
149
142
|
"paddingVertical": 0,
|
|
150
143
|
"textAlignVertical": "center",
|
|
151
144
|
},
|
|
@@ -155,7 +148,7 @@ exports[`first test to ensure configure correctly should render TextInput with p
|
|
|
155
148
|
"backgroundColor": "#ffffff",
|
|
156
149
|
"color": "#001f23",
|
|
157
150
|
},
|
|
158
|
-
|
|
151
|
+
{},
|
|
159
152
|
],
|
|
160
153
|
]
|
|
161
154
|
}
|
|
@@ -163,54 +156,36 @@ exports[`first test to ensure configure correctly should render TextInput with p
|
|
|
163
156
|
themeVariant="text"
|
|
164
157
|
value="test value"
|
|
165
158
|
/>
|
|
166
|
-
</
|
|
159
|
+
</AnimatedView>
|
|
167
160
|
</View>
|
|
168
161
|
<View
|
|
169
162
|
style={
|
|
170
163
|
[
|
|
171
164
|
[
|
|
172
165
|
{
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
166
|
+
"alignItems": "flex-start",
|
|
167
|
+
"flexDirection": "row",
|
|
168
|
+
"justifyContent": "space-between",
|
|
176
169
|
},
|
|
177
170
|
],
|
|
178
171
|
undefined,
|
|
179
172
|
]
|
|
180
173
|
}
|
|
181
|
-
|
|
182
|
-
<View
|
|
183
|
-
style={
|
|
184
|
-
[
|
|
185
|
-
[
|
|
186
|
-
{
|
|
187
|
-
"alignItems": "flex-start",
|
|
188
|
-
"flexDirection": "row",
|
|
189
|
-
"justifyContent": "space-between",
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
undefined,
|
|
193
|
-
]
|
|
194
|
-
}
|
|
195
|
-
/>
|
|
196
|
-
</View>
|
|
174
|
+
/>
|
|
197
175
|
</View>
|
|
198
176
|
<View
|
|
199
|
-
pointerEvents="box-none"
|
|
200
|
-
position="bottom"
|
|
201
177
|
style={
|
|
202
178
|
[
|
|
203
179
|
[
|
|
204
180
|
{
|
|
205
|
-
"
|
|
206
|
-
"flexDirection": "
|
|
207
|
-
"
|
|
208
|
-
"paddingVertical": 15.692307692307693,
|
|
181
|
+
"alignItems": "center",
|
|
182
|
+
"flexDirection": "row",
|
|
183
|
+
"justifyContent": "flex-end",
|
|
209
184
|
},
|
|
210
185
|
],
|
|
211
186
|
undefined,
|
|
212
187
|
]
|
|
213
188
|
}
|
|
214
189
|
/>
|
|
215
|
-
</
|
|
190
|
+
</Pressable>
|
|
216
191
|
`;
|
|
@@ -44,6 +44,12 @@ jest.mock('@hero-design/rn', () => ({
|
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
46
|
})),
|
|
47
|
+
// Mock styled function
|
|
48
|
+
styled: jest.fn(() => jest.fn(() => 'MockStyledComponent')),
|
|
49
|
+
// Mock Typography component
|
|
50
|
+
Typography: {
|
|
51
|
+
Caption: 'MockCaption',
|
|
52
|
+
},
|
|
47
53
|
// Re-export everything else as is
|
|
48
54
|
Button: 'MockButton',
|
|
49
55
|
TextInput: 'MockTextInput',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from '@hero-design/rn';
|
|
3
|
+
import {
|
|
4
|
+
StyledErrorRow,
|
|
5
|
+
StyledError,
|
|
6
|
+
StyledHelperText,
|
|
7
|
+
} from './StyledTextInput';
|
|
8
|
+
|
|
9
|
+
export interface ErrorOrHelpTextProps {
|
|
10
|
+
/** Error message to display (takes priority) */
|
|
11
|
+
error?: string;
|
|
12
|
+
/** Helper text to display when no error */
|
|
13
|
+
helpText?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ErrorOrHelpText Component
|
|
18
|
+
*
|
|
19
|
+
* Displays either error messages or help text below the TextInput.
|
|
20
|
+
* Shows error messages with a danger icon when present, otherwise displays help text.
|
|
21
|
+
*
|
|
22
|
+
* Key Features:
|
|
23
|
+
* - Conditional rendering: error takes priority over help text
|
|
24
|
+
* - Error messages include a danger icon for visual clarity
|
|
25
|
+
* - Uses styled components for consistent theming
|
|
26
|
+
* - Includes proper test IDs for testing
|
|
27
|
+
*
|
|
28
|
+
* Rendering Logic:
|
|
29
|
+
* 1. If error exists: Show error message with danger icon
|
|
30
|
+
* 2. If no error but helpText exists: Show help text
|
|
31
|
+
* 3. If neither exists: Render nothing
|
|
32
|
+
*
|
|
33
|
+
* @param props - The component props (see ErrorOrHelpTextProps interface for details)
|
|
34
|
+
*/
|
|
35
|
+
const ErrorOrHelpText: React.FC<ErrorOrHelpTextProps> = ({
|
|
36
|
+
error,
|
|
37
|
+
helpText,
|
|
38
|
+
}) => {
|
|
39
|
+
return error ? (
|
|
40
|
+
// Error state: Show error message with danger icon
|
|
41
|
+
<StyledErrorRow>
|
|
42
|
+
<Icon
|
|
43
|
+
testID="input-error-icon"
|
|
44
|
+
icon="circle-info"
|
|
45
|
+
size="xsmall"
|
|
46
|
+
intent="danger"
|
|
47
|
+
/>
|
|
48
|
+
<StyledError testID="input-error-message">{error}</StyledError>
|
|
49
|
+
</StyledErrorRow>
|
|
50
|
+
) : (
|
|
51
|
+
// Help text state: Show help text if provided
|
|
52
|
+
!!helpText && <StyledHelperText>{helpText}</StyledHelperText>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
ErrorOrHelpText.displayName = 'ErrorOrHelpText';
|
|
57
|
+
|
|
58
|
+
export default React.memo(ErrorOrHelpText);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
import { StyledFloatingLabelContainer, StyledLabel } from './StyledTextInput';
|
|
4
|
+
import type { State } from './StyledTextInput';
|
|
5
|
+
import type { TextInputVariant } from './types';
|
|
6
|
+
import { useTheme } from '../../theme';
|
|
7
|
+
import { LABEL_ANIMATION_DURATION } from './constants';
|
|
8
|
+
|
|
9
|
+
export interface FloatingLabelProps {
|
|
10
|
+
/** The text to display in the label */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Input variant that affects animation positioning */
|
|
13
|
+
variant: TextInputVariant;
|
|
14
|
+
/** Current input state for styling */
|
|
15
|
+
state: State;
|
|
16
|
+
/** Whether the input is focused */
|
|
17
|
+
isFocused: boolean;
|
|
18
|
+
/** Whether field is required (affects optional text) */
|
|
19
|
+
required?: boolean;
|
|
20
|
+
/** Accessibility ID for screen readers */
|
|
21
|
+
accessibilityLabelledBy?: string;
|
|
22
|
+
/** Whether the input value is empty */
|
|
23
|
+
isEmptyValue: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* FloatingLabel Component
|
|
28
|
+
*
|
|
29
|
+
* Handles the animated floating label behavior for TextInput.
|
|
30
|
+
* The label starts positioned over the input and animates to a smaller size above the input
|
|
31
|
+
* when the field is focused or has content.
|
|
32
|
+
*
|
|
33
|
+
* Key Features:
|
|
34
|
+
* - Smooth scale and translate animations using React Native Animated API
|
|
35
|
+
* - Responds to focus state and content presence
|
|
36
|
+
* - Automatically appends "(Optional)" text for non-required fields
|
|
37
|
+
* - Supports different variants (text/textarea) with appropriate positioning
|
|
38
|
+
* - Maintains accessibility features with proper IDs and test IDs
|
|
39
|
+
* - Self-contained animation management
|
|
40
|
+
*
|
|
41
|
+
* Animation Behavior:
|
|
42
|
+
* - Scale: 1.5 → 1 (label shrinks when floating)
|
|
43
|
+
* - TranslateY: Variable based on variant → 0 (moves up)
|
|
44
|
+
* - Uses bezier easing for smooth transitions
|
|
45
|
+
*
|
|
46
|
+
* @param props - The component props (see FloatingLabelProps for details)
|
|
47
|
+
*/
|
|
48
|
+
const FloatingLabel: React.FC<FloatingLabelProps> = ({
|
|
49
|
+
label,
|
|
50
|
+
variant,
|
|
51
|
+
state,
|
|
52
|
+
isFocused,
|
|
53
|
+
required,
|
|
54
|
+
accessibilityLabelledBy,
|
|
55
|
+
isEmptyValue,
|
|
56
|
+
}) => {
|
|
57
|
+
const theme = useTheme();
|
|
58
|
+
const shouldFloat = isFocused || !isEmptyValue;
|
|
59
|
+
const focusAnimation = useRef(
|
|
60
|
+
new Animated.Value(shouldFloat ? 1 : 0)
|
|
61
|
+
).current;
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
Animated.timing(focusAnimation, {
|
|
65
|
+
toValue: shouldFloat ? 1 : 0,
|
|
66
|
+
duration: LABEL_ANIMATION_DURATION,
|
|
67
|
+
easing: Easing.bezier(0.4, 0, 0.2, 1),
|
|
68
|
+
useNativeDriver: true,
|
|
69
|
+
}).start();
|
|
70
|
+
}, [shouldFloat, focusAnimation]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<StyledFloatingLabelContainer
|
|
74
|
+
themeVariant={variant}
|
|
75
|
+
style={[
|
|
76
|
+
{
|
|
77
|
+
transformOrigin: 'left center',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
transform: [
|
|
81
|
+
{
|
|
82
|
+
translateY: focusAnimation.interpolate({
|
|
83
|
+
inputRange: [0, 1],
|
|
84
|
+
outputRange: [
|
|
85
|
+
variant !== 'textarea' ? 12 : theme.space.small,
|
|
86
|
+
0,
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
scale: focusAnimation.interpolate({
|
|
92
|
+
inputRange: [0, 1],
|
|
93
|
+
outputRange: [1.333, 1],
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
]}
|
|
99
|
+
>
|
|
100
|
+
<Animated.View>
|
|
101
|
+
<StyledLabel
|
|
102
|
+
nativeID={accessibilityLabelledBy}
|
|
103
|
+
testID="input-label"
|
|
104
|
+
themeState={state}
|
|
105
|
+
numberOfLines={1}
|
|
106
|
+
style={{
|
|
107
|
+
backgroundColor: 'transparent',
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{label}
|
|
111
|
+
{!required && ' (Optional)'}
|
|
112
|
+
</StyledLabel>
|
|
113
|
+
</Animated.View>
|
|
114
|
+
</StyledFloatingLabelContainer>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
FloatingLabel.displayName = 'FloatingLabel';
|
|
119
|
+
|
|
120
|
+
export default React.memo(FloatingLabel);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import type { TextInputProps as NativeTextInputProps } from 'react-native';
|
|
4
|
+
import { StyledTextInput } from './StyledTextInput';
|
|
5
|
+
import { useTheme } from '../../theme';
|
|
6
|
+
import type { TextInputVariant } from './types';
|
|
7
|
+
|
|
8
|
+
export interface InputComponentProps {
|
|
9
|
+
/** Input type ('text' or 'textarea') */
|
|
10
|
+
variant: TextInputVariant;
|
|
11
|
+
/** All props passed to the underlying TextInput */
|
|
12
|
+
nativeInputProps: NativeTextInputProps;
|
|
13
|
+
/** Optional custom input renderer function */
|
|
14
|
+
renderInputValue?: (inputProps: NativeTextInputProps) => React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* InputComponent
|
|
19
|
+
*
|
|
20
|
+
* Renders the actual text input element. Supports custom input rendering or uses the default StyledTextInput.
|
|
21
|
+
*
|
|
22
|
+
* Key Features:
|
|
23
|
+
* - Flexible rendering: supports custom input renderers via renderInputValue prop
|
|
24
|
+
* - Variant support: handles both 'text' and 'textarea' input types
|
|
25
|
+
* - Multiline handling: automatically enables multiline for textarea variant
|
|
26
|
+
* - Theme integration: applies theme colors for placeholder text
|
|
27
|
+
* - Ref forwarding: properly forwards refs to the underlying TextInput
|
|
28
|
+
*
|
|
29
|
+
* Rendering Logic:
|
|
30
|
+
* 1. If renderInputValue provided: Use custom renderer
|
|
31
|
+
* 2. Otherwise: Use default StyledTextInput with theme and variant support
|
|
32
|
+
*
|
|
33
|
+
* Multiline Behavior:
|
|
34
|
+
* - 'textarea' variant: Always multiline
|
|
35
|
+
* - 'text' variant: Single line unless explicitly set via nativeInputProps
|
|
36
|
+
*
|
|
37
|
+
* @param props - The component props (see InputComponentProps interface for details)
|
|
38
|
+
*/
|
|
39
|
+
const InputComponent = React.forwardRef<RNTextInput, InputComponentProps>(
|
|
40
|
+
({ variant, nativeInputProps, renderInputValue }, ref) => {
|
|
41
|
+
const theme = useTheme();
|
|
42
|
+
|
|
43
|
+
return renderInputValue ? (
|
|
44
|
+
// Custom input renderer provided
|
|
45
|
+
<>{renderInputValue(nativeInputProps)}</>
|
|
46
|
+
) : (
|
|
47
|
+
// Default styled input with theme and variant support
|
|
48
|
+
<StyledTextInput
|
|
49
|
+
{...nativeInputProps}
|
|
50
|
+
themeVariant={variant}
|
|
51
|
+
multiline={variant === 'textarea' || nativeInputProps.multiline}
|
|
52
|
+
ref={ref}
|
|
53
|
+
placeholderTextColor={theme.__hd__.textInput.colors.placeholder}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
InputComponent.displayName = 'InputComponent';
|
|
60
|
+
|
|
61
|
+
export default React.memo(InputComponent);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import type { TextInputProps as NativeTextInputProps } from 'react-native';
|
|
4
|
+
import { IconName } from '@hero-design/rn';
|
|
5
|
+
import { StyledInputRow } from './StyledTextInput';
|
|
6
|
+
import PrefixComponent from './PrefixComponent';
|
|
7
|
+
import InputComponent from './InputComponent';
|
|
8
|
+
import type { State } from './StyledTextInput';
|
|
9
|
+
import type { TextInputVariant } from './types';
|
|
10
|
+
import { LABEL_ANIMATION_DURATION } from './constants';
|
|
11
|
+
|
|
12
|
+
interface InputRowProps {
|
|
13
|
+
/** Current state of the input (focused, error, disabled, etc.) */
|
|
14
|
+
state: State;
|
|
15
|
+
/** Whether the input is focused */
|
|
16
|
+
isFocused: boolean;
|
|
17
|
+
/** Optional prefix icon or component */
|
|
18
|
+
prefix?: IconName | React.ReactElement;
|
|
19
|
+
/** Input variant - 'text' or 'textarea' */
|
|
20
|
+
variant: TextInputVariant;
|
|
21
|
+
/** Native TextInput props passed to the input component */
|
|
22
|
+
nativeInputProps: NativeTextInputProps;
|
|
23
|
+
/** Optional custom render function for input value */
|
|
24
|
+
renderInputValue?: (inputProps: NativeTextInputProps) => React.ReactNode;
|
|
25
|
+
/** Whether the input value is empty */
|
|
26
|
+
isEmptyValue: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* InputRow Component
|
|
31
|
+
*
|
|
32
|
+
* Layout Structure:
|
|
33
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
34
|
+
* │ StyledInputRow │
|
|
35
|
+
* │ ┌──────────┐ ┌─────────────────────────────────────┐ │
|
|
36
|
+
* │ │ Prefix │ │ Input Field │ │
|
|
37
|
+
* │ │ (Icon or │ │ (TextInput or Custom) │ │
|
|
38
|
+
* │ │ Custom) │ │ flex: 1 │ │
|
|
39
|
+
* │ └──────────┘ └─────────────────────────────────────┘ │
|
|
40
|
+
* └─────────────────────────────────────────────────────────┘
|
|
41
|
+
*
|
|
42
|
+
* Rendering Behavior:
|
|
43
|
+
* - Components are always rendered but hidden with opacity in idle state
|
|
44
|
+
* - Uses accessibility props to hide components from screen readers when not visible
|
|
45
|
+
*
|
|
46
|
+
* Renders the main input row containing:
|
|
47
|
+
* 1. Conditionally visible prefix component (icon or custom element)
|
|
48
|
+
* 2. Conditionally visible input component (TextInput or custom rendered input)
|
|
49
|
+
*/
|
|
50
|
+
const InputRow = React.forwardRef<RNTextInput, InputRowProps>(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
state,
|
|
54
|
+
isFocused,
|
|
55
|
+
prefix,
|
|
56
|
+
variant,
|
|
57
|
+
nativeInputProps,
|
|
58
|
+
renderInputValue,
|
|
59
|
+
isEmptyValue,
|
|
60
|
+
},
|
|
61
|
+
ref
|
|
62
|
+
) => {
|
|
63
|
+
const shouldShow = isFocused || !isEmptyValue;
|
|
64
|
+
const opacity = useRef(new Animated.Value(shouldShow ? 1 : 0)).current;
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
Animated.timing(opacity, {
|
|
68
|
+
toValue: shouldShow ? 1 : 0,
|
|
69
|
+
duration: LABEL_ANIMATION_DURATION,
|
|
70
|
+
useNativeDriver: true,
|
|
71
|
+
}).start();
|
|
72
|
+
}, [shouldShow, opacity]);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<StyledInputRow>
|
|
76
|
+
{/* Prefix animation */}
|
|
77
|
+
<Animated.View style={{ opacity }}>
|
|
78
|
+
<PrefixComponent
|
|
79
|
+
state={state}
|
|
80
|
+
prefix={prefix}
|
|
81
|
+
accessibilityElementsHidden={!shouldShow}
|
|
82
|
+
/>
|
|
83
|
+
</Animated.View>
|
|
84
|
+
{/* Input animation */}
|
|
85
|
+
<Animated.View
|
|
86
|
+
style={{ flex: 1, opacity }}
|
|
87
|
+
testID="input-row-input-wrapper"
|
|
88
|
+
accessibilityLabel="Text input field"
|
|
89
|
+
accessibilityElementsHidden={!shouldShow}
|
|
90
|
+
>
|
|
91
|
+
<InputComponent
|
|
92
|
+
variant={variant}
|
|
93
|
+
nativeInputProps={nativeInputProps}
|
|
94
|
+
renderInputValue={renderInputValue}
|
|
95
|
+
ref={ref}
|
|
96
|
+
/>
|
|
97
|
+
</Animated.View>
|
|
98
|
+
</StyledInputRow>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
export default InputRow;
|