@hero-design/rn-work-uikit 1.2.0-alpha.1 → 1.2.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.
@@ -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, { useEffect, useRef } from 'react';
2
- import { Animated, TextInput as RNTextInput } from 'react-native';
3
- import type { TextInputProps as NativeTextInputProps } from 'react-native';
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
- const shouldShow = isFocused || !isEmptyValue;
64
- const opacity = useRef(new Animated.Value(shouldShow ? 1 : 0)).current;
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
- useEffect(() => {
67
- Animated.timing(opacity, {
68
- toValue: shouldShow ? 1 : 0,
69
- duration: LABEL_ANIMATION_DURATION,
70
- useNativeDriver: true,
71
- }).start();
72
- }, [shouldShow, opacity]);
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
- <Animated.View style={{ opacity }}>
78
- <PrefixComponent
79
- state={state}
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
- <Animated.View
86
- style={{ flex: 1, opacity }}
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={nativeInputProps}
107
+ nativeInputProps={{
108
+ ...nativeInputProps,
109
+ onFocus: handleFocus,
110
+ onBlur: handleBlur,
111
+ }}
94
112
  renderInputValue={renderInputValue}
95
113
  ref={ref}
96
114
  />
97
- </Animated.View>
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.