@hero-design/rn-work-uikit 1.1.0-alpha.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.
- package/.cursorrules +57 -0
- package/CHANGELOG.md +8 -3
- package/DEVELOPMENT.md +118 -0
- package/THEME_OVERRIDE.md +52 -0
- package/eslint.config.js +20 -0
- package/lib/index.js +1000 -4
- package/locales/en_AU.js +10 -0
- package/locales/en_AU.mjs +8 -0
- package/locales/en_CA.js +10 -0
- package/locales/en_CA.mjs +8 -0
- package/locales/index.js +11 -0
- package/locales/index.mjs +9 -0
- package/locales/types.js +2 -0
- package/locales/types.mjs +1 -0
- package/package.json +8 -4
- package/rollup.config.mjs +18 -2
- package/src/__tests__/__snapshots__/index.spec.tsx.snap +91 -116
- package/src/__tests__/index.spec.tsx +15 -0
- package/src/__tests__/theme-export-override.spec.ts +96 -0
- package/src/components/TextInput/ErrorOrHelpText.tsx +58 -0
- package/src/components/TextInput/FloatingLabel.tsx +120 -0
- package/src/components/TextInput/InputComponent.tsx +61 -0
- package/src/components/TextInput/InputRow.tsx +103 -0
- package/src/components/TextInput/MaxLengthMessage.tsx +66 -0
- package/src/components/TextInput/PrefixComponent.tsx +77 -0
- package/src/components/TextInput/StyledTextInput.tsx +134 -0
- package/src/components/TextInput/SuffixComponent.tsx +73 -0
- package/src/components/TextInput/__tests__/ErrorOrHelpText.spec.tsx +20 -0
- package/src/components/TextInput/__tests__/FloatingLabel.spec.tsx +203 -0
- package/src/components/TextInput/__tests__/InputComponent.spec.tsx +39 -0
- package/src/components/TextInput/__tests__/InputRow.spec.tsx +275 -0
- package/src/components/TextInput/__tests__/MaxLengthMessage.spec.tsx +17 -0
- package/src/components/TextInput/__tests__/PrefixComponent.spec.tsx +14 -0
- package/src/components/TextInput/__tests__/StyledTextInput.spec.tsx +114 -0
- package/src/components/TextInput/__tests__/SuffixComponent.spec.tsx +20 -0
- package/src/components/TextInput/__tests__/__snapshots__/StyledTextInput.spec.tsx.snap +571 -0
- package/src/components/TextInput/__tests__/__snapshots__/index.spec.tsx.snap +5671 -0
- package/src/components/TextInput/__tests__/getState.spec.tsx +89 -0
- package/src/components/TextInput/__tests__/index.spec.tsx +699 -0
- package/src/components/TextInput/constants.ts +1 -0
- package/src/components/TextInput/index.tsx +327 -0
- package/src/components/TextInput/types.ts +95 -0
- package/src/emotion.d.ts +15 -0
- package/src/index.ts +16 -1
- package/src/jest.d.ts +24 -0
- package/src/theme/ThemeProvider.ts +20 -0
- package/src/theme/ThemeSwitcher.tsx +76 -0
- package/src/theme/__tests__/ThemeProvider.spec.tsx +32 -0
- package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +1851 -0
- package/src/theme/__tests__/index.spec.ts +7 -0
- package/src/theme/components/textInput.ts +92 -0
- package/src/theme/getTheme.ts +32 -0
- package/src/theme/index.ts +17 -0
- package/src/utils/__tests__/helpers.spec.ts +92 -0
- package/src/utils/helpers.ts +113 -0
- package/testUtils/renderWithTheme.tsx +6 -3
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
2
|
+
import { StyleSheet, TextInput as RNTextInput } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
TextInputProps as NativeTextInputProps,
|
|
5
|
+
StyleProp,
|
|
6
|
+
ViewStyle,
|
|
7
|
+
NativeSyntheticEvent,
|
|
8
|
+
TextInputFocusEventData,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import {
|
|
11
|
+
StyledInputWrapper,
|
|
12
|
+
StyledContainer,
|
|
13
|
+
StyledBorder,
|
|
14
|
+
StyledBottomContainer,
|
|
15
|
+
StyledSuffixContainer,
|
|
16
|
+
} from './StyledTextInput';
|
|
17
|
+
import { useTheme } from '../../theme';
|
|
18
|
+
import type { State } from './StyledTextInput';
|
|
19
|
+
import ErrorOrHelpText from './ErrorOrHelpText';
|
|
20
|
+
import SuffixComponent from './SuffixComponent';
|
|
21
|
+
import MaxLengthMessage from './MaxLengthMessage';
|
|
22
|
+
import FloatingLabel from './FloatingLabel';
|
|
23
|
+
import InputRow from './InputRow';
|
|
24
|
+
import type { TextInputHandles, TextInputProps } from './types';
|
|
25
|
+
|
|
26
|
+
export type {
|
|
27
|
+
TextInputHandles,
|
|
28
|
+
TextInputVariant,
|
|
29
|
+
TextInputProps,
|
|
30
|
+
} from './types';
|
|
31
|
+
|
|
32
|
+
export const getState = ({
|
|
33
|
+
disabled,
|
|
34
|
+
error,
|
|
35
|
+
editable,
|
|
36
|
+
loading,
|
|
37
|
+
isEmptyValue,
|
|
38
|
+
}: {
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
editable?: boolean;
|
|
42
|
+
loading: boolean;
|
|
43
|
+
isEmptyValue?: boolean;
|
|
44
|
+
}): State => {
|
|
45
|
+
if (disabled) {
|
|
46
|
+
return 'disabled';
|
|
47
|
+
}
|
|
48
|
+
if (error) {
|
|
49
|
+
return 'error';
|
|
50
|
+
}
|
|
51
|
+
if (!editable || loading) {
|
|
52
|
+
return 'readonly';
|
|
53
|
+
}
|
|
54
|
+
if (!isEmptyValue) {
|
|
55
|
+
return 'filled';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return 'default';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Fix issue: Placeholder is not shown on iOS when multiline is true
|
|
62
|
+
// https://github.com/callstack/react-native-paper/pull/3331
|
|
63
|
+
const EMPTY_PLACEHOLDER_VALUE = ' ';
|
|
64
|
+
|
|
65
|
+
// Simplified style extraction functions
|
|
66
|
+
const extractBackgroundColor = (style: StyleProp<ViewStyle>) => {
|
|
67
|
+
const flattened = StyleSheet.flatten(style);
|
|
68
|
+
return flattened?.backgroundColor;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const extractBorderStyles = (style: StyleProp<ViewStyle>) => {
|
|
72
|
+
const flattened = StyleSheet.flatten(style);
|
|
73
|
+
if (!flattened) return {};
|
|
74
|
+
|
|
75
|
+
const borderKeys = Object.keys(flattened).filter((key) =>
|
|
76
|
+
key.startsWith('border')
|
|
77
|
+
);
|
|
78
|
+
const borderStyles: Record<string, unknown> = {};
|
|
79
|
+
|
|
80
|
+
borderKeys.forEach((key) => {
|
|
81
|
+
borderStyles[key] = flattened[key as keyof typeof flattened];
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return borderStyles;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* TextInput Layout Structure:
|
|
89
|
+
*
|
|
90
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
91
|
+
* │ StyledContainer (with StyledBorder overlay) │
|
|
92
|
+
* │ ┌─────────────────────────────────────────────────────────┐ │
|
|
93
|
+
* │ │ StyledInputWrapper │ │
|
|
94
|
+
* │ │ ┌─────────────────────────────────────────────────┐ │ │
|
|
95
|
+
* │ │ │ FloatingLabel (Optional) │ │ │
|
|
96
|
+
* │ │ │ "Label" or animated position │ │ ┌──────┐ │
|
|
97
|
+
* │ │ └─────────────────────────────────────────────────┘ │ │Suffix│ │
|
|
98
|
+
* │ │ │ │ Icon │ │
|
|
99
|
+
* │ │ ┌─────────────────────────────────────────────────┐ │ │ or │ │
|
|
100
|
+
* │ │ │ InputRow Component │ │ │Custom│ │
|
|
101
|
+
* │ │ │ ┌──────────┐ ┌─────────────────────────────┐ │ │ │(V- │ │
|
|
102
|
+
* │ │ │ │ Prefix │ │ Input Field │ │ │ │Center│ │
|
|
103
|
+
* │ │ │ │(Animated)│ │ (Animated TextInput) │ │ │ │ ed) │ │
|
|
104
|
+
* │ │ │ └──────────┘ └─────────────────────────────┘ │ │ └──────┘ │
|
|
105
|
+
* │ │ └─────────────────────────────────────────────────┘ │ │
|
|
106
|
+
* │ │ │ │
|
|
107
|
+
* │ │ ┌─────────────────────────────────────────────────┐ │ │
|
|
108
|
+
* │ │ │ StyledBottomContainer │ │ │
|
|
109
|
+
* │ │ │ ┌─────────────────┐ ┌─────────────────────┐ │ │ │
|
|
110
|
+
* │ │ │ │ Error/Help Text │ │ Character Count │ │ │ │
|
|
111
|
+
* │ │ │ │ (flex: 4) │ │ (flex: 1) │ │ │ │
|
|
112
|
+
* │ │ │ └─────────────────┘ └─────────────────────┘ │ │ │
|
|
113
|
+
* │ │ └─────────────────────────────────────────────────┘ │ │
|
|
114
|
+
* │ └─────────────────────────────────────────────────────────┘ │
|
|
115
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
116
|
+
*
|
|
117
|
+
* Note: StyledBorder uses StyleSheet.absoluteFillObject to overlay the entire
|
|
118
|
+
* StyledContainer, providing the border and background styling.
|
|
119
|
+
*/
|
|
120
|
+
const TextInput = forwardRef<TextInputHandles, TextInputProps>(
|
|
121
|
+
(
|
|
122
|
+
{
|
|
123
|
+
label,
|
|
124
|
+
prefix,
|
|
125
|
+
suffix,
|
|
126
|
+
style,
|
|
127
|
+
textStyle,
|
|
128
|
+
testID,
|
|
129
|
+
accessibilityLabelledBy,
|
|
130
|
+
error,
|
|
131
|
+
required,
|
|
132
|
+
editable = true,
|
|
133
|
+
disabled = false,
|
|
134
|
+
loading = false,
|
|
135
|
+
maxLength,
|
|
136
|
+
hideCharacterCount = false,
|
|
137
|
+
helpText,
|
|
138
|
+
value,
|
|
139
|
+
defaultValue,
|
|
140
|
+
renderInputValue,
|
|
141
|
+
allowFontScaling = false,
|
|
142
|
+
variant = 'text',
|
|
143
|
+
...nativeProps
|
|
144
|
+
}: TextInputProps,
|
|
145
|
+
ref?: React.Ref<TextInputHandles>
|
|
146
|
+
) => {
|
|
147
|
+
// Inline the simple getDisplayText function
|
|
148
|
+
const displayText = (value !== undefined ? value : defaultValue) ?? '';
|
|
149
|
+
const isEmptyValue = displayText.length === 0;
|
|
150
|
+
|
|
151
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
152
|
+
|
|
153
|
+
const state = getState({
|
|
154
|
+
disabled,
|
|
155
|
+
error,
|
|
156
|
+
editable,
|
|
157
|
+
loading,
|
|
158
|
+
isEmptyValue,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const theme = useTheme();
|
|
162
|
+
|
|
163
|
+
const innerTextInput = React.useRef<RNTextInput | null>(null);
|
|
164
|
+
React.useImperativeHandle(
|
|
165
|
+
ref,
|
|
166
|
+
() => ({
|
|
167
|
+
// we don't expose this method, it's for testing https://medium.com/developer-rants/how-to-test-useref-without-mocking-useref-699165f4994e
|
|
168
|
+
getNativeTextInputRef: () => innerTextInput.current,
|
|
169
|
+
focus: () => {
|
|
170
|
+
innerTextInput.current?.focus();
|
|
171
|
+
},
|
|
172
|
+
clear: () => innerTextInput.current?.clear(),
|
|
173
|
+
setNativeProps: (args: NativeTextInputProps) =>
|
|
174
|
+
innerTextInput.current?.setNativeProps(args),
|
|
175
|
+
isFocused: () => innerTextInput.current?.isFocused() || false,
|
|
176
|
+
blur: () => innerTextInput.current?.blur(),
|
|
177
|
+
}),
|
|
178
|
+
[innerTextInput]
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Simplified style extraction
|
|
182
|
+
const borderStyles = extractBorderStyles(textStyle);
|
|
183
|
+
const customBackgroundColor = extractBackgroundColor(style);
|
|
184
|
+
const backgroundColor =
|
|
185
|
+
customBackgroundColor ??
|
|
186
|
+
theme.__hd__.textInput.colors.containerBackground;
|
|
187
|
+
|
|
188
|
+
// Simplified callback functions (removed unnecessary memoization for simple cases)
|
|
189
|
+
const handleFocus = useCallback(
|
|
190
|
+
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
191
|
+
setIsFocused(true);
|
|
192
|
+
nativeProps.onFocus?.(event);
|
|
193
|
+
},
|
|
194
|
+
[nativeProps]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const handleBlur = useCallback(
|
|
198
|
+
(event: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
199
|
+
setIsFocused(false);
|
|
200
|
+
nativeProps.onBlur?.(event);
|
|
201
|
+
},
|
|
202
|
+
[nativeProps]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const handleChangeText = useCallback(
|
|
206
|
+
(text: string) => {
|
|
207
|
+
nativeProps.onChangeText?.(text);
|
|
208
|
+
},
|
|
209
|
+
[nativeProps]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Simplified callbacks - these don't need memoization as they're stable
|
|
213
|
+
const handleContainerPress = () => {
|
|
214
|
+
innerTextInput.current?.focus();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Create text input style without border properties
|
|
218
|
+
const textInputStyle = textStyle
|
|
219
|
+
? { ...StyleSheet.flatten(textStyle) }
|
|
220
|
+
: {};
|
|
221
|
+
Object.keys(borderStyles).forEach((key) => {
|
|
222
|
+
delete textInputStyle[key as keyof typeof textInputStyle];
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const nativeInputProps: NativeTextInputProps = {
|
|
226
|
+
style: StyleSheet.flatten([
|
|
227
|
+
{
|
|
228
|
+
backgroundColor,
|
|
229
|
+
color: theme.__hd__.textInput.colors.text,
|
|
230
|
+
},
|
|
231
|
+
textInputStyle,
|
|
232
|
+
]),
|
|
233
|
+
testID: 'text-input',
|
|
234
|
+
accessibilityState: {
|
|
235
|
+
disabled: state === 'disabled' || state === 'readonly',
|
|
236
|
+
},
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
238
|
+
// @ts-ignore
|
|
239
|
+
accessibilityLabelledBy,
|
|
240
|
+
allowFontScaling,
|
|
241
|
+
...nativeProps,
|
|
242
|
+
onFocus: handleFocus,
|
|
243
|
+
onBlur: handleBlur,
|
|
244
|
+
onChangeText: handleChangeText,
|
|
245
|
+
editable,
|
|
246
|
+
maxLength,
|
|
247
|
+
value,
|
|
248
|
+
defaultValue,
|
|
249
|
+
placeholder:
|
|
250
|
+
isFocused || label === undefined
|
|
251
|
+
? nativeProps.placeholder
|
|
252
|
+
: EMPTY_PLACEHOLDER_VALUE,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Create container style without background color
|
|
256
|
+
const containerStyle = style ? { ...StyleSheet.flatten(style) } : {};
|
|
257
|
+
if (customBackgroundColor) {
|
|
258
|
+
delete containerStyle.backgroundColor;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const isDisabledOrReadonly = state === 'disabled' || state === 'readonly';
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<StyledContainer
|
|
265
|
+
style={containerStyle}
|
|
266
|
+
onPress={handleContainerPress}
|
|
267
|
+
disabled={isDisabledOrReadonly}
|
|
268
|
+
accessibilityState={{
|
|
269
|
+
disabled: isDisabledOrReadonly,
|
|
270
|
+
}}
|
|
271
|
+
testID={testID}
|
|
272
|
+
>
|
|
273
|
+
{/*
|
|
274
|
+
StyledBorder: Absolute positioned overlay covering entire container
|
|
275
|
+
- Uses StyleSheet.absoluteFillObject to cover full StyledContainer
|
|
276
|
+
- Provides border styling and background color
|
|
277
|
+
- pointerEvents="none" to allow interaction with underlying components
|
|
278
|
+
*/}
|
|
279
|
+
<StyledBorder
|
|
280
|
+
themeFocused={isFocused}
|
|
281
|
+
themeState={state}
|
|
282
|
+
testID="text-input-border"
|
|
283
|
+
pointerEvents="none"
|
|
284
|
+
style={[{ backgroundColor }, borderStyles]}
|
|
285
|
+
/>
|
|
286
|
+
<StyledInputWrapper>
|
|
287
|
+
{!!label && (
|
|
288
|
+
<FloatingLabel
|
|
289
|
+
label={label}
|
|
290
|
+
variant={variant}
|
|
291
|
+
state={state}
|
|
292
|
+
isFocused={isFocused}
|
|
293
|
+
required={required}
|
|
294
|
+
accessibilityLabelledBy={accessibilityLabelledBy}
|
|
295
|
+
isEmptyValue={isEmptyValue}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
<InputRow
|
|
299
|
+
state={state}
|
|
300
|
+
isFocused={isFocused}
|
|
301
|
+
prefix={prefix}
|
|
302
|
+
variant={variant}
|
|
303
|
+
nativeInputProps={nativeInputProps}
|
|
304
|
+
renderInputValue={renderInputValue}
|
|
305
|
+
ref={innerTextInput}
|
|
306
|
+
isEmptyValue={isEmptyValue}
|
|
307
|
+
/>
|
|
308
|
+
|
|
309
|
+
<StyledBottomContainer>
|
|
310
|
+
<ErrorOrHelpText error={error} helpText={helpText} />
|
|
311
|
+
<MaxLengthMessage
|
|
312
|
+
state={state}
|
|
313
|
+
currentLength={displayText.length}
|
|
314
|
+
maxLength={maxLength}
|
|
315
|
+
hideCharacterCount={hideCharacterCount}
|
|
316
|
+
/>
|
|
317
|
+
</StyledBottomContainer>
|
|
318
|
+
</StyledInputWrapper>
|
|
319
|
+
<StyledSuffixContainer>
|
|
320
|
+
<SuffixComponent state={state} loading={loading} suffix={suffix} />
|
|
321
|
+
</StyledSuffixContainer>
|
|
322
|
+
</StyledContainer>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
export default TextInput;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TextInputProps as NativeTextInputProps,
|
|
3
|
+
StyleProp,
|
|
4
|
+
ViewStyle,
|
|
5
|
+
TextStyle,
|
|
6
|
+
TextInput as RNTextInput,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import type { IconName } from '@hero-design/rn';
|
|
9
|
+
|
|
10
|
+
export type TextInputHandles = Pick<
|
|
11
|
+
RNTextInput,
|
|
12
|
+
'focus' | 'clear' | 'blur' | 'isFocused' | 'setNativeProps'
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export type TextInputVariant = 'text' | 'textarea';
|
|
16
|
+
|
|
17
|
+
export interface TextInputProps extends NativeTextInputProps {
|
|
18
|
+
/**
|
|
19
|
+
* Field label.
|
|
20
|
+
*/
|
|
21
|
+
label?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Name of Icon or ReactElement to render on the left side of the input, before the user's cursor.
|
|
24
|
+
*/
|
|
25
|
+
prefix?: IconName | React.ReactElement;
|
|
26
|
+
/**
|
|
27
|
+
* Name of Icon or ReactElement to render on the right side of the input.
|
|
28
|
+
*/
|
|
29
|
+
suffix?: IconName | React.ReactElement;
|
|
30
|
+
/**
|
|
31
|
+
* Additional wrapper style.
|
|
32
|
+
*/
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
/**
|
|
35
|
+
* Input text style.
|
|
36
|
+
*/
|
|
37
|
+
textStyle?: StyleProp<TextStyle>;
|
|
38
|
+
/**
|
|
39
|
+
* Testing id of the component.
|
|
40
|
+
*/
|
|
41
|
+
testID?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Accessibility label for the input (Android).
|
|
44
|
+
*/
|
|
45
|
+
accessibilityLabelledBy?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Error message to display.
|
|
48
|
+
*/
|
|
49
|
+
error?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the input is required. When false, "(Optional)" will be appended to the label.
|
|
52
|
+
*/
|
|
53
|
+
required?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Placeholder text to display.
|
|
56
|
+
* */
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the input is editable.
|
|
60
|
+
* */
|
|
61
|
+
editable?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the input is disabled.
|
|
64
|
+
*/
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the input is loading.
|
|
68
|
+
*/
|
|
69
|
+
loading?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The max length of the input.
|
|
72
|
+
* If the max length is set, the input will display the current length and the max length.
|
|
73
|
+
* */
|
|
74
|
+
maxLength?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Whether to hide the character count.
|
|
77
|
+
* */
|
|
78
|
+
hideCharacterCount?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The helper text to display.
|
|
81
|
+
*/
|
|
82
|
+
helpText?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Customise input value renderer
|
|
85
|
+
*/
|
|
86
|
+
renderInputValue?: (inputProps: NativeTextInputProps) => React.ReactNode;
|
|
87
|
+
/**
|
|
88
|
+
* Component ref.
|
|
89
|
+
*/
|
|
90
|
+
ref?: React.Ref<TextInputHandles>;
|
|
91
|
+
/**
|
|
92
|
+
* Component variant.
|
|
93
|
+
*/
|
|
94
|
+
variant?: TextInputVariant;
|
|
95
|
+
}
|
package/src/emotion.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import '@emotion/react';
|
|
2
|
+
import '@emotion/native';
|
|
3
|
+
import type { Theme as WorkTheme } from './theme';
|
|
4
|
+
|
|
5
|
+
// Augment the emotion theme for both @emotion/react and @emotion/native
|
|
6
|
+
declare module '@emotion/react' {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
8
|
+
export interface Theme extends WorkTheme {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// This should make styled components automatically get the Theme type
|
|
12
|
+
declare module '@emotion/native' {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
14
|
+
export interface Theme extends WorkTheme {}
|
|
15
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,17 @@
|
|
|
1
|
-
// Re-export
|
|
1
|
+
// Re-export everything from @hero-design/rn except theme exports we want to override
|
|
2
|
+
import TextInput from './components/TextInput';
|
|
3
|
+
|
|
2
4
|
export * from '@hero-design/rn';
|
|
5
|
+
|
|
6
|
+
// Override theme exports with work-specific versions
|
|
7
|
+
export {
|
|
8
|
+
getTheme,
|
|
9
|
+
type Theme,
|
|
10
|
+
ThemeProvider,
|
|
11
|
+
ThemeSwitcher,
|
|
12
|
+
useTheme,
|
|
13
|
+
withTheme,
|
|
14
|
+
} from './theme';
|
|
15
|
+
|
|
16
|
+
export { default as theme } from './theme';
|
|
17
|
+
export { TextInput };
|
package/src/jest.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
namespace jest {
|
|
3
|
+
interface Matchers<R> {
|
|
4
|
+
toHaveProp(prop: string, value?: unknown): R;
|
|
5
|
+
toHaveStyle(style: Record<string, unknown>): R;
|
|
6
|
+
toBeDisabled(): R;
|
|
7
|
+
toBeEnabled(): R;
|
|
8
|
+
toBeVisible(): R;
|
|
9
|
+
toBeEmptyElement(): R;
|
|
10
|
+
toHaveTextContent(text: string | RegExp): R;
|
|
11
|
+
toHaveDisplayValue(value: string | RegExp): R;
|
|
12
|
+
toBeSelected(): R;
|
|
13
|
+
toBePartiallyChecked(): R;
|
|
14
|
+
toBeChecked(): R;
|
|
15
|
+
toHaveA11yLabel(label: string | RegExp): R;
|
|
16
|
+
toHaveA11yHint(hint: string | RegExp): R;
|
|
17
|
+
toHaveA11yRole(role: string): R;
|
|
18
|
+
toHaveA11yState(state: Record<string, boolean | string>): R;
|
|
19
|
+
toHaveA11yValue(value: Record<string, unknown>): R;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ThemeProvider as BaseThemeProvider, useTheme } from '@hero-design/rn';
|
|
3
|
+
import type { Theme } from './getTheme';
|
|
4
|
+
|
|
5
|
+
export interface ThemeProviderProps {
|
|
6
|
+
theme: Partial<Theme> | ((outerTheme: Theme) => Theme);
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ThemeProviderType {
|
|
11
|
+
(props: ThemeProviderProps): React.ReactElement;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const WorkThemeProvider = BaseThemeProvider as ThemeProviderType;
|
|
15
|
+
|
|
16
|
+
const useWorkTheme = useTheme as () => Theme;
|
|
17
|
+
|
|
18
|
+
export { WorkThemeProvider, useWorkTheme };
|
|
19
|
+
|
|
20
|
+
export default WorkThemeProvider;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { PropsWithChildren, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
eBensSystemPalette,
|
|
5
|
+
ehWorkDarkSystemPalette,
|
|
6
|
+
jobsSystemPalette,
|
|
7
|
+
swagDarkSystemPalette,
|
|
8
|
+
swagLightJobsSystemPalette,
|
|
9
|
+
swagLightSystemPalette,
|
|
10
|
+
swagSystemPalette,
|
|
11
|
+
walletSystemPalette,
|
|
12
|
+
workSystemPalette,
|
|
13
|
+
} from '@hero-design/rn';
|
|
14
|
+
import getWorkTheme from './getTheme';
|
|
15
|
+
import { WorkThemeProvider } from './ThemeProvider';
|
|
16
|
+
|
|
17
|
+
type ThemeName =
|
|
18
|
+
| 'swag'
|
|
19
|
+
| 'swagDark'
|
|
20
|
+
| 'swagLight'
|
|
21
|
+
| 'work'
|
|
22
|
+
| 'jobs'
|
|
23
|
+
| 'wallet'
|
|
24
|
+
| 'eBens'
|
|
25
|
+
| 'ehWorkDark'
|
|
26
|
+
| 'ehWork'
|
|
27
|
+
| 'ehJobs';
|
|
28
|
+
|
|
29
|
+
const WorkThemeSwitcher = ({
|
|
30
|
+
name = 'work',
|
|
31
|
+
children,
|
|
32
|
+
}: {
|
|
33
|
+
name?: ThemeName;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
}) => {
|
|
36
|
+
const theme = useMemo(() => {
|
|
37
|
+
switch (name) {
|
|
38
|
+
case 'swag':
|
|
39
|
+
return getWorkTheme(undefined, swagSystemPalette);
|
|
40
|
+
case 'work':
|
|
41
|
+
return getWorkTheme(undefined, workSystemPalette);
|
|
42
|
+
case 'jobs':
|
|
43
|
+
return getWorkTheme(undefined, jobsSystemPalette);
|
|
44
|
+
case 'wallet':
|
|
45
|
+
return getWorkTheme(undefined, walletSystemPalette);
|
|
46
|
+
case 'eBens':
|
|
47
|
+
return getWorkTheme(undefined, eBensSystemPalette);
|
|
48
|
+
case 'swagDark':
|
|
49
|
+
return getWorkTheme(undefined, swagDarkSystemPalette);
|
|
50
|
+
case 'swagLight':
|
|
51
|
+
case 'ehWork':
|
|
52
|
+
return getWorkTheme(undefined, swagLightSystemPalette);
|
|
53
|
+
case 'ehWorkDark':
|
|
54
|
+
return getWorkTheme(undefined, ehWorkDarkSystemPalette);
|
|
55
|
+
case 'ehJobs':
|
|
56
|
+
return getWorkTheme(undefined, swagLightJobsSystemPalette);
|
|
57
|
+
}
|
|
58
|
+
}, [name]);
|
|
59
|
+
|
|
60
|
+
return <WorkThemeProvider theme={theme}>{children}</WorkThemeProvider>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const withWorkTheme =
|
|
64
|
+
<P extends Record<string, unknown>>(
|
|
65
|
+
C: React.ComponentType<P>,
|
|
66
|
+
themeName: ThemeName
|
|
67
|
+
) =>
|
|
68
|
+
(props: PropsWithChildren<P>) => {
|
|
69
|
+
return (
|
|
70
|
+
<WorkThemeSwitcher name={themeName}>
|
|
71
|
+
<C {...props} />
|
|
72
|
+
</WorkThemeSwitcher>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default WorkThemeSwitcher;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { WorkThemeProvider, useWorkTheme } from '../ThemeProvider';
|
|
5
|
+
import workTheme from '..';
|
|
6
|
+
|
|
7
|
+
const TestComponent = () => {
|
|
8
|
+
const theme = useWorkTheme();
|
|
9
|
+
return <Text testID="test-text">Theme loaded: {theme ? 'Yes' : 'No'}</Text>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('WorkThemeProvider', () => {
|
|
13
|
+
it('should provide theme to children', () => {
|
|
14
|
+
const { getByTestId } = render(
|
|
15
|
+
<WorkThemeProvider theme={workTheme}>
|
|
16
|
+
<TestComponent />
|
|
17
|
+
</WorkThemeProvider>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(getByTestId('test-text')).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should allow useWorkTheme hook to access theme', () => {
|
|
24
|
+
const { getByText } = render(
|
|
25
|
+
<WorkThemeProvider theme={workTheme}>
|
|
26
|
+
<TestComponent />
|
|
27
|
+
</WorkThemeProvider>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(getByText('Theme loaded: Yes')).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
});
|