@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.
Files changed (40) hide show
  1. package/.cursorrules +57 -0
  2. package/CHANGELOG.md +6 -0
  3. package/DEVELOPMENT.md +118 -0
  4. package/eslint.config.js +20 -0
  5. package/lib/index.js +871 -5
  6. package/package.json +4 -1
  7. package/src/__tests__/__snapshots__/index.spec.tsx.snap +90 -115
  8. package/src/__tests__/theme-export-override.spec.ts +6 -0
  9. package/src/components/TextInput/ErrorOrHelpText.tsx +58 -0
  10. package/src/components/TextInput/FloatingLabel.tsx +120 -0
  11. package/src/components/TextInput/InputComponent.tsx +61 -0
  12. package/src/components/TextInput/InputRow.tsx +103 -0
  13. package/src/components/TextInput/MaxLengthMessage.tsx +66 -0
  14. package/src/components/TextInput/PrefixComponent.tsx +77 -0
  15. package/src/components/TextInput/StyledTextInput.tsx +134 -0
  16. package/src/components/TextInput/SuffixComponent.tsx +73 -0
  17. package/src/components/TextInput/__tests__/ErrorOrHelpText.spec.tsx +20 -0
  18. package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +203 -0
  19. package/src/components/TextInput/__tests__/InputComponent.spec.tsx +39 -0
  20. package/src/components/TextInput/__tests__/InputRow.spec.tsx +275 -0
  21. package/src/components/TextInput/__tests__/MaxLengthMessage.spec.tsx +17 -0
  22. package/src/components/TextInput/__tests__/PrefixComponent.spec.tsx +14 -0
  23. package/src/components/TextInput/__tests__/StyledTextInput.spec.tsx +114 -0
  24. package/src/components/TextInput/__tests__/SuffixComponent.spec.tsx +20 -0
  25. package/src/components/TextInput/__tests__/__snapshots__/StyledTextInput.spec.tsx.snap +571 -0
  26. package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +5671 -0
  27. package/src/components/TextInput/__tests__/getState.spec.tsx +89 -0
  28. package/src/components/TextInput/__tests__/index.spec.tsx +699 -0
  29. package/src/components/TextInput/constants.ts +1 -0
  30. package/src/components/TextInput/index.tsx +327 -0
  31. package/src/components/TextInput/types.ts +95 -0
  32. package/src/emotion.d.ts +15 -0
  33. package/src/index.ts +3 -0
  34. package/src/jest.d.ts +24 -0
  35. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +15 -8
  36. package/src/theme/components/textInput.ts +33 -0
  37. package/src/utils/__tests__/helpers.spec.ts +92 -0
  38. package/src/utils/helpers.ts +113 -0
  39. package/testUtils/renderWithTheme.tsx +6 -3
  40. package/stats/1.1.0/rn-work-uikit-stats.html +0 -4842
