@aws-amplify/ui-react-native 1.2.20 → 1.2.21

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 (88) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/Authenticator/Authenticator.d.ts +101 -148
  3. package/dist/Authenticator/Authenticator.js +2 -3
  4. package/dist/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.d.ts +13 -2
  5. package/dist/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.js +4 -3
  6. package/dist/Authenticator/Defaults/ConfirmSignIn/ConfirmSignIn.d.ts +13 -2
  7. package/dist/Authenticator/Defaults/ConfirmSignIn/ConfirmSignIn.js +4 -3
  8. package/dist/Authenticator/Defaults/ConfirmSignUp/ConfirmSignUp.d.ts +13 -2
  9. package/dist/Authenticator/Defaults/ConfirmSignUp/ConfirmSignUp.js +4 -3
  10. package/dist/Authenticator/Defaults/ConfirmVerifyUser/ConfirmVerifyUser.d.ts +13 -2
  11. package/dist/Authenticator/Defaults/ConfirmVerifyUser/ConfirmVerifyUser.js +4 -3
  12. package/dist/Authenticator/Defaults/ForceNewPassword/ForceNewPassword.d.ts +13 -2
  13. package/dist/Authenticator/Defaults/ForceNewPassword/ForceNewPassword.js +4 -3
  14. package/dist/Authenticator/Defaults/ResetPassword/ResetPassword.d.ts +13 -2
  15. package/dist/Authenticator/Defaults/ResetPassword/ResetPassword.js +4 -3
  16. package/dist/Authenticator/Defaults/SetupTOTP/SetupTOTP.d.ts +13 -2
  17. package/dist/Authenticator/Defaults/SetupTOTP/SetupTOTP.js +4 -3
  18. package/dist/Authenticator/Defaults/SignIn/SignIn.d.ts +13 -2
  19. package/dist/Authenticator/Defaults/SignIn/SignIn.js +4 -3
  20. package/dist/Authenticator/Defaults/SignUp/SignUp.d.ts +13 -2
  21. package/dist/Authenticator/Defaults/SignUp/SignUp.js +4 -3
  22. package/dist/Authenticator/Defaults/VerifyUser/VerifyUser.d.ts +13 -2
  23. package/dist/Authenticator/Defaults/VerifyUser/VerifyUser.js +4 -3
  24. package/dist/Authenticator/Defaults/types.d.ts +21 -20
  25. package/dist/Authenticator/common/DefaultContent/types.d.ts +1 -1
  26. package/dist/Authenticator/common/DefaultFormFields/DefaultRadioFormFields.d.ts +7 -3
  27. package/dist/Authenticator/common/DefaultFormFields/DefaultRadioFormFields.js +4 -3
  28. package/dist/Authenticator/common/DefaultFormFields/DefaultTextFormFields.d.ts +6 -2
  29. package/dist/Authenticator/common/DefaultFormFields/DefaultTextFormFields.js +3 -3
  30. package/dist/Authenticator/common/DefaultFormFields/types.d.ts +12 -3
  31. package/dist/Authenticator/hooks/types.d.ts +3 -2
  32. package/dist/Authenticator/hooks/useFieldValues/types.d.ts +4 -1
  33. package/dist/Authenticator/hooks/useFieldValues/useFieldValues.d.ts +1 -1
  34. package/dist/Authenticator/hooks/useFieldValues/useFieldValues.js +21 -3
  35. package/dist/Authenticator/hooks/useFieldValues/utils.d.ts +10 -1
  36. package/dist/Authenticator/hooks/useFieldValues/utils.js +32 -2
  37. package/dist/primitives/TextField/TextField.js +2 -1
  38. package/dist/primitives/TextField/styles.js +3 -0
  39. package/dist/primitives/TextField/types.d.ts +1 -0
  40. package/dist/theme/createTheme.js +24 -18
  41. package/dist/theme/types.d.ts +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/jest.config.js +1 -0
  45. package/package.json +5 -5
  46. package/src/Authenticator/Authenticator.tsx +2 -6
  47. package/src/Authenticator/Defaults/ConfirmResetPassword/ConfirmResetPassword.tsx +7 -3
  48. package/src/Authenticator/Defaults/ConfirmSignIn/ConfirmSignIn.tsx +7 -3
  49. package/src/Authenticator/Defaults/ConfirmSignIn/__tests__/ConfirmSignIn.spec.tsx +1 -0
  50. package/src/Authenticator/Defaults/ConfirmSignUp/ConfirmSignUp.tsx +7 -3
  51. package/src/Authenticator/Defaults/ConfirmSignUp/__tests__/ConfirmSignUp.spec.tsx +1 -0
  52. package/src/Authenticator/Defaults/ConfirmVerifyUser/ConfirmVerifyUser.tsx +7 -3
  53. package/src/Authenticator/Defaults/ConfirmVerifyUser/__tests__/ConfirmVerifyUser.spec.tsx +1 -0
  54. package/src/Authenticator/Defaults/ForceNewPassword/ForceNewPassword.tsx +7 -3
  55. package/src/Authenticator/Defaults/ForceNewPassword/__tests__/__snapshots__/ForceNewPassword.spec.tsx.snap +1 -1
  56. package/src/Authenticator/Defaults/ResetPassword/ResetPassword.tsx +7 -3
  57. package/src/Authenticator/Defaults/ResetPassword/__tests__/ResetPassword.spec.tsx +1 -0
  58. package/src/Authenticator/Defaults/SetupTOTP/SetupTOTP.tsx +7 -3
  59. package/src/Authenticator/Defaults/SetupTOTP/__tests__/SetupTOTP.spec.tsx +1 -0
  60. package/src/Authenticator/Defaults/SignIn/SignIn.tsx +7 -3
  61. package/src/Authenticator/Defaults/SignIn/__tests__/SignIn.spec.tsx +1 -0
  62. package/src/Authenticator/Defaults/SignUp/SignUp.tsx +7 -3
  63. package/src/Authenticator/Defaults/SignUp/__tests__/__snapshots__/SignUp.spec.tsx.snap +1 -1
  64. package/src/Authenticator/Defaults/VerifyUser/VerifyUser.tsx +7 -3
  65. package/src/Authenticator/Defaults/VerifyUser/__tests__/VerifyUser.spec.tsx +1 -0
  66. package/src/Authenticator/Defaults/types.ts +63 -49
  67. package/src/Authenticator/__tests__/Authenticator.spec.tsx +16 -19
  68. package/src/Authenticator/__tests__/__snapshots__/Authenticator.spec.tsx.snap +1 -9
  69. package/src/Authenticator/__tests__/withAuthenticator.spec.tsx +1 -1
  70. package/src/Authenticator/common/DefaultContent/types.ts +1 -4
  71. package/src/Authenticator/common/DefaultFormFields/DefaultRadioFormFields.tsx +8 -6
  72. package/src/Authenticator/common/DefaultFormFields/DefaultTextFormFields.tsx +10 -7
  73. package/src/Authenticator/common/DefaultFormFields/types.ts +15 -5
  74. package/src/Authenticator/hooks/types.ts +3 -0
  75. package/src/Authenticator/hooks/useFieldValues/__tests__/useFieldValues.spec.ts +75 -2
  76. package/src/Authenticator/hooks/useFieldValues/__tests__/utils.spec.ts +67 -1
  77. package/src/Authenticator/hooks/useFieldValues/types.ts +5 -0
  78. package/src/Authenticator/hooks/useFieldValues/useFieldValues.ts +26 -1
  79. package/src/Authenticator/hooks/useFieldValues/utils.ts +44 -1
  80. package/src/primitives/TextField/TextField.tsx +2 -1
  81. package/src/primitives/TextField/__tests__/TextField.spec.tsx +57 -8
  82. package/src/primitives/TextField/__tests__/__snapshots__/TextField.spec.tsx.snap +31 -31
  83. package/src/primitives/TextField/styles.ts +3 -0
  84. package/src/primitives/TextField/types.ts +1 -0
  85. package/src/theme/__tests__/createTheme.spec.ts +48 -0
  86. package/src/theme/createTheme.ts +44 -21
  87. package/src/theme/types.ts +17 -16
  88. package/src/version.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import { useMemo, useState } from 'react';
