@hero-design/rn-work-uikit 1.1.0 → 1.2.0-alpha.1

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 (49) hide show
  1. package/.cursorrules +57 -0
  2. package/CHANGELOG.md +16 -0
  3. package/DEVELOPMENT.md +118 -0
  4. package/assets/fonts/BeVietnamPro-Bold.ttf +0 -0
  5. package/assets/fonts/BeVietnamPro-Light.ttf +0 -0
  6. package/assets/fonts/BeVietnamPro-Regular.ttf +0 -0
  7. package/assets/fonts/BeVietnamPro-SemiBold.ttf +0 -0
  8. package/assets/fonts/Saiga-Light.otf +0 -0
  9. package/assets/fonts/Saiga-Medium.otf +0 -0
  10. package/assets/fonts/Saiga-Regular.otf +0 -0
  11. package/assets/fonts/hero-icons-mobile.ttf +0 -0
  12. package/eslint.config.js +20 -0
  13. package/lib/index.js +871 -5
  14. package/package.json +4 -1
  15. package/rollup.config.mjs +2 -2
  16. package/src/__tests__/__snapshots__/index.spec.tsx.snap +90 -115
  17. package/src/__tests__/theme-export-override.spec.ts +6 -0
  18. package/src/components/TextInput/ErrorOrHelpText.tsx +58 -0
  19. package/src/components/TextInput/FloatingLabel.tsx +120 -0
  20. package/src/components/TextInput/InputComponent.tsx +61 -0
  21. package/src/components/TextInput/InputRow.tsx +103 -0
  22. package/src/components/TextInput/MaxLengthMessage.tsx +66 -0
  23. package/src/components/TextInput/PrefixComponent.tsx +77 -0
  24. package/src/components/TextInput/StyledTextInput.tsx +134 -0
  25. package/src/components/TextInput/SuffixComponent.tsx +73 -0
  26. package/src/components/TextInput/__tests__/ErrorOrHelpText.spec.tsx +20 -0
  27. package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +203 -0
  28. package/src/components/TextInput/__tests__/InputComponent.spec.tsx +39 -0
  29. package/src/components/TextInput/__tests__/InputRow.spec.tsx +275 -0
  30. package/src/components/TextInput/__tests__/MaxLengthMessage.spec.tsx +17 -0
  31. package/src/components/TextInput/__tests__/PrefixComponent.spec.tsx +14 -0
  32. package/src/components/TextInput/__tests__/StyledTextInput.spec.tsx +114 -0
  33. package/src/components/TextInput/__tests__/SuffixComponent.spec.tsx +20 -0
  34. package/src/components/TextInput/__tests__/__snapshots__/StyledTextInput.spec.tsx.snap +571 -0
  35. package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +5671 -0
  36. package/src/components/TextInput/__tests__/getState.spec.tsx +89 -0
  37. package/src/components/TextInput/__tests__/index.spec.tsx +699 -0
  38. package/src/components/TextInput/constants.ts +1 -0
  39. package/src/components/TextInput/index.tsx +327 -0
  40. package/src/components/TextInput/types.ts +95 -0
  41. package/src/emotion.d.ts +15 -0
  42. package/src/index.ts +3 -0
  43. package/src/jest.d.ts +24 -0
  44. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +15 -8
  45. package/src/theme/components/textInput.ts +33 -0
  46. package/src/utils/__tests__/helpers.spec.ts +92 -0
  47. package/src/utils/helpers.ts +113 -0
  48. package/testUtils/renderWithTheme.tsx +6 -3
  49. package/stats/1.1.0/rn-work-uikit-stats.html +0 -4842
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { StyledCharacterCount } from './StyledTextInput';
3
+ import type { State } from './StyledTextInput';
4
+
5
+ export interface MaxLengthMessageProps {
6
+ /** Maximum allowed characters (undefined = no limit) */
7
+ maxLength?: number;
8
+ /** Current input state for styling */
9
+ state: State;
10
+ /** Current number of characters entered */
11
+ currentLength: number;
12
+ /** Whether to hide the counter display */
13
+ hideCharacterCount: boolean;
14
+ }
15
+
16
+ /**
17
+ * MaxLengthMessage Component
18
+ *
19
+ * Displays character count information for TextInput fields with maximum length restrictions.
20
+ * Shows current/max character count with state-aware styling.
21
+ *
22
+ * Key Features:
23
+ * - Character count display: Shows "currentLength/maxLength" format
24
+ * - Conditional rendering: Only shows when maxLength is set and not hidden
25
+ * - State-aware styling: Colors change based on input state (error, disabled, etc.)
26
+ * - Real-time updates: Updates as user types to show current progress
27
+ * - Hide capability: Can be hidden via hideCharacterCount prop
28
+ *
29
+ * Display Logic:
30
+ * - Shows only when: maxLength is defined AND hideCharacterCount is false
31
+ * - Format: "currentLength/maxLength" (e.g., "25/100")
32
+ * - Hidden when: maxLength is undefined OR hideCharacterCount is true
33
+ *
34
+ * State-based Styling:
35
+ * - Uses themeState to apply appropriate colors from theme
36
+ * - Error state: Shows error color to indicate validation issues
37
+ * - Disabled state: Shows muted color for disabled inputs
38
+ * - Normal states: Shows standard text colors
39
+ *
40
+ * Usage Example:
41
+ * - Input with 25 characters and 100 max: displays "25/100"
42
+ * - Input approaching limit: color may change based on theme
43
+ * - Input over limit: error state styling applied
44
+ *
45
+ * @param props - The component props (see MaxLengthMessageProps interface for details)
46
+ */
47
+ const MaxLengthMessage: React.FC<MaxLengthMessageProps> = ({
48
+ maxLength,
49
+ state,
50
+ currentLength,
51
+ hideCharacterCount,
52
+ }) => {
53
+ if (!maxLength || hideCharacterCount) {
54
+ return null;
55
+ }
56
+
57
+ return (
58
+ <StyledCharacterCount themeState={state}>
59
+ {currentLength}/{maxLength}
60
+ </StyledCharacterCount>
61
+ );
62
+ };
63
+
64
+ MaxLengthMessage.displayName = 'MaxLengthMessage';
65
+
66
+ export default React.memo(MaxLengthMessage);
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { Icon, IconName } from '@hero-design/rn';
3
+ import { View, ViewProps } from 'react-native';
4
+ import type { State } from './StyledTextInput';
5
+
6
+ export interface PrefixComponentProps extends ViewProps {
7
+ /** Current input state for styling decisions */
8
+ state: State;
9
+ /** Icon name string or custom React element to display */
10
+ prefix?: IconName | React.ReactElement;
11
+ }
12
+
13
+ /**
14
+ * PrefixComponent
15
+ *
16
+ * Renders prefix content (icons or custom elements) on the left side of the TextInput,
17
+ * before the user's input cursor.
18
+ *
19
+ * Key Features:
20
+ * - Icon rendering: supports string icon names from the design system
21
+ * - Custom element support: can render any React element as prefix
22
+ * - State-aware styling: adjusts icon intent based on input state
23
+ * - Consistent sizing: uses 'xsmall' size for visual balance
24
+ * - Proper test IDs for automated testing
25
+ *
26
+ * Rendering Logic:
27
+ * 1. String icon name: Renders Icon component with state-based intent
28
+ * 2. Custom element: Renders the provided React element as-is
29
+ * 3. No prefix: Renders null
30
+ *
31
+ * Icon Intent Logic:
32
+ * - Disabled state: Uses 'disabled-text' intent for muted appearance
33
+ * - All other states: Uses 'text' intent for normal appearance
34
+ *
35
+ * Visual Positioning:
36
+ * - Positioned before the text input cursor
37
+ * - Uses smaller 'xsmall' size to avoid overwhelming the input
38
+ * - Maintains proper spacing via parent container
39
+ *
40
+ * @param props - The component props (see PrefixComponentProps interface for details)
41
+ */
42
+ const PrefixComponent: React.FC<PrefixComponentProps> = ({
43
+ state,
44
+ prefix,
45
+ ...rest
46
+ }) => {
47
+ const actualPrefix = typeof prefix === 'string' ? prefix : '';
48
+
49
+ if (typeof prefix === 'string' && actualPrefix) {
50
+ return (
51
+ <View testID="input-prefix" {...rest}>
52
+ <Icon
53
+ intent={state === 'disabled' ? 'disabled-text' : 'text'}
54
+ icon={actualPrefix}
55
+ size="xsmall"
56
+ testID="input-prefix-icon"
57
+ accessibilityLabel={`Prefix icon: ${actualPrefix}`}
58
+ accessibilityRole="image"
59
+ />
60
+ </View>
61
+ );
62
+ }
63
+
64
+ if (React.isValidElement(prefix)) {
65
+ return (
66
+ <View testID="input-prefix" {...rest}>
67
+ {prefix}
68
+ </View>
69
+ );
70
+ }
71
+
72
+ return null;
73
+ };
74
+
75
+ PrefixComponent.displayName = 'PrefixComponent';
76
+
77
+ export default React.memo(PrefixComponent);
@@ -0,0 +1,134 @@
1
+ import { TextInput, View, StyleSheet, Animated, Pressable } from 'react-native';
2
+ import { Typography, styled } from '@hero-design/rn';
3
+
4
+ export type State = 'default' | 'filled' | 'disabled' | 'readonly' | 'error';
5
+
6
+ type Variant = 'text' | 'textarea';
7
+
8
+ const StyledContainer = styled(Pressable)(({ theme }) => ({
9
+ width: '100%',
10
+ flexDirection: 'row',
11
+ paddingHorizontal: theme.__hd__.textInput.space.containerPadding,
12
+ minHeight: theme.__hd__.textInput.sizes.containerMinHeight,
13
+ marginTop: theme.__hd__.textInput.space.containerMarginTop,
14
+ }));
15
+
16
+ const StyledFloatingLabelContainer = styled(Animated.View)<{
17
+ themeVariant: Variant;
18
+ }>(({ themeVariant }) => ({
19
+ flexDirection: 'row',
20
+ flex: 1,
21
+ alignItems: themeVariant === 'text' ? 'center' : 'flex-start',
22
+ backgroundColor: 'transparent',
23
+ }));
24
+
25
+ const StyledLabel = styled(Typography.Caption)<{
26
+ themeState: State;
27
+ }>(({ theme, themeState }) => ({
28
+ color: theme.__hd__.textInput.colors.labels[themeState],
29
+ }));
30
+
31
+ const StyledErrorRow = styled(View)(({ theme }) => ({
32
+ marginRight: theme.__hd__.textInput.space.errorContainerMarginRight,
33
+ flexDirection: 'row',
34
+ alignItems: 'center',
35
+ flex: 1,
36
+ flexGrow: 4,
37
+ }));
38
+
39
+ const StyledError = styled(Typography.Caption)(({ theme }) => ({
40
+ color: theme.__hd__.textInput.colors.error,
41
+ marginLeft: theme.__hd__.textInput.space.errorMarginLeft,
42
+ }));
43
+
44
+ const StyledCharacterCount = styled(Typography.Caption)<{
45
+ themeState: State;
46
+ }>(({ theme, themeState }) => ({
47
+ color: theme.__hd__.textInput.colors.maxLengthLabels[themeState],
48
+ flex: 1,
49
+ flexGrow: 1,
50
+ textAlign: 'right',
51
+ }));
52
+
53
+ const StyledHelperText = Typography.Caption;
54
+ const StyledTextInput = styled(TextInput)<{ themeVariant: Variant }>(
55
+ ({ theme, themeVariant }) => ({
56
+ textAlignVertical: themeVariant === 'text' ? 'center' : 'top',
57
+ fontSize: theme.__hd__.textInput.fontSizes.text,
58
+ flexGrow: 2,
59
+ paddingVertical: 0,
60
+ maxHeight: theme.__hd__.textInput.sizes.textInputMaxHeight,
61
+ minHeight: theme.__hd__.textInput.sizes.textInputMinHeight,
62
+ height:
63
+ themeVariant === 'text'
64
+ ? undefined
65
+ : theme.__hd__.textInput.sizes.textareaHeight,
66
+ fontFamily: theme.__hd__.textInput.fonts.text,
67
+ })
68
+ );
69
+
70
+ const StyledBorder = styled(View)<{
71
+ themeState: State;
72
+ themeFocused: boolean;
73
+ }>(({ theme, themeFocused, themeState }) => ({
74
+ ...StyleSheet.absoluteFillObject,
75
+ borderWidth: theme.__hd__.textInput.borderWidths.container.normal,
76
+ borderRadius: theme.__hd__.textInput.radii.container,
77
+ borderColor: themeFocused
78
+ ? theme.__hd__.textInput.colors.borders.focused
79
+ : theme.__hd__.textInput.colors.borders[themeState],
80
+ }));
81
+
82
+ const StyledInputWrapper = styled(View)(({ theme }) => ({
83
+ flexDirection: 'column',
84
+ flex: 1,
85
+ backgroundColor: 'transparent',
86
+ borderRadius: theme.__hd__.textInput.radii.container,
87
+ marginBottom: theme.__hd__.textInput.space.inputWrapperMarginVertical,
88
+ marginTop: theme.__hd__.textInput.space.inputWrapperMarginVertical,
89
+ overflow: 'hidden',
90
+ }));
91
+
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
+ }));
99
+
100
+ const StyledErrorAndHelpTextContainer = styled(View)(({ theme }) => ({
101
+ paddingHorizontal:
102
+ theme.__hd__.textInput.space.errorAndHelpTextContainerHorizontalPadding,
103
+ minHeight: theme.__hd__.textInput.sizes.errorAndHelpTextContainerHeight,
104
+ paddingTop: theme.__hd__.textInput.space.errorAndHelpTextContainerPaddingTop,
105
+ }));
106
+
107
+ const StyledBottomContainer = styled(View)(() => ({
108
+ flexDirection: 'row',
109
+ justifyContent: 'space-between',
110
+ alignItems: 'flex-start',
111
+ }));
112
+
113
+ const StyledSuffixContainer = styled(View)(() => ({
114
+ flexDirection: 'row',
115
+ alignItems: 'center',
116
+ justifyContent: 'flex-end',
117
+ }));
118
+
119
+ export {
120
+ StyledInputWrapper,
121
+ StyledTextInput,
122
+ StyledError,
123
+ StyledCharacterCount,
124
+ StyledContainer,
125
+ StyledErrorRow,
126
+ StyledHelperText,
127
+ StyledInputRow,
128
+ StyledFloatingLabelContainer,
129
+ StyledErrorAndHelpTextContainer,
130
+ StyledBorder,
131
+ StyledBottomContainer,
132
+ StyledSuffixContainer,
133
+ StyledLabel,
134
+ };
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import { Icon, IconName } from '@hero-design/rn';
3
+ import type { State } from './StyledTextInput';
4
+
5
+ export interface SuffixComponentProps {
6
+ /** Current input state for styling decisions */
7
+ state: State;
8
+ /** Whether to show loading spinner instead of suffix */
9
+ loading: boolean;
10
+ /** Icon name string or custom React element to display */
11
+ suffix?: IconName | React.ReactElement;
12
+ }
13
+
14
+ /**
15
+ * SuffixComponent
16
+ *
17
+ * Renders suffix content (icons or custom elements) on the right side of the TextInput.
18
+ * Handles loading states and custom elements.
19
+ *
20
+ * Key Features:
21
+ * - Loading state handling: shows spinner when loading is true
22
+ * - Icon rendering: supports string icon names from the design system
23
+ * - Custom element support: can render any React element as suffix
24
+ * - State-aware styling: adjusts icon intent based on input state
25
+ * - Spinning animation: loading icons automatically spin
26
+ * - Proper test IDs for automated testing
27
+ *
28
+ * Rendering Priority:
29
+ * 1. Loading state: Shows 'loading' icon with spin animation
30
+ * 2. String icon name: Renders Icon component with state-based intent
31
+ * 3. Custom element: Renders the provided React element as-is
32
+ * 4. No suffix: Renders null
33
+ *
34
+ * Icon Intent Logic:
35
+ * - Disabled state: Uses 'disabled-text' intent
36
+ * - All other states: Uses 'text' intent
37
+ *
38
+ * @param props - The component props (see SuffixComponentProps interface for details)
39
+ */
40
+ const SuffixComponent: React.FC<SuffixComponentProps> = ({
41
+ state,
42
+ loading,
43
+ suffix,
44
+ }) => {
45
+ // Loading state overrides suffix
46
+ const actualSuffix = loading ? 'loading' : suffix;
47
+
48
+ if (typeof actualSuffix === 'string') {
49
+ // Render icon from design system
50
+ return (
51
+ <Icon
52
+ intent={state === 'disabled' ? 'disabled-text' : 'text'}
53
+ testID="input-suffix"
54
+ accessibilityLabel={`Suffix icon: ${actualSuffix}`}
55
+ accessibilityRole="image"
56
+ icon={actualSuffix}
57
+ spin={actualSuffix === 'loading'} // Animate loading icon
58
+ size="medium"
59
+ />
60
+ );
61
+ }
62
+
63
+ if (actualSuffix) {
64
+ // Render custom element
65
+ return suffix as React.ReactElement;
66
+ }
67
+
68
+ return null;
69
+ };
70
+
71
+ SuffixComponent.displayName = 'SuffixComponent';
72
+
73
+ export default React.memo(SuffixComponent);
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
3
+ import ErrorOrHelpText from '../ErrorOrHelpText';
4
+
5
+ describe('ErrorOrHelpText', () => {
6
+ it('renders correctly with error', () => {
7
+ const { queryAllByText } = renderWithTheme(
8
+ <ErrorOrHelpText error="This is error" helpText="This is help text" />
9
+ );
10
+ expect(queryAllByText('This is error')).toHaveLength(1);
11
+ expect(queryAllByText('This is help text')).toHaveLength(0);
12
+ });
13
+
14
+ it('renders correctly with help text', () => {
15
+ const { queryAllByText } = renderWithTheme(
16
+ <ErrorOrHelpText error="" helpText="This is help text" />
17
+ );
18
+ expect(queryAllByText('This is help text')).toHaveLength(1);
19
+ });
20
+ });
@@ -0,0 +1,203 @@
1
+ import React from 'react';
2
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
3
+ import FloatingLabel from '../FloatingLabel';
4
+
5
+ describe('FloatingLabel', () => {
6
+ describe('label text display based on required prop', () => {
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'}
11
+ `(
12
+ 'should display $expectedText for $description',
13
+ ({ required, expectedText, shouldShowOptional }) => {
14
+ const label = required ? 'Email' : 'Phone';
15
+ const { getByText, queryByText } = renderWithTheme(
16
+ <FloatingLabel
17
+ label={label}
18
+ variant="text"
19
+ state="default"
20
+ isFocused={false}
21
+ required={required}
22
+ isEmptyValue
23
+ />
24
+ );
25
+
26
+ // User should see the appropriate label text
27
+ expect(getByText(expectedText)).toBeTruthy();
28
+
29
+ if (!shouldShowOptional) {
30
+ expect(queryByText(`${label} (Optional)`)).toBeFalsy();
31
+ expect(queryByText('(Optional)')).toBeFalsy();
32
+ }
33
+ }
34
+ );
35
+ });
36
+
37
+ describe('label behavior based on focus and content state', () => {
38
+ 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'}
42
+ `(
43
+ 'should show $expectedBehavior when $description',
44
+ ({ state, isEmptyValue }) => {
45
+ const { getByTestId } = renderWithTheme(
46
+ <FloatingLabel
47
+ label={state === 'focused' ? 'Password' : 'Message'}
48
+ variant="text"
49
+ state={state}
50
+ isFocused={state === 'focused'}
51
+ required
52
+ isEmptyValue={isEmptyValue}
53
+ />
54
+ );
55
+
56
+ // User should see the label behaving appropriately
57
+ 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);
64
+ }
65
+ );
66
+ });
67
+
68
+ describe('variant-specific positioning', () => {
69
+ it.each`
70
+ variant | required | expectedText | description
71
+ ${'textarea'} | ${false} | ${'Description (Optional)'} | ${'multiline input with optional label'}
72
+ `(
73
+ 'should position label appropriately for $variant ($description)',
74
+ ({ variant, required, expectedText }) => {
75
+ const { getByTestId } = renderWithTheme(
76
+ <FloatingLabel
77
+ label="Description"
78
+ variant={variant}
79
+ state="default"
80
+ isFocused={false}
81
+ required={required}
82
+ isEmptyValue
83
+ />
84
+ );
85
+
86
+ // User should see the label with proper variant positioning
87
+ 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
+ );
93
+ }
94
+ );
95
+ });
96
+
97
+ describe('when user encounters different input states', () => {
98
+ 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'}
102
+ `(
103
+ 'should display appropriate styling for $state state ($description)',
104
+ ({ state, required, expectedText }) => {
105
+ const { getByTestId } = renderWithTheme(
106
+ <FloatingLabel
107
+ label={state === 'error' ? 'Email' : 'Disabled Field'}
108
+ variant="text"
109
+ state={state}
110
+ isFocused={false}
111
+ required={required}
112
+ isEmptyValue
113
+ />
114
+ );
115
+
116
+ // User should see the label with appropriate state styling
117
+ 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);
123
+ }
124
+ );
125
+ });
126
+
127
+ describe('when used with accessibility features', () => {
128
+ it('should provide proper accessibility labeling', () => {
129
+ const accessibilityId = 'email-input-label';
130
+ const { getByTestId } = renderWithTheme(
131
+ <FloatingLabel
132
+ label="Email Address"
133
+ variant="text"
134
+ state="default"
135
+ isFocused={false}
136
+ required
137
+ accessibilityLabelledBy={accessibilityId}
138
+ isEmptyValue
139
+ />
140
+ );
141
+
142
+ // User should have proper accessibility support
143
+ const label = getByTestId('input-label');
144
+ expect(label).toHaveProp('nativeID', accessibilityId);
145
+ });
146
+ });
147
+
148
+ describe('label color styling based on state', () => {
149
+ it.each`
150
+ state | isFocused | expectedColor | description
151
+ ${'default'} | ${false} | ${'#808f91'} | ${'inactive/empty field'}
152
+ ${'default'} | ${true} | ${'#001f23'} | ${'user is typing'}
153
+ ${'filled'} | ${false} | ${'#001f23'} | ${'field has content'}
154
+ ${'filled'} | ${true} | ${'#001f23'} | ${'field has content and focused'}
155
+ ${'error'} | ${false} | ${'#cb300a'} | ${'validation error'}
156
+ ${'error'} | ${true} | ${'#001f23'} | ${'validation error but focused'}
157
+ ${'disabled'} | ${false} | ${'#bfc1c5'} | ${'non-interactive field'}
158
+ ${'readonly'} | ${false} | ${'#808f91'} | ${'read-only field'}
159
+ `(
160
+ 'should apply $expectedColor color for $state state when focused=$isFocused ($description)',
161
+ ({ state, isFocused, expectedColor: _expectedColor }) => {
162
+ const { getByTestId } = renderWithTheme(
163
+ <FloatingLabel
164
+ label={`${state} Label`}
165
+ variant="text"
166
+ state={state}
167
+ isFocused={isFocused}
168
+ required
169
+ isEmptyValue={state !== 'filled'}
170
+ />
171
+ );
172
+
173
+ const label = getByTestId('input-label');
174
+ expect(label).toHaveProp('themeState', state);
175
+ }
176
+ );
177
+
178
+ describe('component behavior', () => {
179
+ it('should render correctly with all props', () => {
180
+ const { getByTestId } = renderWithTheme(
181
+ <FloatingLabel
182
+ label="Full Name"
183
+ variant="text"
184
+ state="filled"
185
+ isFocused
186
+ required={false}
187
+ accessibilityLabelledBy="fullname-label"
188
+ isEmptyValue={false}
189
+ />
190
+ );
191
+
192
+ // User should see a fully rendered floating label
193
+ const label = getByTestId('input-label');
194
+ expect(label).toBeTruthy();
195
+ 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
+ );
200
+ });
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { TextInput as RNTextInput } from 'react-native';
3
+ import renderWithTheme from '../../../../testUtils/renderWithTheme';
4
+ import InputComponent from '../InputComponent';
5
+
6
+ describe('InputComponent', () => {
7
+ it('renders correctly with renderInputValue', () => {
8
+ const ref = React.createRef<RNTextInput>();
9
+ const wrapper = renderWithTheme(
10
+ <InputComponent
11
+ variant="textarea"
12
+ nativeInputProps={{}}
13
+ renderInputValue={(props) => (
14
+ <RNTextInput
15
+ {...props}
16
+ value="customised text"
17
+ testID="custom-text-input"
18
+ />
19
+ )}
20
+ ref={ref}
21
+ />
22
+ );
23
+ expect(wrapper.queryAllByTestId('custom-text-input')).toHaveLength(1);
24
+ expect(wrapper.queryAllByDisplayValue('customised text')).toHaveLength(1);
25
+ });
26
+
27
+ it('renders correctly without renderInputValue', () => {
28
+ const ref = React.createRef<RNTextInput>();
29
+ const wrapper = renderWithTheme(
30
+ <InputComponent
31
+ variant="textarea"
32
+ nativeInputProps={{ testID: 'text-input', value: 'text input value' }}
33
+ ref={ref}
34
+ />
35
+ );
36
+ expect(wrapper.queryAllByTestId('text-input')).toHaveLength(1);
37
+ expect(wrapper.queryAllByDisplayValue('text input value')).toHaveLength(1);
38
+ });
39
+ });