@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.
@@ -5,12 +5,37 @@ export type State = 'default' | 'filled' | 'disabled' | 'readonly' | 'error';
5
5
 
6
6
  type Variant = 'text' | 'textarea';
7
7
 
8
- const StyledContainer = styled(Pressable)(({ theme }) => ({
8
+ const getZIndexByState = ({
9
+ themeFocused,
10
+ themeHasError,
11
+ }: {
12
+ themeFocused: boolean;
13
+ themeHasError: boolean;
14
+ }) => {
15
+ if (themeFocused) {
16
+ return 2;
17
+ }
18
+
19
+ if (themeHasError) {
20
+ return 1;
21
+ }
22
+
23
+ return 0;
24
+ };
25
+
26
+ const StyledContainer = styled(Pressable)<{
27
+ themeFocused: boolean;
28
+ themeHasError: boolean;
29
+ themeUseGroupStyleEnabled: boolean;
30
+ }>(({ theme, themeFocused, themeHasError, themeUseGroupStyleEnabled }) => ({
9
31
  width: '100%',
10
32
  flexDirection: 'row',
11
33
  paddingHorizontal: theme.__hd__.textInput.space.containerPadding,
12
34
  minHeight: theme.__hd__.textInput.sizes.containerMinHeight,
13
35
  marginTop: theme.__hd__.textInput.space.containerMarginTop,
36
+ ...(themeUseGroupStyleEnabled && {
37
+ zIndex: getZIndexByState({ themeFocused, themeHasError }),
38
+ }),
14
39
  }));
15
40
 
16
41
  const StyledFloatingLabelContainer = styled(Animated.View)<{
@@ -89,13 +114,16 @@ const StyledInputWrapper = styled(View)(({ theme }) => ({
89
114
  overflow: 'hidden',
90
115
  }));
91
116
 
92
- const StyledInputRow = styled(View)(({ theme }) => ({
93
- flexDirection: 'row',
94
- alignItems: 'center',
95
- flexGrow: 2,
96
- flexShrink: 1,
97
- gap: theme.__hd__.textInput.space.prefixAndInputContainerGap,
98
- }));
117
+ const StyledInputRow = styled(Pressable)<{ themeOpacity: number }>(
118
+ ({ theme, themeOpacity }) => ({
119
+ flexDirection: 'row',
120
+ alignItems: 'center',
121
+ flexGrow: 2,
122
+ flexShrink: 1,
123
+ gap: theme.__hd__.textInput.space.prefixAndInputContainerGap,
124
+ opacity: themeOpacity,
125
+ })
126
+ );
99
127
 
100
128
  const StyledErrorAndHelpTextContainer = styled(View)(({ theme }) => ({
101
129
  paddingHorizontal:
@@ -5,16 +5,15 @@ import FloatingLabel from '../FloatingLabel';
5
5
  describe('FloatingLabel', () => {
6
6
  describe('label text display based on required prop', () => {
7
7
  it.each`
8
- required | expectedText | shouldShowOptional | description
9
- ${true} | ${'Email'} | ${false} | ${'required field without optional indicator'}
10
- ${false} | ${'Phone (Optional)'} | ${true} | ${'optional field with optional indicator'}
8
+ required | expectedText | optionalText
9
+ ${true} | ${'Email'} | ${null}
10
+ ${false} | ${'Phone'} | ${'(Optional)'}
11
11
  `(
12
- 'should display $expectedText for $description',
13
- ({ required, expectedText, shouldShowOptional }) => {
14
- const label = required ? 'Email' : 'Phone';
15
- const { getByText, queryByText } = renderWithTheme(
12
+ 'should display "$expectedText" with "$optionalText" when required is $required',
13
+ ({ required, expectedText, optionalText }) => {
14
+ const { getByTestId, queryByText } = renderWithTheme(
16
15
  <FloatingLabel
17
- label={label}
16
+ label={expectedText}
18
17
  variant="text"
19
18
  state="default"
20
19
  isFocused={false}
@@ -23,12 +22,11 @@ describe('FloatingLabel', () => {
23
22
  />
24
23
  );
25
24
 
26
- // User should see the appropriate label text
27
- expect(getByText(expectedText)).toBeTruthy();
25
+ const labelComponent = getByTestId('input-label');
26
+ expect(labelComponent).toHaveTextContent(expectedText);
28
27
 
29
- if (!shouldShowOptional) {
30
- expect(queryByText(`${label} (Optional)`)).toBeFalsy();
31
- expect(queryByText('(Optional)')).toBeFalsy();
28
+ if (optionalText) {
29
+ expect(queryByText(optionalText)).toBeVisible();
32
30
  }
33
31
  }
34
32
  );
@@ -36,39 +34,42 @@ describe('FloatingLabel', () => {
36
34
 
37
35
  describe('label behavior based on focus and content state', () => {
38
36
  it.each`
39
- state | isEmptyValue | expectedBehavior | description
40
- ${'focused'} | ${true} | ${'label in focused state'} | ${'user focuses empty input'}
41
- ${'filled'} | ${false} | ${'label remains in floating position'} | ${'user enters text in input'}
37
+ state | isEmptyValue | required | expectedText | description
38
+ ${'focused'} | ${true} | ${true} | ${'Password'} | ${'user focuses empty input'}
39
+ ${'filled'} | ${false} | ${true} | ${'Message'} | ${'user enters text in input'}
42
40
  `(
43
- 'should show $expectedBehavior when $description',
44
- ({ state, isEmptyValue }) => {
45
- const { getByTestId } = renderWithTheme(
41
+ 'should show correct label when $description',
42
+ ({ state, isEmptyValue, required, expectedText }) => {
43
+ const { getByTestId, queryByText } = renderWithTheme(
46
44
  <FloatingLabel
47
- label={state === 'focused' ? 'Password' : 'Message'}
45
+ label={expectedText}
48
46
  variant="text"
49
47
  state={state}
50
48
  isFocused={state === 'focused'}
51
- required
49
+ required={required}
52
50
  isEmptyValue={isEmptyValue}
53
51
  />
54
52
  );
55
53
 
56
54
  // User should see the label behaving appropriately
57
55
  const label = getByTestId('input-label');
58
- expect(label).toBeTruthy();
59
- const { children } = label.props;
60
- const expectedText = state === 'focused' ? 'Password' : 'Message';
61
- expect(
62
- Array.isArray(children) ? children.filter(Boolean).join('') : children
63
- ).toBe(expectedText);
56
+ expect(label).toHaveTextContent(expectedText);
57
+
58
+ const optionalLabel = queryByText(' (Optional)');
59
+ if (!required) {
60
+ expect(optionalLabel).not.toBeNull();
61
+ } else {
62
+ expect(optionalLabel).toBeNull();
63
+ }
64
64
  }
65
65
  );
66
66
  });
67
67
 
68
68
  describe('variant-specific positioning', () => {
69
69
  it.each`
70
- variant | required | expectedText | description
71
- ${'textarea'} | ${false} | ${'Description (Optional)'} | ${'multiline input with optional label'}
70
+ variant | required | expectedText | description
71
+ ${'textarea'} | ${false} | ${'Description'} | ${'multiline input with optional label'}
72
+ ${'textarea'} | ${true} | ${'Description'} | ${'multiline input with required label'}
72
73
  `(
73
74
  'should position label appropriately for $variant ($description)',
74
75
  ({ variant, required, expectedText }) => {
@@ -85,20 +86,16 @@ describe('FloatingLabel', () => {
85
86
 
86
87
  // User should see the label with proper variant positioning
87
88
  const label = getByTestId('input-label');
88
- expect(label).toBeTruthy();
89
- const { children } = label.props;
90
- expect(Array.isArray(children) ? children.join('') : children).toBe(
91
- expectedText
92
- );
89
+ expect(label).toHaveTextContent(expectedText);
93
90
  }
94
91
  );
95
92
  });
96
93
 
97
94
  describe('when user encounters different input states', () => {
98
95
  it.each`
99
- state | required | expectedText | description
100
- ${'error'} | ${true} | ${'Email'} | ${'validation error with required field'}
101
- ${'disabled'} | ${false} | ${'Disabled Field (Optional)'} | ${'non-interactive optional field'}
96
+ state | required | expectedText | description
97
+ ${'error'} | ${true} | ${'Email'} | ${'validation error with required field'}
98
+ ${'disabled'} | ${false} | ${'Disabled Field'} | ${'non-interactive optional field'}
102
99
  `(
103
100
  'should display appropriate styling for $state state ($description)',
104
101
  ({ state, required, expectedText }) => {
@@ -115,11 +112,7 @@ describe('FloatingLabel', () => {
115
112
 
116
113
  // User should see the label with appropriate state styling
117
114
  const label = getByTestId('input-label');
118
- expect(label).toBeTruthy();
119
- const { children } = label.props;
120
- expect(
121
- Array.isArray(children) ? children.filter(Boolean).join('') : children
122
- ).toBe(expectedText);
115
+ expect(label).toHaveTextContent(expectedText);
123
116
  }
124
117
  );
125
118
  });
@@ -193,10 +186,7 @@ describe('FloatingLabel', () => {
193
186
  const label = getByTestId('input-label');
194
187
  expect(label).toBeTruthy();
195
188
  expect(label).toHaveProp('nativeID', 'fullname-label');
196
- const { children } = label.props;
197
- expect(Array.isArray(children) ? children.join('') : children).toBe(
198
- 'Full Name (Optional)'
199
- );
189
+ expect(label).toHaveTextContent('Full Name');
200
190
  });
201
191
  });
202
192
  });
@@ -1,23 +1,20 @@
1
1
  import React from 'react';
2
2
  import { Text, TextInput as RNTextInput } from 'react-native';
3
3
  import type { TextInputProps as NativeTextInputProps } from 'react-native';
4
+ import { fireEvent, waitFor } from '@testing-library/react-native';
4
5
  // import { TextInput as RNTextInput } from 'react-native';
5
6
  import renderWithTheme from '../../../../testUtils/renderWithTheme';
6
7
  import InputRow from '../InputRow';
7
8
 
8
- // Mock the LABEL_ANIMATION_DURATION constant
9
- jest.mock('../constants', () => ({
10
- LABEL_ANIMATION_DURATION: 0, // Instant animation for testing
11
- }));
12
-
13
9
  describe('InputRow', () => {
14
10
  const defaultProps = {
15
11
  state: 'default' as const,
16
- isFocused: false,
17
12
  variant: 'text' as const,
13
+ testID: 'input-row',
18
14
  nativeInputProps: {
19
15
  value: '',
20
16
  placeholder: 'Enter text',
17
+ testID: 'input-row-text-input',
21
18
  },
22
19
  isEmptyValue: true,
23
20
  };
@@ -26,73 +23,42 @@ describe('InputRow', () => {
26
23
  jest.clearAllMocks();
27
24
  });
28
25
 
29
- describe('when user sees an empty input field', () => {
30
- it('should hide both prefix and input components with opacity', () => {
31
- const ref = React.createRef<RNTextInput>();
32
- const { getByTestId } = renderWithTheme(
33
- <InputRow {...defaultProps} prefix="search" ref={ref} />
34
- );
35
-
36
- // Components should be present but hidden
37
- const inputWrapper = getByTestId('input-row-input-wrapper');
38
- expect(inputWrapper).toBeTruthy();
39
- expect(inputWrapper).toHaveProp('accessibilityElementsHidden', true);
26
+ describe('visibility behavior', () => {
27
+ it('should be hidden when unfocused and empty by default', () => {
28
+ const { getByTestId } = renderWithTheme(<InputRow {...defaultProps} />);
29
+ expect(getByTestId('input-row')).toHaveProp('themeOpacity', 0);
40
30
  });
41
- });
42
31
 
43
- describe('when user focuses the input', () => {
44
- it('should show both prefix and input components with animation', () => {
45
- const ref = React.createRef<RNTextInput>();
32
+ it('should be visible when unfocused and empty if shouldShowWhenUnfocused is true', () => {
46
33
  const { getByTestId } = renderWithTheme(
47
- <InputRow {...defaultProps} isFocused isEmptyValue ref={ref} />
34
+ <InputRow {...defaultProps} shouldShowWhenUnfocused />
48
35
  );
36
+ expect(getByTestId('input-row')).toHaveProp('themeOpacity', 1);
37
+ });
49
38
 
50
- // Should be visible when focused
51
- const inputWrapper = getByTestId('input-row-input-wrapper');
52
- expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
39
+ it('should become visible on focus', async () => {
40
+ const { getByTestId } = renderWithTheme(<InputRow {...defaultProps} />);
41
+
42
+ expect(getByTestId('input-row')).toHaveProp('themeOpacity', 0);
43
+ fireEvent(getByTestId('input-row-text-input'), 'focus');
44
+
45
+ await waitFor(() => {
46
+ expect(getByTestId('input-row')).toHaveProp('themeOpacity', 1);
47
+ });
53
48
  });
54
- });
55
49
 
56
- describe('when user enters text in the input', () => {
57
- it('should keep components visible even when not focused', () => {
58
- const ref = React.createRef<RNTextInput>();
50
+ it('should be visible when it has a value, even if unfocused', () => {
59
51
  const { getByTestId } = renderWithTheme(
60
52
  <InputRow
61
53
  {...defaultProps}
62
- prefix="search"
63
- state="filled"
64
54
  isEmptyValue={false}
65
55
  nativeInputProps={{
66
56
  ...defaultProps.nativeInputProps,
67
57
  value: 'user@example.com',
68
58
  }}
69
- ref={ref}
70
59
  />
71
60
  );
72
-
73
- // User has entered text - components should be visible
74
- const inputWrapper = getByTestId('input-row-input-wrapper');
75
- expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
76
- });
77
- });
78
-
79
- describe('when user uses input with prefix icon', () => {
80
- it('should render input wrapper when prefix is provided', () => {
81
- const ref = React.createRef<RNTextInput>();
82
- const { getByTestId } = renderWithTheme(
83
- <InputRow
84
- {...defaultProps}
85
- prefix="search"
86
- isFocused
87
- isEmptyValue={false}
88
- ref={ref}
89
- />
90
- );
91
-
92
- // User should see the input wrapper with prefix
93
- const inputWrapper = getByTestId('input-row-input-wrapper');
94
- expect(inputWrapper).toBeTruthy();
95
- expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
61
+ expect(getByTestId('input-row')).toHaveProp('themeOpacity', 1);
96
62
  });
97
63
  });
98
64
 
@@ -105,12 +71,11 @@ describe('InputRow', () => {
105
71
  <InputRow
106
72
  {...defaultProps}
107
73
  prefix={<CustomPrefix />}
108
- isFocused
109
74
  isEmptyValue={false}
110
75
  ref={ref}
76
+ shouldShowWhenUnfocused
111
77
  />
112
78
  );
113
-
114
79
  // User should see the custom prefix
115
80
  expect(getByTestId('custom-prefix')).toBeTruthy();
116
81
  });
@@ -123,7 +88,6 @@ describe('InputRow', () => {
123
88
  <InputRow
124
89
  {...defaultProps}
125
90
  variant="textarea"
126
- isFocused
127
91
  isEmptyValue={false}
128
92
  nativeInputProps={{
129
93
  ...defaultProps.nativeInputProps,
@@ -131,9 +95,9 @@ describe('InputRow', () => {
131
95
  numberOfLines: 4,
132
96
  }}
133
97
  ref={ref}
98
+ shouldShowWhenUnfocused
134
99
  />
135
100
  );
136
-
137
101
  // User should see the textarea input
138
102
  const inputWrapper = getByTestId('input-row-input-wrapper');
139
103
  expect(inputWrapper).toBeTruthy();
@@ -143,27 +107,28 @@ describe('InputRow', () => {
143
107
 
144
108
  describe('when user encounters different input states', () => {
145
109
  it('should render input wrapper in error state', () => {
146
- const ref = React.createRef<RNTextInput>();
147
110
  const { getByTestId } = renderWithTheme(
148
111
  <InputRow
149
112
  {...defaultProps}
150
113
  state="error"
151
114
  isEmptyValue={false}
152
- ref={ref}
115
+ shouldShowWhenUnfocused
153
116
  />
154
117
  );
155
-
156
118
  // Components should render with error state
157
119
  const inputWrapper = getByTestId('input-row-input-wrapper');
158
120
  expect(inputWrapper).toBeTruthy();
159
121
  });
160
122
 
161
123
  it('should handle disabled state appropriately', () => {
162
- const ref = React.createRef<RNTextInput>();
163
124
  const { getByTestId } = renderWithTheme(
164
- <InputRow {...defaultProps} state="disabled" isEmptyValue ref={ref} />
125
+ <InputRow
126
+ {...defaultProps}
127
+ state="disabled"
128
+ isEmptyValue
129
+ shouldShowWhenUnfocused
130
+ />
165
131
  );
166
-
167
132
  // Even disabled inputs should render components
168
133
  const inputWrapper = getByTestId('input-row-input-wrapper');
169
134
  expect(inputWrapper).toBeTruthy();
@@ -181,16 +146,15 @@ describe('InputRow', () => {
181
146
  <InputRow
182
147
  {...defaultProps}
183
148
  renderInputValue={customRenderer}
184
- isFocused
185
149
  isEmptyValue={false}
186
150
  nativeInputProps={{
187
151
  ...defaultProps.nativeInputProps,
188
152
  value: 'test value',
189
153
  }}
190
154
  ref={ref}
155
+ shouldShowWhenUnfocused
191
156
  />
192
157
  );
193
-
194
158
  // User should see the custom rendered input
195
159
  expect(getByTestId('custom-input-renderer')).toBeTruthy();
196
160
  });
@@ -200,7 +164,12 @@ describe('InputRow', () => {
200
164
  it('should properly handle input ref', () => {
201
165
  const ref = React.createRef<RNTextInput>();
202
166
  renderWithTheme(
203
- <InputRow {...defaultProps} isFocused isEmptyValue={false} ref={ref} />
167
+ <InputRow
168
+ {...defaultProps}
169
+ isEmptyValue={false}
170
+ ref={ref}
171
+ shouldShowWhenUnfocused
172
+ />
204
173
  );
205
174
  expect(ref.current).toBeDefined();
206
175
  });
@@ -208,31 +177,18 @@ describe('InputRow', () => {
208
177
 
209
178
  describe('accessibility features', () => {
210
179
  it('should provide proper accessibility labels for input wrapper', () => {
211
- const ref = React.createRef<RNTextInput>();
212
- const { getByTestId } = renderWithTheme(
213
- <InputRow {...defaultProps} isFocused isEmptyValue={false} ref={ref} />
214
- );
215
-
216
- // User should have proper accessibility support
217
- const inputWrapper = getByTestId('input-row-input-wrapper');
218
- expect(inputWrapper).toHaveProp('accessibilityLabel', 'Text input field');
219
- });
220
-
221
- it('should hide components from screen readers when not visible', () => {
222
180
  const ref = React.createRef<RNTextInput>();
223
181
  const { getByTestId } = renderWithTheme(
224
182
  <InputRow
225
183
  {...defaultProps}
226
- prefix="search"
227
- state="default"
228
- isEmptyValue
184
+ isEmptyValue={false}
229
185
  ref={ref}
186
+ shouldShowWhenUnfocused
230
187
  />
231
188
  );
232
-
233
- // Components should be hidden from screen readers when not visible
189
+ // User should have proper accessibility support
234
190
  const inputWrapper = getByTestId('input-row-input-wrapper');
235
- expect(inputWrapper).toHaveProp('accessibilityElementsHidden', true);
191
+ expect(inputWrapper).toHaveProp('accessibilityLabel', 'Text input field');
236
192
  });
237
193
  });
238
194
 
@@ -243,7 +199,6 @@ describe('InputRow', () => {
243
199
  <InputRow
244
200
  {...defaultProps}
245
201
  prefix="search"
246
- isFocused
247
202
  isEmptyValue={false}
248
203
  nativeInputProps={{
249
204
  ...defaultProps.nativeInputProps,
@@ -251,25 +206,25 @@ describe('InputRow', () => {
251
206
  placeholder: 'Enter email',
252
207
  }}
253
208
  ref={ref}
209
+ shouldShowWhenUnfocused
254
210
  />
255
211
  );
256
-
257
- // User should see a fully rendered input row
258
212
  const inputWrapper = getByTestId('input-row-input-wrapper');
259
213
  expect(inputWrapper).toBeTruthy();
260
- expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
261
214
  });
262
215
 
263
216
  it('should render without prefix', () => {
264
217
  const ref = React.createRef<RNTextInput>();
265
- const { getByTestId, queryByText } = renderWithTheme(
266
- <InputRow {...defaultProps} ref={ref} />
218
+ const { getByTestId } = renderWithTheme(
219
+ <InputRow
220
+ {...defaultProps}
221
+ isEmptyValue={false}
222
+ ref={ref}
223
+ shouldShowWhenUnfocused
224
+ />
267
225
  );
268
-
269
- // User should see input without prefix
270
226
  const inputWrapper = getByTestId('input-row-input-wrapper');
271
227
  expect(inputWrapper).toBeTruthy();
272
- expect(queryByText('search')).toBeNull();
273
228
  });
274
229
  });
275
230
  });