2
2
  import { Logger } from 'aws-amplify';
3
+ import { ValidationError } from '@aws-amplify/ui';
3
4
 
4
5
  import { OnChangeText, TextFieldOnBlur, TypedField } from '../types';
5
6
 
@@ -8,6 +9,7 @@ import {
8
9
  getSanitizedTextFields,
9
10
  getSanitizedRadioFields,
10
11
  isRadioFieldOptions,
12
+ runFieldValidation,
11
13
  } from './utils';
12
14
 
13
15
  const logger = new Logger('Authenticator');
@@ -18,8 +20,12 @@ export default function useFieldValues<FieldType extends TypedField>({
18
20
  handleBlur,
19
21
  handleChange,
20
22
  handleSubmit,
23
+ validationErrors,
21
24
  }: UseFieldValuesParams<FieldType>): UseFieldValues<FieldType> {
22
25
  const [values, setValues] = useState<Record<string, string>>({});
26
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
27
+ const [fieldValidationErrors, setFieldValidationErrors] =
28
+ useState<ValidationError>({});
23
29
  const isRadioFieldComponent = componentName === 'VerifyUser';
24
30
 
25
31
  const sanitizedFields = useMemo(() => {
@@ -53,11 +59,18 @@ export default function useFieldValues<FieldType extends TypedField>({
53
59
  const { name, label, labelHidden, ...rest } = field;
54
60
 
55
61
  const onBlur: TextFieldOnBlur = (event) => {
62
+ setTouched({ ...touched, [name]: true });
63
+
56
64
  // call `onBlur` passed as text `field` option
57
65
  field.onBlur?.(event);
58
66
 
59
67
  // call machine blur handler
60
68
  handleBlur({ name, value: values[name] });
69
+
70
+ setFieldValidationErrors({
71
+ ...fieldValidationErrors,
72
+ [name]: runFieldValidation(field, values[name], validationErrors),
73
+ });
61
74
  };
62
75
 
63
76
  const onChangeText: OnChangeText = (value) => {
@@ -67,6 +80,13 @@ export default function useFieldValues<FieldType extends TypedField>({
67
80
  // call machine change handler
68
81
  handleChange({ name, value });
69
82
 
83
+ if (touched[name]) {
84
+ setFieldValidationErrors({
85
+ ...fieldValidationErrors,
86
+ [name]: runFieldValidation(field, value, validationErrors),
87
+ });
88
+ }
89
+
70
90
  setValues({ ...values, [name]: value });
71
91
  };
72
92
 
@@ -112,5 +132,10 @@ export default function useFieldValues<FieldType extends TypedField>({
112
132
  handleSubmit?.(submitValue);
113
133
  };
114
134
 
115
- return { fields: fieldsWithHandlers, disableFormSubmit, handleFormSubmit };
135
+ return {
136
+ fields: fieldsWithHandlers,
137
+ disableFormSubmit,
138
+ fieldValidationErrors: { ...fieldValidationErrors, ...validationErrors },
139
+ handleFormSubmit,
140
+ };
116
141
  }
@@ -1,7 +1,11 @@
1
1
  import { Logger } from 'aws-amplify';
2
2
  import {
3
+ authenticatorTextUtil,
4
+ isString,
3
5
  isUnverifiedContactMethodType,
6
+ isValidEmail,
4
7
  UnverifiedContactMethodType,
8
+ ValidationError,
5
9
  } from '@aws-amplify/ui';
6
10
  import {
7
11
  AuthenticatorLegacyField,
@@ -14,12 +18,15 @@ import {
14
18
  AuthenticatorFieldTypeKey,
15
19
  MachineFieldTypeKey,
16
20
  RadioFieldOptions,
21
+ TextFieldOptionsType,
17
22
  TypedField,
18
23
  } from '../types';
19
24
  import { KEY_ALLOW_LIST } from './constants';
20
25
 
21
26
  const logger = new Logger('Authenticator');
22
27
 
28
+ const { getInvalidEmailText, getRequiredFieldText } = authenticatorTextUtil;
29
+
23
30
  export const isRadioFieldOptions = (
24
31
  field: TypedField
25
32
  ): field is RadioFieldOptions => field?.type === 'radio';
@@ -109,7 +116,8 @@ const isKeyAllowed = (key: string) =>
109
116
 
110
117
  const isValidMachineFieldType = (
111
118
  type: string | undefined
112
- ): type is MachineFieldTypeKey => type === 'password' || type === 'tel';
119
+ ): type is MachineFieldTypeKey =>
120
+ type === 'password' || type === 'tel' || type == 'email';
113
121
 
114
122
  const getFieldType = (type: string | undefined): AuthenticatorFieldTypeKey => {
115
123
  if (isValidMachineFieldType(type)) {
@@ -176,3 +184,38 @@ export function getRouteTypedFields({
176
184
 
177
185
  return isVerifyUserRoute ? radioFields : getTypedFields(fields);
178
186
  }
187
+
188
+ /**
189
+ *
190
+ * @param {TextFieldOptionsType} field text field type
191
+ * @param {string | undefined} value text field value
192
+ * @param {string[]} stateValidations validation errors array from state machine
193
+ * @returns {string[]} field errors array
194
+ */
195
+ export const runFieldValidation = (
196
+ field: TextFieldOptionsType,
197
+ value: string | undefined,
198
+ stateValidations: ValidationError | undefined
199
+ ): string[] => {
200
+ const fieldErrors: string[] = [];
201
+ if (field.required && !value) {
202
+ fieldErrors.push(getRequiredFieldText());
203
+ }
204
+ if (field.type === 'email') {
205
+ if (!isValidEmail(value)) {
206
+ fieldErrors.push(getInvalidEmailText());
207
+ }
208
+ }
209
+
210
+ // add state machine validation errors, if any
211
+ const stateFieldValidation = stateValidations?.[field.name];
212
+ if (stateFieldValidation) {
213
+ if (isString(stateFieldValidation)) {
214
+ fieldErrors.push(stateFieldValidation);
215
+ } else {
216
+ return fieldErrors.concat(stateFieldValidation);
217
+ }
218
+ }
219
+
220
+ return fieldErrors;
221
+ };
@@ -33,8 +33,9 @@ export default function TextField({
33
33
  () => ({
34
34
  ...themedStyle.fieldContainer,
35
35
  ...(disabled && themedStyle.disabled),
36
+ ...(error && themedStyle.error),
36
37
  }),
37
- [disabled, themedStyle]
38
+ [disabled, error, themedStyle]
38
39
  );
39
40
 
40
41
  return (
@@ -67,7 +67,7 @@ describe('TextField', () => {
67
67
  expect(queryByText(message)).toBeNull();
68
68
  });
69
69
 
70
- it(`doesn't render the errorMessage if errorMessage prop is undefined`, () => {
70
+ it(`shows error style, but doesn't render the errorMessage if errorMessage prop is undefined`, () => {
71
71
  const message = 'Error message';
72
72
  const { toJSON, queryByText } = render(
73
73
  <TextField {...defaultProps} error />
@@ -111,18 +111,13 @@ describe('TextField', () => {
111
111
  });
112
112
 
113
113
  it('applies theme and style props', () => {
114
- const errorMessageText = 'Error!';
115
- const customErrorMessageStyle = { color: 'red' };
116
114
  const customFieldStyle = { color: 'orange' };
117
115
  const customLabelStyle = { color: 'blue' };
118
116
  const customStyle = { backgroundColor: 'purple' };
119
117
 
120
- const { getByTestId, getByText } = render(
118
+ const { getByTestId } = render(
121
119
  <TextField
122
120
  {...defaultProps}
123
- error
124
- errorMessage={errorMessageText}
125
- errorMessageStyle={customErrorMessageStyle}
126
121
  fieldStyle={customFieldStyle}
127
122
  labelStyle={customLabelStyle}
128
123
  style={customStyle}
@@ -135,7 +130,6 @@ describe('TextField', () => {
135
130
  const container = getByTestId(TEXTFIELD_CONTAINER_TEST_ID);
136
131
  const inputContainer = getByTestId(INPUT_CONTAINER_TEST_ID);
137
132
  const input = getByTestId(testID);
138
- const errorMessage = getByText(errorMessageText);
139
133
 
140
134
  expect(container.props.style).toStrictEqual([
141
135
  themedStyle.container,
@@ -148,6 +142,61 @@ describe('TextField', () => {
148
142
  themedStyle.field,
149
143
  customFieldStyle,
150
144
  ]);
145
+ });
146
+
147
+ it('applies theme and style props with error', () => {
148
+ const errorMessageText = 'Error!';
149
+ const customErrorMessageStyle = { color: 'red' };
150
+ const customStyle = { backgroundColor: 'purple' };
151
+
152
+ const { getByTestId, getByText } = render(
153
+ <TextField
154
+ {...defaultProps}
155
+ style={customStyle}
156
+ error
157
+ errorMessage={errorMessageText}
158
+ errorMessageStyle={customErrorMessageStyle}
159
+ />
160
+ );
161
+
162
+ const { result } = renderHook(() => useTheme());
163
+ const themedStyle = getThemedStyles(result.current);
164
+
165
+ const container = getByTestId(TEXTFIELD_CONTAINER_TEST_ID);
166
+ const inputContainer = getByTestId(INPUT_CONTAINER_TEST_ID);
167
+ const errorMessage = getByText(errorMessageText);
168
+
169
+ expect(container.props.style).toStrictEqual([
170
+ themedStyle.container,
171
+ customStyle,
172
+ ]);
173
+ expect(inputContainer.props.style).toStrictEqual({
174
+ ...themedStyle.fieldContainer,
175
+ ...themedStyle.error,
176
+ });
151
177
  expect(errorMessage.props.style).toContain(customErrorMessageStyle);
152
178
  });
179
+
180
+ it('applies theme and style props for disabled', () => {
181
+ const customStyle = { backgroundColor: 'purple' };
182
+
183
+ const { getByTestId } = render(
184
+ <TextField {...defaultProps} style={customStyle} disabled />
185
+ );
186
+
187
+ const { result } = renderHook(() => useTheme());
188
+ const themedStyle = getThemedStyles(result.current);
189
+
190
+ const container = getByTestId(TEXTFIELD_CONTAINER_TEST_ID);
191
+ const inputContainer = getByTestId(INPUT_CONTAINER_TEST_ID);
192
+
193
+ expect(container.props.style).toStrictEqual([
194
+ themedStyle.container,
195
+ customStyle,
196
+ ]);
197
+ expect(inputContainer.props.style).toStrictEqual({
198
+ ...themedStyle.fieldContainer,
199
+ ...themedStyle.disabled,
200
+ });
201
+ });
153
202
  });
@@ -80,7 +80,7 @@ exports[`TextField doesn't render the errorMessage if error prop is false 1`] =
80
80
  </View>
81
81
  `;
82
82
 
83
- exports[`TextField doesn't render the errorMessage if errorMessage prop is undefined 1`] = `
83
+ exports[`TextField renders as expected 1`] = `
84
84
  <View
85
85
  style={
86
86
  Array [
@@ -160,7 +160,7 @@ exports[`TextField doesn't render the errorMessage if errorMessage prop is undef
160
160
  </View>
161
161
  `;
162
162
 
163
- exports[`TextField renders as expected 1`] = `
163
+ exports[`TextField renders as expected as password field 1`] = `
164
164
  <View
165
165
  style={
166
166
  Array [
@@ -223,6 +223,7 @@ exports[`TextField renders as expected 1`] = `
223
223
  editable={true}
224
224
  placeholder="Placeholder"
225
225
  placeholderTextColor="hsl(210, 10%, 40%)"
226
+ secureTextEntry={true}
226
227
  style={
227
228
  Array [
228
229
  Object {
@@ -240,7 +241,7 @@ exports[`TextField renders as expected 1`] = `
240
241
  </View>
241
242
  `;
242
243
 
243
- exports[`TextField renders as expected as password field 1`] = `
244
+ exports[`TextField renders as expected when disabled 1`] = `
244
245
  <View
245
246
  style={
246
247
  Array [
@@ -287,6 +288,7 @@ exports[`TextField renders as expected as password field 1`] = `
287
288
  "borderRadius": 4,
288
289
  "borderWidth": 1,
289
290
  "flexDirection": "row",
291
+ "opacity": 0.6,
290
292
  "paddingHorizontal": 8,
291
293
  }
292
294
  }
@@ -295,15 +297,14 @@ exports[`TextField renders as expected as password field 1`] = `
295
297
  <TextInput
296
298
  accessibilityState={
297
299
  Object {
298
- "disabled": undefined,
300
+ "disabled": true,
299
301
  }
300
302
  }
301
303
  accessible={true}
302
304
  autoCapitalize="none"
303
- editable={true}
305
+ editable={false}
304
306
  placeholder="Placeholder"
305
307
  placeholderTextColor="hsl(210, 10%, 40%)"
306
- secureTextEntry={true}
307
308
  style={
308
309
  Array [
309
310
  Object {
@@ -321,7 +322,7 @@ exports[`TextField renders as expected as password field 1`] = `
321
322
  </View>
322
323
  `;
323
324
 
324
- exports[`TextField renders as expected when disabled 1`] = `
325
+ exports[`TextField renders as expected with error message 1`] = `
325
326
  <View
326
327
  style={
327
328
  Array [
@@ -364,11 +365,10 @@ exports[`TextField renders as expected when disabled 1`] = `
364
365
  style={
365
366
  Object {
366
367
  "alignItems": "center",
367
- "borderColor": "hsl(210, 10%, 58%)",
368
+ "borderColor": "hsl(0, 95%, 30%)",
368
369
  "borderRadius": 4,
369
370
  "borderWidth": 1,
370
371
  "flexDirection": "row",
371
- "opacity": 0.6,
372
372
  "paddingHorizontal": 8,
373
373
  }
374
374
  }
@@ -377,12 +377,12 @@ exports[`TextField renders as expected when disabled 1`] = `
377
377
  <TextInput
378
378
  accessibilityState={
379
379
  Object {
380
- "disabled": true,
380
+ "disabled": undefined,
381
381
  }
382
382
  }
383
383
  accessible={true}
384
384
  autoCapitalize="none"
385
- editable={false}
385
+ editable={true}
386
386
  placeholder="Placeholder"
387
387
  placeholderTextColor="hsl(210, 10%, 40%)"
388
388
  style={
@@ -399,10 +399,28 @@ exports[`TextField renders as expected when disabled 1`] = `
399
399
  testID="textInput"
400
400
  />
401
401
  </View>
402
+ <Text
403
+ accessibilityRole="text"
404
+ style={
405
+ Array [
406
+ Object {
407
+ "fontSize": 14,
408
+ "fontWeight": "400",
409
+ "lineHeight": 21,
410
+ },
411
+ Object {
412
+ "color": "hsl(210, 50%, 10%)",
413
+ },
414
+ undefined,
415
+ ]
416
+ }
417
+ >
418
+ Error message
419
+ </Text>
402
420
  </View>
403
421
  `;
404
422
 
405
- exports[`TextField renders as expected with error message 1`] = `
423
+ exports[`TextField shows error style, but doesn't render the errorMessage if errorMessage prop is undefined 1`] = `
406
424
  <View
407
425
  style={
408
426
  Array [
@@ -445,7 +463,7 @@ exports[`TextField renders as expected with error message 1`] = `
445
463
  style={
446
464
  Object {
447
465
  "alignItems": "center",
448
- "borderColor": "hsl(210, 10%, 58%)",
466
+ "borderColor": "hsl(0, 95%, 30%)",
449
467
  "borderRadius": 4,
450
468
  "borderWidth": 1,
451
469
  "flexDirection": "row",
@@ -479,23 +497,5 @@ exports[`TextField renders as expected with error message 1`] = `
479
497
  testID="textInput"
480
498
  />
481
499
  </View>
482
- <Text
483
- accessibilityRole="text"
484
- style={
485
- Array [
486
- Object {
487
- "fontSize": 14,
488
- "fontWeight": "400",
489
- "lineHeight": 21,
490
- },
491
- Object {
492
- "color": "hsl(210, 50%, 10%)",
493
- },
494
- undefined,
495
- ]
496
- }
497
- >
498
- Error message
499
- </Text>
500
500
  </View>
501
501
  `;
@@ -19,6 +19,9 @@ export const getThemedStyles = (theme: StrictTheme): TextFieldStyles => {
19
19
  opacity: opacities[60],
20
20
  ...components?.textField?.disabled,
21
21
  },
22
+ error: {
23
+ borderColor: colors.border.error,
24
+ },
22
25
  fieldContainer: {
23
26
  alignItems: 'center',
24
27
  borderColor: colors.border.primary,
@@ -60,6 +60,7 @@ export interface TextFieldProps extends Omit<TextInputProps, 'editable'> {
60
60
  export interface TextFieldStyles {
61
61
  container?: ViewStyle;
62
62
  disabled?: ViewStyle;
63
+ error?: ViewStyle;
63
64
  field?: TextStyle;
64
65
  fieldContainer?: ViewStyle;
65
66
  label?: TextStyle;
@@ -21,6 +21,54 @@ describe('createTheme', () => {
21
21
  });
22
22
  });
23
23
 
24
+ describe('number conversions', () => {
25
+ it('should convert strings to numbers where applicable', () => {
26
+ const { tokens } = createTheme({
27
+ tokens: {
28
+ borderWidths: {
29
+ small: '4',
30
+ medium: '1rem',
31
+ large: 6,
32
+ },
33
+ opacities: {
34
+ '10': '0.2',
35
+ },
36
+ space: {
37
+ small: 4,
38
+ medium: '6',
39
+ large: '{space.small.value}',
40
+ },
41
+ fontSizes: {
42
+ small: '1rem',
43
+ },
44
+ },
45
+ });
46
+ expect(tokens.borderWidths.small).toBe(4);
47
+ expect(tokens.borderWidths.medium).toBe(16);
48
+ expect(tokens.borderWidths.large).toBe(6);
49
+ expect(tokens.opacities['10']).toBe(0.2);
50
+ expect(tokens.space.small).toBe(4);
51
+ expect(tokens.space.medium).toBe(6);
52
+ expect(tokens.space.large).toBe(4);
53
+ expect(tokens.fontSizes.small).toBe(16);
54
+ });
55
+
56
+ it('should use the spaceModifier for space tokens with rem', () => {
57
+ const { tokens } = createTheme({
58
+ spaceModifier: 1.25,
59
+ tokens: {
60
+ space: {
61
+ small: 4,
62
+ medium: '1rem',
63
+ },
64
+ },
65
+ });
66
+
67
+ expect(tokens.space.small).toEqual(4);
68
+ expect(tokens.space.medium).toEqual(20);
69
+ });
70
+ });
71
+
24
72
  describe('with mixture of value and no value', () => {
25
73
  const { tokens } = createTheme({
26
74
  tokens: {
@@ -2,21 +2,33 @@ import deepExtend from 'style-dictionary/lib/utils/deepExtend';
2
2
  import resolveObject from 'style-dictionary/lib/utils/resolveObject';
3
3
  import usesReference from 'style-dictionary/lib/utils/references/usesReference';
4
4
  import { isFunction, setupTokens } from '@aws-amplify/ui';
5
- import { Theme, StrictTheme, ColorMode, Components } from './types';
5
+ import {
6
+ Theme,
7
+ StrictTheme,
8
+ ColorMode,
9
+ Components,
10
+ StrictTokens,
11
+ } from './types';
6
12
  import { defaultTheme } from './defaultTheme';
7
13
 
8
14
  // This will resolve all references in component themes by either
9
15
  // calling the component theme function with the already resolved base tokens
10
16
  // OR
11
17
  // resolving the component theme object
12
- const setupComponents = ({ components, tokens }: StrictTheme) => {
18
+ const setupComponents = ({
19
+ components,
20
+ tokens,
21
+ }: {
22
+ components: Components;
23
+ tokens: StrictTokens;
24
+ }) => {
13
25
  const output = components
14
26
  ? Object.entries(components).reduce(
15
27
  (acc, [key, value]) => ({
16
28
  ...acc,
17
29
  [key]: isFunction(value) ? (value(tokens) as typeof value) : value,
18
30
  }),
19
- {} as Components<'components'>
31
+ {}
20
32
  )
21
33
  : {};
22
34
 
@@ -26,6 +38,16 @@ const setupComponents = ({ components, tokens }: StrictTheme) => {
26
38
  }).components;
27
39
  };
28
40
 
41
+ const shouldParseFloatValue = (pathKey: string) =>
42
+ [
43
+ 'space',
44
+ 'borderWidths',
45
+ 'opacities',
46
+ 'fontSizes',
47
+ 'lineHeights',
48
+ 'radii',
49
+ ].includes(pathKey);
50
+
29
51
  const setupToken = ({
30
52
  token,
31
53
  path = [],
@@ -39,27 +61,28 @@ const setupToken = ({
39
61
  }): string | number => {
40
62
  const { value } = token;
41
63
  if (typeof value === 'string') {
42
- // Perform transforms
43
- if (path[0] === 'space') {
44
- if (value.includes('rem')) {
45
- return Math.floor(parseFloat(value) * 16 * spaceModifier);
46
- }
47
- }
48
- if (value.includes('rem')) {
49
- return Math.floor(parseFloat(value) * 16);
50
- }
51
- if (value.includes('px')) {
52
- return parseInt(value, 10);
53
- }
54
- if (path[0] === 'opacities') {
55
- return parseFloat(value);
56
- }
57
64
  // Remove .value from references if there is a reference
65
+ // this needs to come first so we don't get NaNs for references
58
66
  if (usesReference(value)) {
59
67
  return value.replace('.value', '');
60
68
  }
69
+
70
+ if (shouldParseFloatValue(path[0])) {
71
+ if (value.includes('rem')) {
72
+ if (path[0] === 'space') {
73
+ return Math.floor(parseFloat(value) * 16 * spaceModifier);
74
+ }
75
+ return Math.floor(parseFloat(value) * 16);
76
+ }
77
+ if (value.includes('px')) {
78
+ return parseInt(value, 10);
79
+ }
80
+ return parseFloat(value);
81
+ }
82
+
61
83
  return value;
62
84
  }
85
+
63
86
  // Font Weights in RN are strings
64
87
  if (path[0] === 'fontWeights') {
65
88
  return `${value}`;
@@ -118,14 +141,14 @@ export const createTheme = (
118
141
  }) as StrictTheme['tokens']
119
142
  );
120
143
 
121
- let { components } = mergedTheme;
144
+ let components;
122
145
 
123
146
  // Resolve component token references too
124
147
  if (mergedTheme.components) {
125
148
  components = setupComponents({
126
- ...mergedTheme,
149
+ components: mergedTheme.components,
127
150
  tokens,
128
- }) as Components<'output'>;
151
+ });
129
152
  }
130
153
 
131
154
  return { ...mergedTheme, tokens, components };
@@ -26,22 +26,23 @@ type ComponentTheme<ComponentType, Output> = Output extends 'output'
26
26
  : ((tokens: StrictTheme['tokens']) => ComponentType) | ComponentType;
27
27
 
28
28
  // TODO: make optional all the way down
29
- export type Components<Output> = Record<string, object> & {
30
- button?: ComponentTheme<ButtonStyles, Output>;
31
- checkbox?: ComponentTheme<CheckboxStyles, Output>;
32
- divider?: ComponentTheme<DividerStyles, Output>;
33
- errorMessage?: ComponentTheme<ErrorMessageStyles, Output>;
34
- heading?: ComponentTheme<HeadingStyles, Output>;
35
- icon?: ComponentTheme<IconStyles, Output>;
36
- iconButton?: ComponentTheme<IconButtonStyles, Output>;
37
- label?: ComponentTheme<LabelStyles, Output>;
38
- passwordField?: ComponentTheme<PasswordFieldStyles, Output>;
39
- phoneNumberField?: ComponentTheme<PhoneNumberFieldStyles, Output>;
40
- radio?: ComponentTheme<RadioStyles, Output>;
41
- radioGroup?: ComponentTheme<RadioGroupStyles, Output>;
42
- tabs?: ComponentTheme<TabsStyles, Output>;
43
- textField?: ComponentTheme<TextFieldStyles, Output>;
44
- };
29
+ export type Components<Output extends 'input' | 'output' | unknown = unknown> =
30
+ {
31
+ button?: ComponentTheme<ButtonStyles, Output>;
32
+ checkbox?: ComponentTheme<CheckboxStyles, Output>;
33
+ divider?: ComponentTheme<DividerStyles, Output>;
34
+ errorMessage?: ComponentTheme<ErrorMessageStyles, Output>;
35
+ heading?: ComponentTheme<HeadingStyles, Output>;
36
+ icon?: ComponentTheme<IconStyles, Output>;
37
+ iconButton?: ComponentTheme<IconButtonStyles, Output>;
38
+ label?: ComponentTheme<LabelStyles, Output>;
39
+ passwordField?: ComponentTheme<PasswordFieldStyles, Output>;
40
+ phoneNumberField?: ComponentTheme<PhoneNumberFieldStyles, Output>;
41
+ radio?: ComponentTheme<RadioStyles, Output>;
42
+ radioGroup?: ComponentTheme<RadioGroupStyles, Output>;
43
+ tabs?: ComponentTheme<TabsStyles, Output>;
44
+ textField?: ComponentTheme<TextFieldStyles, Output>;
45
+ };
45
46
 
46
47
  export type ColorMode = 'light' | 'dark' | 'system' | null;
47
48
  export type Override = Omit<Theme, 'overrides'> & {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.2.20';
1
+ export const VERSION = '1.2.21';