@@ -0,0 +1,275 @@
1
+ import React from 'react';
2
+ import { Text, TextInput as RNTextInput } from 'react-native';
3
+ import type { TextInputProps as NativeTextInputProps } from 'react-native';
4
+ // import { TextInput as RNTextInput } from 'react-native';
5
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
6
+ import InputRow from '../InputRow';
7
+
8
+ // Mock the LABEL_ANIMATION_DURATION constant
9
+ jest.mock('../constants', () => ({
10
+ LABEL_ANIMATION_DURATION: 0, // Instant animation for testing
11
+ }));
12
+
13
+ describe('InputRow', () => {
14
+ const defaultProps = {
15
+ state: 'default' as const,
16
+ isFocused: false,
17
+ variant: 'text' as const,
18
+ nativeInputProps: {
19
+ value: '',
20
+ placeholder: 'Enter text',
21
+ },
22
+ isEmptyValue: true,
23
+ };
24
+
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
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);
40
+ });
41
+ });
42
+
43
+ describe('when user focuses the input', () => {
44
+ it('should show both prefix and input components with animation', () => {
45
+ const ref = React.createRef<RNTextInput>();
46
+ const { getByTestId } = renderWithTheme(
47
+ <InputRow {...defaultProps} isFocused isEmptyValue ref={ref} />
48
+ );
49
+
50
+ // Should be visible when focused
51
+ const inputWrapper = getByTestId('input-row-input-wrapper');
52
+ expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
53
+ });
54
+ });
55
+
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>();
59
+ const { getByTestId } = renderWithTheme(
60
+ <InputRow
61
+ {...defaultProps}
62
+ prefix="search"
63
+ state="filled"
64
+ isEmptyValue={false}
65
+ nativeInputProps={{
66
+ ...defaultProps.nativeInputProps,
67
+ value: 'user@example.com',
68
+ }}
69
+ ref={ref}
70
+ />
71
+ );
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);
96
+ });
97
+ });
98
+
99
+ describe('when user uses input with custom prefix', () => {
100
+ it('should render custom prefix element', () => {
101
+ const CustomPrefix = () => <Text testID="custom-prefix">Custom</Text>;
102
+ const ref = React.createRef<RNTextInput>();
103
+
104
+ const { getByTestId } = renderWithTheme(
105
+ <InputRow
106
+ {...defaultProps}
107
+ prefix={<CustomPrefix />}
108
+ isFocused
109
+ isEmptyValue={false}
110
+ ref={ref}
111
+ />
112
+ );
113
+
114
+ // User should see the custom prefix
115
+ expect(getByTestId('custom-prefix')).toBeTruthy();
116
+ });
117
+ });
118
+
119
+ describe('when user uses textarea variant', () => {
120
+ it('should render input component with textarea variant', () => {
121
+ const ref = React.createRef<RNTextInput>();
122
+ const { getByTestId } = renderWithTheme(
123
+ <InputRow
124
+ {...defaultProps}
125
+ variant="textarea"
126
+ isFocused
127
+ isEmptyValue={false}
128
+ nativeInputProps={{
129
+ ...defaultProps.nativeInputProps,
130
+ multiline: true,
131
+ numberOfLines: 4,
132
+ }}
133
+ ref={ref}
134
+ />
135
+ );
136
+
137
+ // User should see the textarea input
138
+ const inputWrapper = getByTestId('input-row-input-wrapper');
139
+ expect(inputWrapper).toBeTruthy();
140
+ expect(inputWrapper).toHaveProp('accessibilityLabel', 'Text input field');
141
+ });
142
+ });
143
+
144
+ describe('when user encounters different input states', () => {
145
+ it('should render input wrapper in error state', () => {
146
+ const ref = React.createRef<RNTextInput>();
147
+ const { getByTestId } = renderWithTheme(
148
+ <InputRow
149
+ {...defaultProps}
150
+ state="error"
151
+ isEmptyValue={false}
152
+ ref={ref}
153
+ />
154
+ );
155
+
156
+ // Components should render with error state
157
+ const inputWrapper = getByTestId('input-row-input-wrapper');
158
+ expect(inputWrapper).toBeTruthy();
159
+ });
160
+
161
+ it('should handle disabled state appropriately', () => {
162
+ const ref = React.createRef<RNTextInput>();
163
+ const { getByTestId } = renderWithTheme(
164
+ <InputRow {...defaultProps} state="disabled" isEmptyValue ref={ref} />
165
+ );
166
+
167
+ // Even disabled inputs should render components
168
+ const inputWrapper = getByTestId('input-row-input-wrapper');
169
+ expect(inputWrapper).toBeTruthy();
170
+ });
171
+ });
172
+
173
+ describe('when user uses custom input renderer', () => {
174
+ it('should use custom render function for input value', () => {
175
+ const customRenderer = (inputProps: NativeTextInputProps) => (
176
+ <Text testID="custom-input-renderer">Custom: {inputProps.value}</Text>
177
+ );
178
+ const ref = React.createRef<RNTextInput>();
179
+
180
+ const { getByTestId } = renderWithTheme(
181
+ <InputRow
182
+ {...defaultProps}
183
+ renderInputValue={customRenderer}
184
+ isFocused
185
+ isEmptyValue={false}
186
+ nativeInputProps={{
187
+ ...defaultProps.nativeInputProps,
188
+ value: 'test value',
189
+ }}
190
+ ref={ref}
191
+ />
192
+ );
193
+
194
+ // User should see the custom rendered input
195
+ expect(getByTestId('custom-input-renderer')).toBeTruthy();
196
+ });
197
+ });
198
+
199
+ describe('input reference handling', () => {
200
+ it('should properly handle input ref', () => {
201
+ const ref = React.createRef<RNTextInput>();
202
+ renderWithTheme(
203
+ <InputRow {...defaultProps} isFocused isEmptyValue={false} ref={ref} />
204
+ );
205
+ expect(ref.current).toBeDefined();
206
+ });
207
+ });
208
+
209
+ describe('accessibility features', () => {
210
+ 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
+ const ref = React.createRef<RNTextInput>();
223
+ const { getByTestId } = renderWithTheme(
224
+ <InputRow
225
+ {...defaultProps}
226
+ prefix="search"
227
+ state="default"
228
+ isEmptyValue
229
+ ref={ref}
230
+ />
231
+ );
232
+
233
+ // Components should be hidden from screen readers when not visible
234
+ const inputWrapper = getByTestId('input-row-input-wrapper');
235
+ expect(inputWrapper).toHaveProp('accessibilityElementsHidden', true);
236
+ });
237
+ });
238
+
239
+ describe('component behavior', () => {
240
+ it('should render correctly with all props', () => {
241
+ const ref = React.createRef<RNTextInput>();
242
+ const { getByTestId } = renderWithTheme(
243
+ <InputRow
244
+ {...defaultProps}
245
+ prefix="search"
246
+ isFocused
247
+ isEmptyValue={false}
248
+ nativeInputProps={{
249
+ ...defaultProps.nativeInputProps,
250
+ value: 'test@example.com',
251
+ placeholder: 'Enter email',
252
+ }}
253
+ ref={ref}
254
+ />
255
+ );
256
+
257
+ // User should see a fully rendered input row
258
+ const inputWrapper = getByTestId('input-row-input-wrapper');
259
+ expect(inputWrapper).toBeTruthy();
260
+ expect(inputWrapper).toHaveProp('accessibilityElementsHidden', false);
261
+ });
262
+
263
+ it('should render without prefix', () => {
264
+ const ref = React.createRef<RNTextInput>();
265
+ const { getByTestId, queryByText } = renderWithTheme(
266
+ <InputRow {...defaultProps} ref={ref} />
267
+ );
268
+
269
+ // User should see input without prefix
270
+ const inputWrapper = getByTestId('input-row-input-wrapper');
271
+ expect(inputWrapper).toBeTruthy();
272
+ expect(queryByText('search')).toBeNull();
273
+ });
274
+ });
275
+ });
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
3
+ import MaxLengthMessage from '../MaxLengthMessage';
4
+
5
+ describe('MaxLengthMessage', () => {
6
+ it('renders correctly with maxLength', () => {
7
+ const { queryAllByText } = renderWithTheme(
8
+ <MaxLengthMessage
9
+ maxLength={10}
10
+ state="default"
11
+ currentLength={5}
12
+ hideCharacterCount={false}
13
+ />
14
+ );
15
+ expect(queryAllByText('5/10')).toHaveLength(1);
16
+ });
17
+ });
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
3
+ import PrefixComponent from '../PrefixComponent';
4
+
5
+ describe('PrefixComponent', () => {
6
+ it('renders prefix icon', () => {
7
+ const wrapper = renderWithTheme(
8
+ <PrefixComponent prefix="dollar-sign" state="default" />
9
+ );
10
+ expect(wrapper.getByA11yLabel('Prefix icon: dollar-sign')).toBeDefined();
11
+ // Optionally, keep testID fallback
12
+ expect(wrapper.getByTestId('input-prefix')).toBeDefined();
13
+ });
14
+ });
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
3
+ import {
4
+ StyledHelperText,
5
+ StyledTextInput,
6
+ StyledCharacterCount,
7
+ StyledError,
8
+ StyledErrorRow,
9
+ StyledLabel,
10
+ StyledBorder,
11
+ } from '../StyledTextInput';
12
+
13
+ describe('StyledLabel', () => {
14
+ it.each`
15
+ themeState
16
+ ${'default'}
17
+ ${'filled'}
18
+ ${'error'}
19
+ ${'disabled'}
20
+ ${'readonly'}
21
+ `('renders correctly with themeState $themeState', ({ themeState }): void => {
22
+ const { toJSON } = renderWithTheme(
23
+ <StyledLabel themeState={themeState}>Label</StyledLabel>
24
+ );
25
+
26
+ expect(toJSON()).toMatchSnapshot();
27
+ });
28
+ });
29
+
30
+ describe('StyledErrorRow', () => {
31
+ it('renders correctly', (): void => {
32
+ const { toJSON } = renderWithTheme(<StyledErrorRow />);
33
+
34
+ expect(toJSON()).toMatchSnapshot();
35
+ });
36
+ });
37
+
38
+ describe('StyledError', () => {
39
+ it('renders correctly', (): void => {
40
+ const { toJSON } = renderWithTheme(
41
+ <StyledError>must not exceed character limit</StyledError>
42
+ );
43
+
44
+ expect(toJSON()).toMatchSnapshot();
45
+ });
46
+ });
47
+
48
+ describe('StyledCharacterCount', () => {
49
+ it.each`
50
+ themeState
51
+ ${'default'}
52
+ ${'filled'}
53
+ ${'error'}
54
+ ${'disabled'}
55
+ ${'readonly'}
56
+ `('renders correctly with themeState $themeState', ({ themeState }): void => {
57
+ const { toJSON } = renderWithTheme(
58
+ <StyledCharacterCount themeState={themeState}>
59
+ 100/255
60
+ </StyledCharacterCount>
61
+ );
62
+
63
+ expect(toJSON()).toMatchSnapshot();
64
+ });
65
+ });
66
+
67
+ describe('StyledHelperText', () => {
68
+ it('renders correctly', (): void => {
69
+ const { toJSON } = renderWithTheme(
70
+ <StyledHelperText>helper text</StyledHelperText>
71
+ );
72
+
73
+ expect(toJSON()).toMatchSnapshot();
74
+ });
75
+ });
76
+
77
+ describe('StyledBorder', () => {
78
+ it.each`
79
+ themeState
80
+ ${'default'}
81
+ ${'filled'}
82
+ ${'error'}
83
+ ${'disabled'}
84
+ ${'readonly'}
85
+ `('renders correctly with themeState $themeState', ({ themeState }): void => {
86
+ const { toJSON } = renderWithTheme(
87
+ <StyledBorder themeState={themeState} themeFocused={false} />
88
+ );
89
+
90
+ expect(toJSON()).toMatchSnapshot();
91
+ });
92
+
93
+ it('renders correctly when focused', (): void => {
94
+ const { toJSON } = renderWithTheme(
95
+ <StyledBorder themeState="error" themeFocused />
96
+ );
97
+
98
+ expect(toJSON()).toMatchSnapshot();
99
+ });
100
+ });
101
+
102
+ describe('StyledTextInput', () => {
103
+ it.each`
104
+ themeVariant
105
+ ${'text'}
106
+ ${'textarea'}
107
+ `('renders correctly with $themeState state', ({ themeVariant }) => {
108
+ const { toJSON } = renderWithTheme(
109
+ <StyledTextInput themeVariant={themeVariant} />
110
+ );
111
+
112
+ expect(toJSON()).toMatchSnapshot();
113
+ });
114
+ });
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
3
+ import SuffixComponent from '../SuffixComponent';
4
+
5
+ describe('SuffixComponent', () => {
6
+ it('renders loading icon with loading', () => {
7
+ const wrapper = renderWithTheme(
8
+ <SuffixComponent loading suffix="dollar-sign" state="default" />
9
+ );
10
+
11
+ expect(wrapper.getByA11yLabel('Suffix icon: loading')).toBeDefined();
12
+ });
13
+
14
+ it('renders suffix icon', () => {
15
+ const wrapper = renderWithTheme(
16
+ <SuffixComponent loading={false} suffix="dollar-sign" state="default" />
17
+ );
18
+ expect(wrapper.getByA11yLabel('Suffix icon: dollar-sign')).toBeDefined();
19
+ });
20
+ });