@hero-design/rn-work-uikit 1.2.0-alpha.0 → 1.2.0-alpha.2
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/CHANGELOG.md +31 -1
- package/assets/fonts/BeVietnamPro-Bold.ttf +0 -0
- package/assets/fonts/BeVietnamPro-Light.ttf +0 -0
- package/assets/fonts/BeVietnamPro-Regular.ttf +0 -0
- package/assets/fonts/BeVietnamPro-SemiBold.ttf +0 -0
- package/assets/fonts/Saiga-Light.otf +0 -0
- package/assets/fonts/Saiga-Medium.otf +0 -0
- package/assets/fonts/Saiga-Regular.otf +0 -0
- package/assets/fonts/hero-icons-mobile.ttf +0 -0
- package/lib/index.js +2103 -186
- package/package.json +2 -2
- package/rollup.config.mjs +2 -3
- package/src/__tests__/__snapshots__/index.spec.tsx.snap +10 -29
- package/src/components/TextInput/FloatingLabel.tsx +8 -2
- package/src/components/TextInput/Group/__tests__/__snapshots__/index.spec.tsx.snap +883 -0
- package/src/components/TextInput/Group/__tests__/index.spec.tsx +179 -0
- package/src/components/TextInput/Group/__tests__/utils.spec.ts +73 -0
- package/src/components/TextInput/Group/index.tsx +102 -0
- package/src/components/TextInput/Group/utils.ts +67 -0
- package/src/components/TextInput/InputRow.tsx +47 -29
- package/src/components/TextInput/MIGRATION.md +133 -0
- package/src/components/TextInput/StyledTextInput.tsx +36 -8
- package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +36 -46
- package/src/components/TextInput/__tests__/InputRow.spec.tsx +50 -95
- package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +559 -544
- package/src/components/TextInput/__tests__/index.spec.tsx +241 -262
- package/src/components/TextInput/index.tsx +39 -17
- package/src/components/TextInput/types.ts +7 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { within } from '@testing-library/react-native';
|
|
3
|
+
import TextInputGroup from '..';
|
|
4
|
+
import renderWithTheme from '../../../../../testUtils/renderWithTheme';
|
|
5
|
+
import TextInput from '../..';
|
|
6
|
+
import { theme } from '../../../..';
|
|
7
|
+
|
|
8
|
+
describe('TextInputGroup', () => {
|
|
9
|
+
it('should render', () => {
|
|
10
|
+
const { getByText, getByTestId, toJSON } = renderWithTheme(
|
|
11
|
+
<TextInputGroup>
|
|
12
|
+
<TextInput label="Text Input 1" value="Text Input 1" required />
|
|
13
|
+
<TextInput
|
|
14
|
+
label="Text Input 2"
|
|
15
|
+
value="Text Input 2"
|
|
16
|
+
error="This is an error"
|
|
17
|
+
testID="text-input-2"
|
|
18
|
+
/>
|
|
19
|
+
<TextInput label="Text Input 3" value="Text Input 3" required />
|
|
20
|
+
</TextInputGroup>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(toJSON()).toMatchSnapshot('xxx');
|
|
24
|
+
|
|
25
|
+
expect(getByText('Text Input 1')).toBeVisible();
|
|
26
|
+
expect(getByText('Text Input 2', { exact: false })).toBeVisible();
|
|
27
|
+
expect(
|
|
28
|
+
within(getByTestId('text-input-2')).getByText('(Optional)', {
|
|
29
|
+
exact: false,
|
|
30
|
+
})
|
|
31
|
+
).toBeVisible();
|
|
32
|
+
expect(getByText('This is an error')).toBeVisible();
|
|
33
|
+
expect(getByText('Text Input 3')).toBeVisible();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders with correct border styling', () => {
|
|
37
|
+
const { getByTestId } = renderWithTheme(
|
|
38
|
+
<TextInputGroup>
|
|
39
|
+
<TextInput value="Text Input 1" testID="text-input-1" />
|
|
40
|
+
<TextInput value="Text Input 2" testID="text-input-2" />
|
|
41
|
+
<TextInput value="Text Input 3" testID="text-input-3" />
|
|
42
|
+
</TextInputGroup>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
within(getByTestId('text-input-1'))
|
|
47
|
+
.getByTestId('text-input-border')
|
|
48
|
+
.props.style.flat()
|
|
49
|
+
).toEqual(
|
|
50
|
+
expect.arrayContaining([
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
borderBottomLeftRadius: 0,
|
|
53
|
+
borderBottomRightRadius: 0,
|
|
54
|
+
}),
|
|
55
|
+
])
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(
|
|
59
|
+
within(getByTestId('text-input-2'))
|
|
60
|
+
.getByTestId('text-input-border')
|
|
61
|
+
.props.style.flat()
|
|
62
|
+
).toEqual(
|
|
63
|
+
expect.arrayContaining([
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
borderRadius: 0,
|
|
66
|
+
}),
|
|
67
|
+
])
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
within(getByTestId('text-input-3'))
|
|
72
|
+
.getByTestId('text-input-border')
|
|
73
|
+
.props.style.flat()
|
|
74
|
+
).toEqual(
|
|
75
|
+
expect.arrayContaining([
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
borderTopLeftRadius: 0,
|
|
78
|
+
borderTopRightRadius: 0,
|
|
79
|
+
}),
|
|
80
|
+
])
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('merges with the children styles in correct order', () => {
|
|
85
|
+
const { getByTestId } = renderWithTheme(
|
|
86
|
+
<TextInputGroup>
|
|
87
|
+
<TextInput
|
|
88
|
+
value="Text Input 1"
|
|
89
|
+
testID="text-input-1"
|
|
90
|
+
textStyle={{
|
|
91
|
+
borderColor: '#ffffff',
|
|
92
|
+
borderWidth: 1,
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
<TextInput
|
|
96
|
+
value="Text Input 2"
|
|
97
|
+
testID="text-input-2"
|
|
98
|
+
style={{ width: 300 }}
|
|
99
|
+
/>
|
|
100
|
+
</TextInputGroup>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Merging text styles
|
|
104
|
+
expect(
|
|
105
|
+
within(getByTestId('text-input-1'))
|
|
106
|
+
.getByTestId('text-input-border')
|
|
107
|
+
.props.style.flat()
|
|
108
|
+
).toEqual(
|
|
109
|
+
expect.arrayContaining([
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
// Passed style
|
|
112
|
+
borderWidth: 1,
|
|
113
|
+
borderColor: '#ffffff',
|
|
114
|
+
// Injected style
|
|
115
|
+
borderBottomLeftRadius: 0,
|
|
116
|
+
borderBottomRightRadius: 0,
|
|
117
|
+
}),
|
|
118
|
+
])
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Merging container styles
|
|
122
|
+
expect(getByTestId('text-input-2').props.style.flat()).toEqual(
|
|
123
|
+
expect.arrayContaining([
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
// Passed style
|
|
126
|
+
width: 300,
|
|
127
|
+
// Injected style
|
|
128
|
+
marginTop: -theme.__hd__.textInput.borderWidths.container.normal,
|
|
129
|
+
}),
|
|
130
|
+
])
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
within(getByTestId('text-input-2'))
|
|
135
|
+
.getByTestId('text-input-border')
|
|
136
|
+
.props.style.flat()
|
|
137
|
+
).toEqual(
|
|
138
|
+
expect.arrayContaining([
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
// Injected style
|
|
141
|
+
borderTopLeftRadius: 0,
|
|
142
|
+
borderTopRightRadius: 0,
|
|
143
|
+
}),
|
|
144
|
+
])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('does not alter the children styles if there is only one child', () => {
|
|
149
|
+
const { getByTestId } = renderWithTheme(
|
|
150
|
+
<TextInputGroup>
|
|
151
|
+
<TextInput
|
|
152
|
+
value="Text Input 1"
|
|
153
|
+
testID="text-input-1"
|
|
154
|
+
style={{
|
|
155
|
+
marginTop: theme.space.medium,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
</TextInputGroup>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Container style is not injected
|
|
162
|
+
expect(getByTestId('text-input-1').props.style.flat()).toEqual(
|
|
163
|
+
expect.arrayContaining([
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
// Passed style instead of injected style
|
|
166
|
+
marginTop: theme.space.medium,
|
|
167
|
+
}),
|
|
168
|
+
])
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Border style is not injected
|
|
172
|
+
const internalBorderStyle =
|
|
173
|
+
getByTestId('text-input-border').props.style.flat();
|
|
174
|
+
const borderKeys = Object.keys(internalBorderStyle).filter((key) =>
|
|
175
|
+
key.startsWith('border')
|
|
176
|
+
);
|
|
177
|
+
expect(borderKeys).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { theme } from '../../../..';
|
|
2
|
+
import { generateBorderStyle, generateMarginStyle } from '../utils';
|
|
3
|
+
|
|
4
|
+
describe('utils', () => {
|
|
5
|
+
describe('generateBorderStyle', () => {
|
|
6
|
+
it('should generate the correct border style for the first child', () => {
|
|
7
|
+
const borderStyle = generateBorderStyle({ index: 0, length: 3 });
|
|
8
|
+
expect(borderStyle).toEqual({
|
|
9
|
+
borderBottomLeftRadius: 0,
|
|
10
|
+
borderBottomRightRadius: 0,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should generate the correct border style for the last child', () => {
|
|
15
|
+
const borderStyle = generateBorderStyle({ index: 2, length: 3 });
|
|
16
|
+
expect(borderStyle).toEqual({
|
|
17
|
+
borderTopLeftRadius: 0,
|
|
18
|
+
borderTopRightRadius: 0,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should generate the correct border style for the middle child', () => {
|
|
23
|
+
const borderStyle = generateBorderStyle({ index: 1, length: 3 });
|
|
24
|
+
expect(borderStyle).toEqual({
|
|
25
|
+
borderRadius: 0,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('generateMarginStyle', () => {
|
|
31
|
+
it('should generate the correct margin style for the first child', () => {
|
|
32
|
+
const marginStyle = generateMarginStyle({
|
|
33
|
+
index: 0,
|
|
34
|
+
length: 3,
|
|
35
|
+
theme,
|
|
36
|
+
});
|
|
37
|
+
expect(marginStyle).toEqual({
|
|
38
|
+
marginTop: 0,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should generate the correct margin style for the last child', () => {
|
|
43
|
+
const marginStyle = generateMarginStyle({
|
|
44
|
+
index: 2,
|
|
45
|
+
length: 3,
|
|
46
|
+
theme,
|
|
47
|
+
});
|
|
48
|
+
expect(marginStyle).toEqual({
|
|
49
|
+
marginTop: -theme.__hd__.textInput.borderWidths.container.normal,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should generate the correct margin style for the middle child', () => {
|
|
54
|
+
const marginStyle = generateMarginStyle({
|
|
55
|
+
index: 1,
|
|
56
|
+
length: 3,
|
|
57
|
+
theme,
|
|
58
|
+
});
|
|
59
|
+
expect(marginStyle).toEqual({
|
|
60
|
+
marginTop: -theme.__hd__.textInput.borderWidths.container.normal,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should generate the correct margin style for a single child', () => {
|
|
65
|
+
const marginStyle = generateMarginStyle({
|
|
66
|
+
index: 0,
|
|
67
|
+
length: 1,
|
|
68
|
+
theme,
|
|
69
|
+
});
|
|
70
|
+
expect(marginStyle).toEqual({});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { ReactNode, useMemo } from 'react';
|
|
2
|
+
import { StyleProp, StyleSheet, ViewProps, ViewStyle } from 'react-native';
|
|
3
|
+
import { Box, useTheme } from '@hero-design/rn';
|
|
4
|
+
import { generateBorderStyle, generateMarginStyle } from './utils';
|
|
5
|
+
|
|
6
|
+
export interface TextInputGroupProps extends ViewProps {
|
|
7
|
+
/**
|
|
8
|
+
* The children of the TextInputGroup. In order for the group styling to work,
|
|
9
|
+
* they must be either HD TextInput components or enhanced TextInput components
|
|
10
|
+
* that supports TextInput interface.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* const EnhancedTextInput = (props: TextInputProps) => {
|
|
14
|
+
* return <TextInput {...props} />;
|
|
15
|
+
* };
|
|
16
|
+
*
|
|
17
|
+
* <TextInput.Group>
|
|
18
|
+
* <TextInput ... />
|
|
19
|
+
* <EnhancedTextInput ... />
|
|
20
|
+
* </TextInput.Group>
|
|
21
|
+
*/
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* The style of the TextInputGroup.
|
|
25
|
+
*/
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
/**
|
|
28
|
+
* The testID of the TextInputGroup.
|
|
29
|
+
*/
|
|
30
|
+
testID?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TextInputGroup = ({
|
|
34
|
+
children,
|
|
35
|
+
style,
|
|
36
|
+
testID,
|
|
37
|
+
...props
|
|
38
|
+
}: TextInputGroupProps) => {
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
const childrenArray = React.Children.toArray(children).filter(
|
|
41
|
+
React.isValidElement
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// If there are multiple children, inject styles to group them together.
|
|
45
|
+
const groupedChildren = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
childrenArray.map((child, index) => {
|
|
48
|
+
const rawChildStyle = (child as React.ReactElement).props.style;
|
|
49
|
+
const rawChildTextStyle = (child as React.ReactElement).props.textStyle;
|
|
50
|
+
|
|
51
|
+
// Handle array styles by flattening them first
|
|
52
|
+
const childStyle = StyleSheet.flatten(rawChildStyle);
|
|
53
|
+
const childTextStyle = StyleSheet.flatten(rawChildTextStyle);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Merge the child style with the group injected style.
|
|
57
|
+
* Order of precedence:
|
|
58
|
+
* 1. Child style.
|
|
59
|
+
* 2. Group injected style.
|
|
60
|
+
*/
|
|
61
|
+
const mergedStyle = {
|
|
62
|
+
...childStyle,
|
|
63
|
+
...generateMarginStyle({
|
|
64
|
+
index,
|
|
65
|
+
length: childrenArray.length,
|
|
66
|
+
theme,
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Merge the child text style with the group text style.
|
|
72
|
+
* Order of precedence:
|
|
73
|
+
* 1. Group text style through textStyle prop.
|
|
74
|
+
* 2. Child text style.
|
|
75
|
+
* 3. Group injected border style.
|
|
76
|
+
*/
|
|
77
|
+
const mergedTextStyle = {
|
|
78
|
+
...childTextStyle,
|
|
79
|
+
...generateBorderStyle({
|
|
80
|
+
index,
|
|
81
|
+
length: childrenArray.length,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return React.cloneElement(child as React.ReactElement, {
|
|
86
|
+
style: mergedStyle,
|
|
87
|
+
textStyle: mergedTextStyle,
|
|
88
|
+
// Internal text input prop to allow for different styling
|
|
89
|
+
enableGroupStyle: true,
|
|
90
|
+
});
|
|
91
|
+
}),
|
|
92
|
+
[childrenArray, theme]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Box style={style} testID={testID} {...props}>
|
|
97
|
+
{groupedChildren}
|
|
98
|
+
</Box>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default TextInputGroup;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
import { Theme } from '@hero-design/rn';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates the border style for the TextInputGroup.
|
|
6
|
+
* @param index - The index of the TextInput.
|
|
7
|
+
* @param length - The length of the TextInputGroup.
|
|
8
|
+
* @returns The border style for the TextInputGroup.
|
|
9
|
+
*/
|
|
10
|
+
const generateBorderStyle = ({
|
|
11
|
+
index,
|
|
12
|
+
length,
|
|
13
|
+
}: {
|
|
14
|
+
index: number;
|
|
15
|
+
length: number;
|
|
16
|
+
}): CSSProperties => {
|
|
17
|
+
const isFirst = index === 0;
|
|
18
|
+
const isLast = index === length - 1;
|
|
19
|
+
|
|
20
|
+
if (length === 1) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isFirst) {
|
|
25
|
+
return {
|
|
26
|
+
borderBottomLeftRadius: 0,
|
|
27
|
+
borderBottomRightRadius: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isLast) {
|
|
32
|
+
return {
|
|
33
|
+
borderTopLeftRadius: 0,
|
|
34
|
+
borderTopRightRadius: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
borderRadius: 0,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const generateMarginStyle = ({
|
|
44
|
+
index,
|
|
45
|
+
length,
|
|
46
|
+
theme,
|
|
47
|
+
}: {
|
|
48
|
+
index: number;
|
|
49
|
+
length: number;
|
|
50
|
+
theme: Theme;
|
|
51
|
+
}) => {
|
|
52
|
+
if (length === 1) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (index === 0) {
|
|
57
|
+
return {
|
|
58
|
+
marginTop: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
marginTop: -theme.__hd__.textInput.borderWidths.container.normal,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export { generateBorderStyle, generateMarginStyle };
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { TextInput as RNTextInput, View } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
NativeSyntheticEvent,
|
|
5
|
+
TextInputProps as NativeTextInputProps,
|
|
6
|
+
TextInputFocusEventData,
|
|
7
|
+
} from 'react-native';
|
|
4
8
|
import { IconName } from '@hero-design/rn';
|
|
5
9
|
import { StyledInputRow } from './StyledTextInput';
|
|
6
10
|
import PrefixComponent from './PrefixComponent';
|
|
7
11
|
import InputComponent from './InputComponent';
|
|
8
12
|
import type { State } from './StyledTextInput';
|
|
9
13
|
import type { TextInputVariant } from './types';
|
|
10
|
-
import { LABEL_ANIMATION_DURATION } from './constants';
|
|
11
14
|
|
|
12
15
|
interface InputRowProps {
|
|
13
16
|
/** Current state of the input (focused, error, disabled, etc.) */
|
|
14
17
|
state: State;
|
|
15
|
-
/** Whether the input is focused */
|
|
16
|
-
isFocused: boolean;
|
|
17
18
|
/** Optional prefix icon or component */
|
|
18
19
|
prefix?: IconName | React.ReactElement;
|
|
19
20
|
/** Input variant - 'text' or 'textarea' */
|
|
@@ -24,6 +25,10 @@ interface InputRowProps {
|
|
|
24
25
|
renderInputValue?: (inputProps: NativeTextInputProps) => React.ReactNode;
|
|
25
26
|
/** Whether the input value is empty */
|
|
26
27
|
isEmptyValue: boolean;
|
|
28
|
+
/** Whether the input should be visible when not focused and empty. Defaults to true. */
|
|
29
|
+
shouldShowWhenUnfocused?: boolean;
|
|
30
|
+
/** Test ID for testing purposes */
|
|
31
|
+
testID?: string;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/**
|
|
@@ -51,50 +56,63 @@ const InputRow = React.forwardRef<RNTextInput, InputRowProps>(
|
|
|
51
56
|
(
|
|
52
57
|
{
|
|
53
58
|
state,
|
|
54
|
-
isFocused,
|
|
55
59
|
prefix,
|
|
56
60
|
variant,
|
|
57
61
|
nativeInputProps,
|
|
58
62
|
renderInputValue,
|
|
59
63
|
isEmptyValue,
|
|
64
|
+
testID,
|
|
65
|
+
shouldShowWhenUnfocused = false,
|
|
60
66
|
},
|
|
61
67
|
ref
|
|
62
68
|
) => {
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
// We need to track focus state locally because the native TextInput's onFocus/onBlur
|
|
70
|
+
// events are the source of truth for when the input is actually focused, and we need
|
|
71
|
+
// this state to control visibility aand accessibility behavior
|
|
72
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const shouldShow = shouldShowWhenUnfocused || isFocused || !isEmptyValue;
|
|
75
|
+
|
|
76
|
+
// Simplified callback functions (removed unnecessary memoization for simple cases)
|
|
77
|
+
const handleFocus = useCallback(
|
|
78
|
+
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
79
|
+
setIsFocused(true);
|
|
80
|
+
nativeInputProps.onFocus?.(event);
|
|
81
|
+
},
|
|
82
|
+
[nativeInputProps]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleBlur = useCallback(
|
|
86
|
+
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
87
|
+
setIsFocused(false);
|
|
88
|
+
nativeInputProps.onBlur?.(event);
|
|
89
|
+
},
|
|
90
|
+
[nativeInputProps]
|
|
91
|
+
);
|
|
73
92
|
|
|
74
93
|
return (
|
|
75
|
-
<StyledInputRow>
|
|
94
|
+
<StyledInputRow testID={testID} themeOpacity={shouldShow ? 1 : 0}>
|
|
76
95
|
{/* Prefix animation */}
|
|
77
|
-
<
|
|
78
|
-
<PrefixComponent
|
|
79
|
-
|
|
80
|
-
prefix={prefix}
|
|
81
|
-
accessibilityElementsHidden={!shouldShow}
|
|
82
|
-
/>
|
|
83
|
-
</Animated.View>
|
|
96
|
+
<View>
|
|
97
|
+
<PrefixComponent state={state} prefix={prefix} />
|
|
98
|
+
</View>
|
|
84
99
|
{/* Input animation */}
|
|
85
|
-
<
|
|
86
|
-
style={{ flex: 1
|
|
100
|
+
<View
|
|
101
|
+
style={{ flex: 1 }}
|
|
87
102
|
testID="input-row-input-wrapper"
|
|
88
103
|
accessibilityLabel="Text input field"
|
|
89
|
-
accessibilityElementsHidden={!shouldShow}
|
|
90
104
|
>
|
|
91
105
|
<InputComponent
|
|
92
106
|
variant={variant}
|
|
93
|
-
nativeInputProps={
|
|
107
|
+
nativeInputProps={{
|
|
108
|
+
...nativeInputProps,
|
|
109
|
+
onFocus: handleFocus,
|
|
110
|
+
onBlur: handleBlur,
|
|
111
|
+
}}
|
|
94
112
|
renderInputValue={renderInputValue}
|
|
95
113
|
ref={ref}
|
|
96
114
|
/>
|
|
97
|
-
</
|
|
115
|
+
</View>
|
|
98
116
|
</StyledInputRow>
|
|
99
117
|
);
|
|
100
118
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Migration Plan: `TextInput` from `@hero-design/rn` to `@hero-design/rn-work-uikit`
|
|
2
|
+
|
|
3
|
+
This document outlines the steps and considerations for migrating the `TextInput` component from the `@hero-design/rn` package to the `@hero-design/rn-work-uikit` package.
|
|
4
|
+
|
|
5
|
+
## 1. Overview of Changes
|
|
6
|
+
|
|
7
|
+
While the component's props interface remains largely the same, the `@hero-design/rn-work-uikit` version introduces significant UI and behavioral changes. The new implementation is more modular and introduces a floating label for a more modern user experience.
|
|
8
|
+
|
|
9
|
+
## 2. Key Changes to Be Aware Of
|
|
10
|
+
|
|
11
|
+
### 2.1. Prefix Icon Visibility
|
|
12
|
+
|
|
13
|
+
In the `@hero-design/rn-work-uikit` version, the prefix icon is not visible when the `TextInput` is in its default, idle state (i.e., not focused and with no value). The prefix only appears once the `TextInput` is focused or contains a value. This is different from the `@hero-design/rn` version, where the prefix was always visible.
|
|
14
|
+
|
|
15
|
+
### 2.2. "Optional" Label Text
|
|
16
|
+
|
|
17
|
+
The behavior of the `required` prop has changed:
|
|
18
|
+
|
|
19
|
+
* **Old Behavior (`@hero-design/rn`):** When `required={true}`, an asterisk (`*`) was appended to the label.
|
|
20
|
+
* **New Behavior (`@hero-design/rn-work-uikit`):** When `required={false}` (or it is not provided), the text `(Optional)` is appended to the label. When `required={true}`, nothing is appended.
|
|
21
|
+
|
|
22
|
+
This is an inversion of how optionality is communicated to the user.
|
|
23
|
+
|
|
24
|
+
### 2.3. Layout of Helper and Error Text
|
|
25
|
+
|
|
26
|
+
The layout for displaying `helpText`, `error` messages, and the `maxLength` character count has been updated.
|
|
27
|
+
|
|
28
|
+
* **Old Behavior (`@hero-design/rn`):** These elements were typically displayed below the input.
|
|
29
|
+
* **New Behavior (`@hero-design/rn-work-uikit`):** The `error` or `helpText` is displayed on the left, and the `maxLength` count is on the right, within the same line under the input field. This makes for a more compact component.
|
|
30
|
+
|
|
31
|
+
## 3. Migration Steps
|
|
32
|
+
|
|
33
|
+
1. **Update Imports:**
|
|
34
|
+
Change the import statement from `@hero-design/rn` to `@hero-design/rn-work-uikit`.
|
|
35
|
+
|
|
36
|
+
```diff
|
|
37
|
+
- import { TextInput } from '@hero-design/rn';
|
|
38
|
+
+ import { TextInput } from '@hero-design/rn-work-uikit';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
2. **Review existing `TextInput` implementations:**
|
|
42
|
+
Audit your app for all uses of the `TextInput` component. Pay close attention to forms and screens where the visual layout is critical.
|
|
43
|
+
|
|
44
|
+
3. **Perform Visual Regression Testing:**
|
|
45
|
+
Due to the floating label and other layout changes, existing screens may look different. It is crucial to perform visual regression testing on all screens that use `TextInput`.
|
|
46
|
+
|
|
47
|
+
## 4. Updating Unit and Snapshot Tests
|
|
48
|
+
|
|
49
|
+
The UI changes will likely break your existing snapshot tests and may affect other unit tests.
|
|
50
|
+
|
|
51
|
+
### 4.1. Snapshot Tests
|
|
52
|
+
|
|
53
|
+
Snapshot tests for components using `TextInput` will need to be updated. After you've confirmed the new look is correct, you can update the snapshots.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# For Jest users
|
|
57
|
+
npm test -- -u
|
|
58
|
+
# or
|
|
59
|
+
yarn test -u
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 4.2. Unit Test Adjustments
|
|
63
|
+
|
|
64
|
+
Here's what to look for in your unit tests:
|
|
65
|
+
|
|
66
|
+
* **Label Testing:**
|
|
67
|
+
* Tests that checked for an asterisk (`*`) for required fields will now fail.
|
|
68
|
+
* You should now check for the `(Optional)` text when the field is *not* required.
|
|
69
|
+
|
|
70
|
+
*Example Test Case (using React Native Testing Library):*
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// Old test for required field
|
|
74
|
+
it('should display an asterisk for required fields', () => {
|
|
75
|
+
const { getByText } = render(<TextInput label="My Field" required />);
|
|
76
|
+
// This will now fail
|
|
77
|
+
expect(getByText('*')).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// New test for optional field
|
|
81
|
+
it('should display (Optional) for non-required fields', () => {
|
|
82
|
+
const { queryByText } = render(<TextInput label="My Field" required={false} />);
|
|
83
|
+
expect(queryByText(/\(Optional\)/)).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// New test for required field
|
|
87
|
+
it('should not display (Optional) for required fields', () => {
|
|
88
|
+
const { queryByText } = render(<TextInput label="My Field" required={true} />);
|
|
89
|
+
expect(queryByText(/\(Optional\)/)).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
* **Finding Elements:**
|
|
94
|
+
* Because of the new floating label animation, the way you query for elements might need to change, especially if you were targeting specific wrapper views for styling.
|
|
95
|
+
* The internal structure of the component has changed significantly. Tests that rely on `Dive` in Enzyme or deep selectors will be brittle and will likely need to be rewritten. It's better to test based on what the user sees, e.g., using `getByTestId`, `getByText`, etc.
|
|
96
|
+
|
|
97
|
+
* **Interaction Tests:**
|
|
98
|
+
* If you have tests that simulate user input, the floating label will animate. This shouldn't affect most tests, but if you have complex tests that check for element positions, they might need adjustment.
|
|
99
|
+
|
|
100
|
+
### 4.3. Breaking Change: `testID` Handling
|
|
101
|
+
|
|
102
|
+
The way `testID` is applied to the underlying native `TextInput` component has changed, which will break existing tests that query for the input field.
|
|
103
|
+
|
|
104
|
+
* **Old Behavior (`@hero-design/rn`):** The `testID` for the native input was dynamically created by appending `-text-input` to the `testID` prop (e.g., `my-test-id-text-input`).
|
|
105
|
+
* **New Behavior (`@hero-design/rn-work-uikit`):** The `testID` for the native input is now **hardcoded to `'text-input'`.**
|
|
106
|
+
|
|
107
|
+
Your tests must be updated to reflect this change.
|
|
108
|
+
|
|
109
|
+
**Example Test Update:**
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
// Old test query
|
|
113
|
+
const input = getByTestId('my-form-field-text-input');
|
|
114
|
+
|
|
115
|
+
// New test query
|
|
116
|
+
const input = getByTestId('text-input');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Because the native input `testID` is now static, if you have multiple `TextInput` components on one screen, you must first scope your query to the correct component container.
|
|
120
|
+
|
|
121
|
+
**Example with multiple `TextInput`s:**
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
import { render, within } from '@testing-library/react-native';
|
|
125
|
+
|
|
126
|
+
// Get the container for a specific TextInput using its unique testID
|
|
127
|
+
const textInputContainer = getByTestId('my-unique-test-id');
|
|
128
|
+
|
|
129
|
+
// Find the native input within that container
|
|
130
|
+
const nativeInput = within(textInputContainer).getByTestId('text-input');
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This plan should provide a good starting point for the migration. The key is to be aware of the visual changes and to thoroughly test all instances of the `TextInput` component.
|