@telus-uds/components-base 2.2.0 → 2.4.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 (46) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/lib/Autocomplete/Autocomplete.js +8 -3
  3. package/lib/Card/CardBase.js +7 -1
  4. package/lib/Card/PressableCardBase.js +1 -1
  5. package/lib/FileUpload/FileUpload.js +57 -22
  6. package/lib/FileUpload/dictionary.js +6 -2
  7. package/lib/InputLabel/InputLabel.js +36 -2
  8. package/lib/InputSupports/InputSupports.js +31 -8
  9. package/lib/InputSupports/dictionary.js +12 -0
  10. package/lib/InputSupports/useInputSupports.js +12 -3
  11. package/lib/Link/LinkBase.js +17 -15
  12. package/lib/Listbox/ListboxOverlay.js +6 -3
  13. package/lib/Modal/Modal.js +2 -2
  14. package/lib/MultiSelectFilter/MultiSelectFilter.js +54 -41
  15. package/lib/Notification/Notification.js +7 -3
  16. package/lib/Search/Search.js +39 -8
  17. package/lib/Select/Picker.native.js +8 -4
  18. package/lib/Select/constants.js +4 -2
  19. package/lib/StepTracker/StepTracker.js +10 -3
  20. package/lib/TextInput/TextArea.js +7 -6
  21. package/lib/TextInput/TextInput.js +7 -6
  22. package/lib/TextInput/TextInputBase.js +48 -14
  23. package/lib/utils/props/inputSupportsProps.js +15 -3
  24. package/package.json +3 -3
  25. package/src/Autocomplete/Autocomplete.jsx +9 -2
  26. package/src/Card/CardBase.jsx +5 -2
  27. package/src/Card/PressableCardBase.jsx +12 -1
  28. package/src/FileUpload/FileUpload.jsx +71 -28
  29. package/src/FileUpload/dictionary.js +6 -2
  30. package/src/InputLabel/InputLabel.jsx +39 -2
  31. package/src/InputSupports/InputSupports.jsx +33 -7
  32. package/src/InputSupports/dictionary.js +12 -0
  33. package/src/InputSupports/useInputSupports.js +24 -3
  34. package/src/Link/LinkBase.jsx +17 -15
  35. package/src/Listbox/ListboxOverlay.jsx +5 -3
  36. package/src/Modal/Modal.jsx +1 -1
  37. package/src/MultiSelectFilter/MultiSelectFilter.jsx +55 -27
  38. package/src/Notification/Notification.jsx +9 -3
  39. package/src/Search/Search.jsx +52 -24
  40. package/src/Select/Picker.native.jsx +10 -4
  41. package/src/Select/constants.js +4 -1
  42. package/src/StepTracker/StepTracker.jsx +16 -5
  43. package/src/TextInput/TextArea.jsx +12 -5
  44. package/src/TextInput/TextInput.jsx +13 -6
  45. package/src/TextInput/TextInputBase.jsx +57 -10
  46. package/src/utils/props/inputSupportsProps.js +15 -3
package/CHANGELOG.md CHANGED
@@ -1,12 +1,46 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- <!-- This log was last generated on Fri, 06 Dec 2024 02:03:26 GMT and should not be manually modified. -->
3
+ This log was last generated on Fri, 10 Jan 2025 21:41:25 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 2.4.0
8
+
9
+ Fri, 10 Jan 2025 21:41:25 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `Search`: `showLeftIcon` prop added to display the update icon in the left side for the component. (35577399+JoshHC@users.noreply.github.com)
14
+ - `MultiSelectFilter`: add label and button icon tokens (kristina.kirpichnikova@telus.com)
15
+ - Bump @telus-uds/system-theme-tokens to v3.3.0
16
+
17
+ ### Patches
18
+
19
+ - `Link`: add missing outer border tokens for mobile devices (guillermo.peitzner@telus.com)
20
+ - `Modal`: add fallback to avoid error when pressing Tab (Mauricio.BatresMontejo@telus.com)
21
+ - `Select`: fix Option not appearing in component when selected on iOS largest text sizes (sergio.ramirez@telus.com)
22
+ - `InputLabel`: add character count to support InputText and TextArea (Mauricio.BatresMontejo@telus.com)
23
+ - `Notification`: add system style variant retrocompatibility (guillermo.peitzner@telus.com)
24
+ - `InputLabel`: static styles of the label modified to fix hint and tooltip misalignment (35577399+JoshHC@users.noreply.github.com)
25
+
26
+ ## 2.3.0
27
+
28
+ Thu, 19 Dec 2024 05:02:20 GMT
29
+
30
+ ### Minor changes
31
+
32
+ - `Autocomplete`: Enable `tokens` prop to set custom styles for the `maxHeight` and `scroll` behavior. (jaime.tuyuc@telus.com)
33
+ - `FileUpload`: add minFilesCount and minFileSize props (guillermo.peitzner@telus.com)
34
+ - Bump @telus-uds/system-theme-tokens to v3.2.0
35
+
36
+ ### Patches
37
+
38
+ - `Card`: Include additional `tokenKeys` to remove warnings. (jaime.tuyuc@telus.com)
39
+ - `StepTracker`: elements must only use supported ARIA attributes (sergio.ramirez@telus.com)
40
+
7
41
  ## 2.2.0
8
42
 
9
- Fri, 06 Dec 2024 02:03:26 GMT
43
+ Fri, 06 Dec 2024 02:06:03 GMT
10
44
 
11
45
  ### Minor changes
12
46
 
@@ -6,7 +6,7 @@ import View from "react-native-web/dist/exports/View";
6
6
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
7
7
  import throttle from 'lodash.throttle';
8
8
  import matchAll from 'string.prototype.matchall';
9
- import { inputSupportsProps, selectSystemProps, textInputProps, textInputHandlerProps, useCopy, htmlAttrs, useOverlaidPosition, useSafeLayoutEffect } from '../utils';
9
+ import { inputSupportsProps, selectSystemProps, textInputProps, textInputHandlerProps, useCopy, htmlAttrs, useOverlaidPosition, useSafeLayoutEffect, getTokensPropType } from '../utils';
10
10
  import { useThemeTokens } from '../ThemeProvider';
11
11
  import Listbox from '../Listbox';
12
12
  import Typography from '../Typography';
@@ -105,13 +105,16 @@ const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
105
105
  value,
106
106
  helpText = '',
107
107
  loadingLabel,
108
+ tokens,
108
109
  ...rest
109
110
  } = _ref2;
110
111
  const {
111
- color: resultsTextColor
112
- } = useThemeTokens('Search', {}, {
112
+ color: resultsTextColor,
113
+ ...restOfTokens
114
+ } = useThemeTokens('Autocomplete', tokens, {
113
115
  focus: true
114
116
  });
117
+
115
118
  // The wrapped input is mostly responsible for controlled vs uncontrolled handling,
116
119
  // but we also need to adjust suggestions based on the mode:
117
120
  // - in controlled mode we rely entirely on the suggestions passed via the `items` prop,
@@ -371,6 +374,7 @@ const Autocomplete = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
371
374
  minWidth: fullWidth ? inputWidth : MIN_LISTBOX_WIDTH,
372
375
  maxWidth: inputWidth,
373
376
  onLayout: handleMeasure,
377
+ tokens: restOfTokens,
374
378
  ref: openOverlayRef,
375
379
  children: isLoading ? /*#__PURE__*/_jsx(Loading, {
376
380
  label: loadingLabel ?? getCopy('loading')
@@ -423,6 +427,7 @@ Autocomplete.propTypes = {
423
427
  * </Autocomplete>
424
428
  */
425
429
  children: PropTypes.func,
430
+ tokens: getTokensPropType('Autocomplete'),
426
431
  /**
427
432
  * Copy language identifier
428
433
  */
@@ -25,7 +25,9 @@ const selectStyles = _ref => {
25
25
  minWidth,
26
26
  shadow,
27
27
  backgroundGradient,
28
- gradient
28
+ gradient,
29
+ maxHeight,
30
+ overflowY
29
31
  } = _ref;
30
32
  return {
31
33
  flex,
@@ -47,6 +49,10 @@ const selectStyles = _ref => {
47
49
  } : {}),
48
50
  ...(backgroundGradient && Platform.OS === 'web' ? {
49
51
  backgroundImage: `linear-gradient(${backgroundGradient.angle}deg, ${backgroundGradient.stops[0].color}, ${backgroundGradient.stops[1].color})`
52
+ } : {}),
53
+ ...(Platform.OS === 'web' ? {
54
+ maxHeight,
55
+ overflowY
50
56
  } : {})
51
57
  };
52
58
  };
@@ -11,7 +11,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, foc
11
11
  const tokenKeys = ['flex', 'backgroundColor', 'borderColor', 'gradient', 'borderRadius', 'borderWidth', 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'minWidth', 'shadow', 'contentAlignItems', 'contentJustifyContent', 'contentFlexGrow', 'contentFlexShrink',
12
12
  // Outer border tokens. TODO: centralise common token sets like these as part of
13
13
  // https://github.com/telus/universal-design-system/issues/782
14
- 'outerBorderColor', 'outerBorderWidth', 'outerBorderGap'];
14
+ 'outerBorderColor', 'outerBorderWidth', 'outerBorderGap', 'icon', 'iconBackgroundColor', 'iconColor', 'iconSize', 'inputBackgroundColor', 'inputBorderColor', 'inputBorderRadius', 'inputBorderWidth', 'inputHeight', 'inputShadow', 'inputWidth'];
15
15
  export const selectPressableCardTokens = tokens => Object.fromEntries(tokenKeys.map(key => [key, tokens[key]]));
16
16
 
17
17
  /**
@@ -73,6 +73,8 @@ const selectComponentTokens = (tokens, componentIdentifier) => {
73
73
  * @param {boolean} props.allowMultipleFiles - Whether multiple files can be uploaded.
74
74
  * @param {number} props.maxFileSize - The maximum file size in megabytes.
75
75
  * @param {number} props.maxFilesCount - The maximum number of files that can be uploaded.
76
+ * @param {number} props.minFilesCount - The minimum number of files that can be uploaded.
77
+ * @param {number} props.minFileSize - The minimum file size in megabytes.
76
78
  * @param {Function} props.onUpload - The callback function for file upload.
77
79
  * @param {Function} props.onDelete - The callback function for file deletion.
78
80
  * @param {Object} props.documentPicker - The document picker object.
@@ -88,11 +90,16 @@ const FileUpload = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
88
90
  allowMultipleFiles = false,
89
91
  maxFileSize,
90
92
  maxFilesCount = 1,
93
+ minFilesCount = 1,
94
+ minFileSize = 0,
91
95
  onUpload,
92
96
  onDelete,
93
97
  documentPicker,
94
98
  ...rest
95
99
  } = _ref2;
100
+ if (minFilesCount <= 0) {
101
+ throw new Error('minFilesCount should be greater than 0');
102
+ }
96
103
  const themeTokens = useThemeTokens('FileUpload', tokens, variant);
97
104
  const getCopy = useCopy({
98
105
  dictionary,
@@ -104,9 +111,12 @@ const FileUpload = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
104
111
  React.useEffect(() => {
105
112
  setRenderTrigger(prev => prev + 1);
106
113
  }, [filesStatus]);
107
- const areFileTypesExtensionsValid = files => files.some(file => {
108
- const fileExtension = file.name.split('.').pop().toLowerCase();
109
- if (!fileTypes.includes(fileExtension)) {
114
+ const areFileTypesExtensionsValid = files => {
115
+ const invalidFiles = files.filter(file => {
116
+ const fileExtension = file.name.split('.').pop().toLowerCase();
117
+ return !fileTypes.includes(fileExtension);
118
+ });
119
+ if (invalidFiles.length > 0) {
110
120
  setFilesStatus([{
111
121
  description: getCopy('wrongFileType').replace('%{fileType}', stringifyFileTypesList(fileTypes, getCopy)),
112
122
  style: 'error'
@@ -114,26 +124,46 @@ const FileUpload = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
114
124
  return false;
115
125
  }
116
126
  return true;
117
- });
118
- const areFileSizesWithinLimit = files => files.some(file => {
119
- if (file.size > convertFromMegaByteToByte(maxFileSize)) {
127
+ };
128
+ const areFileSizesWithinLimit = files => {
129
+ const invalidFiles = files.filter(file => {
130
+ const fileSize = file.size;
131
+ return fileSize < convertFromMegaByteToByte(minFileSize) || fileSize > convertFromMegaByteToByte(maxFileSize);
132
+ });
133
+ if (invalidFiles.length > 0) {
134
+ const invalidFile = invalidFiles[0];
135
+ const fileSize = invalidFile.size;
136
+ if (fileSize < convertFromMegaByteToByte(minFileSize)) {
137
+ setFilesStatus([{
138
+ description: getCopy('fileTooSmall').replace('%{size}', minFileSize),
139
+ style: 'error'
140
+ }]);
141
+ } else {
142
+ setFilesStatus([{
143
+ description: getCopy('fileTooBig').replace('%{size}', maxFileSize),
144
+ style: 'error'
145
+ }]);
146
+ }
147
+ return false;
148
+ }
149
+ return true;
150
+ };
151
+ const areFilesCountWithinLimit = files => {
152
+ if (maxFilesCount && files.length > maxFilesCount) {
120
153
  setFilesStatus([{
121
- description: getCopy('fileTooBig').replace('%{size}', maxFileSize),
154
+ description: getCopy('tooManyFiles').replace('%{maxFilesCount}', maxFilesCount),
122
155
  style: 'error'
123
156
  }]);
124
157
  return false;
125
158
  }
126
- return true;
127
- });
128
- const areMoreFilesUploadedThanAllowed = files => {
129
- if (files.length > maxFilesCount) {
159
+ if (minFilesCount && files.length < minFilesCount) {
130
160
  setFilesStatus([{
131
- description: getCopy('tooManyFiles').replace('%{maxFilesCount}', maxFilesCount),
161
+ description: getCopy('fewFiles').replace('%{minFilesCount}', minFilesCount),
132
162
  style: 'error'
133
163
  }]);
134
- return true;
164
+ return false;
135
165
  }
136
- return false;
166
+ return true;
137
167
  };
138
168
  const handleFilesUpload = async files => {
139
169
  try {
@@ -169,11 +199,8 @@ const FileUpload = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
169
199
  }
170
200
  };
171
201
  const handleFileDeletion = async file => {
172
- const copyFile = {
173
- ...file
174
- };
175
202
  try {
176
- const result = await onDelete(copyFile);
203
+ const result = await onDelete(file);
177
204
  setFilesStatus(prevStatus => {
178
205
  const filteredFilesStatus = prevStatus.filter(f => f.file && f.file.name !== file.name);
179
206
  return result.error ? [...filteredFilesStatus, {
@@ -212,7 +239,7 @@ const FileUpload = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
212
239
  if (files.length === 0) return;
213
240
  if (fileTypes && !areFileTypesExtensionsValid(files)) return;
214
241
  if (maxFileSize && !areFileSizesWithinLimit(files)) return;
215
- if (maxFilesCount && areMoreFilesUploadedThanAllowed(files)) return;
242
+ if ((maxFilesCount || minFilesCount) && !areFilesCountWithinLimit(files)) return;
216
243
  await handleFilesUpload(files);
217
244
  if (inputFileRef.current) inputFileRef.current.value = '';
218
245
  };
@@ -225,8 +252,8 @@ const FileUpload = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
225
252
  multiple: allowMultipleFiles
226
253
  });
227
254
  if (result) {
228
- const files = Array.isArray(result) ? result : [result];
229
- if (files.find(file => file.type === 'cancel')) return;
255
+ if (result.canceled) return;
256
+ const files = Array.isArray(result.assets) ? result.assets : [result.assets];
230
257
  const event = {
231
258
  target: {
232
259
  files
@@ -323,6 +350,14 @@ FileUpload.propTypes = {
323
350
  /**
324
351
  * The document picker to use for mobile
325
352
  */
326
- documentPicker: PropTypes.object
353
+ documentPicker: PropTypes.object,
354
+ /**
355
+ * The minimum number of files allowed for upload.
356
+ */
357
+ minFilesCount: PropTypes.number,
358
+ /**
359
+ * The minimum file size allowed for upload in MB.
360
+ */
361
+ minFileSize: PropTypes.number
327
362
  };
328
363
  export default FileUpload;
@@ -16,7 +16,9 @@ export default {
16
16
  uploadSuccess: 'uploaded successfully.',
17
17
  uploadError: 'was not uploaded due to',
18
18
  deleteProblem: 'was not deleted due to',
19
- tooManyFiles: 'You can only select up to %{maxFilesCount} files at the same time.'
19
+ tooManyFiles: 'You can only select up to %{maxFilesCount} files at the same time.',
20
+ fileTooSmall: 'The selected file must be at least %{size}MB.',
21
+ fewFiles: 'You must select at least %{minFilesCount} files.'
20
22
  },
21
23
  fr: {
22
24
  label: 'Téléverser les fichiers"',
@@ -35,6 +37,8 @@ export default {
35
37
  uploadSuccess: 'téléchargé avec succès.',
36
38
  uploadError: `n'a pas été téléchargé en raison de`,
37
39
  deleteProblem: `n'a pas été supprimé en raison de`,
38
- tooManyFiles: `Vous ne pouvez sélectionner que jusqu'à %{maxFilesCount} fichiers en même temps.`
40
+ tooManyFiles: `Vous ne pouvez sélectionner que jusqu'à %{maxFilesCount} fichiers en même temps.`,
41
+ fileTooSmall: `Le fichier sélectionné doit faire au moins %{size}Mo.`,
42
+ fewFiles: `Vous devez sélectionner au moins %{minFilesCount} fichiers.`
39
43
  }
40
44
  };
@@ -49,6 +49,8 @@ const InputLabel = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
49
49
  tooltip,
50
50
  tokens,
51
51
  variant,
52
+ characterCount,
53
+ maxCharacterAllowed,
52
54
  ...rest
53
55
  } = _ref3;
54
56
  const themeTokens = useThemeTokens('InputLabel', tokens, variant);
@@ -81,11 +83,26 @@ const InputLabel = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
81
83
  content: tooltip,
82
84
  copy: copy
83
85
  })
86
+ }), maxCharacterAllowed && isHintInline && /*#__PURE__*/_jsxs(Text, {
87
+ style: [selectHintStyles({
88
+ ...themeTokens
89
+ }), staticStyles.characterCountlabel],
90
+ children: [characterCount, "/", maxCharacterAllowed]
84
91
  })]
85
- }), hint && !isHintInline && /*#__PURE__*/_jsx(Text, {
92
+ }), hint && !maxCharacterAllowed && !isHintInline && /*#__PURE__*/_jsx(Text, {
86
93
  style: [selectHintStyles(themeTokens), staticStyles.hintBelow],
87
94
  nativeID: hintId,
88
95
  children: hint
96
+ }), hint && maxCharacterAllowed && !isHintInline && /*#__PURE__*/_jsxs(View, {
97
+ style: staticStyles.container,
98
+ children: [/*#__PURE__*/_jsx(Text, {
99
+ style: [selectHintStyles(themeTokens), staticStyles.flexHintBelow],
100
+ nativeID: hintId,
101
+ children: hint
102
+ }), /*#__PURE__*/_jsxs(Text, {
103
+ style: [selectHintStyles(themeTokens), staticStyles.characterCountlabel],
104
+ children: [characterCount, "/", maxCharacterAllowed]
105
+ })]
89
106
  })]
90
107
  });
91
108
  });
@@ -120,6 +137,14 @@ InputLabel.propTypes = {
120
137
  * Content of an optional `Tooltip`. If set, a tooltip button will be shown next to the label.
121
138
  */
122
139
  tooltip: PropTypes.string,
140
+ /**
141
+ * Current number of characterts of an input text.
142
+ */
143
+ characterCount: PropTypes.number,
144
+ /**
145
+ * Max number of characters that allows an input text.
146
+ */
147
+ maxCharacterAllowed: PropTypes.number,
123
148
  tokens: getTokensPropType('InputLabel'),
124
149
  variant: variantProp.propType
125
150
  };
@@ -129,13 +154,22 @@ const staticStyles = StyleSheet.create({
129
154
  flexShrink: 1,
130
155
  flexDirection: 'row'
131
156
  },
157
+ characterCountlabel: {
158
+ marginLeft: 'auto',
159
+ marginTop: 'auto'
160
+ },
132
161
  label: {
133
- flexShrink: 1
162
+ flexShrink: 1,
163
+ alignSelf: 'center'
134
164
  },
135
165
  hintBelow: {
136
166
  flexBasis: '100%',
137
167
  flexShrink: 0
138
168
  },
169
+ flexHintBelow: {
170
+ flexBasis: '100%',
171
+ flexShrink: 1
172
+ },
139
173
  tooltipAlign: {
140
174
  alignSelf: 'center',
141
175
  justifyContent: 'center'
@@ -5,7 +5,8 @@ import Feedback from '../Feedback';
5
5
  import StackView from '../StackView';
6
6
  import { useThemeTokens } from '../ThemeProvider';
7
7
  import useInputSupports from './useInputSupports';
8
- import { getTokensPropType } from '../utils';
8
+ import { getTokensPropType, useCopy } from '../utils';
9
+ import dictionary from './dictionary';
9
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
11
  const InputSupports = /*#__PURE__*/React.forwardRef((_ref, ref) => {
11
12
  let {
@@ -19,11 +20,19 @@ const InputSupports = /*#__PURE__*/React.forwardRef((_ref, ref) => {
19
20
  feedbackProps = {},
20
21
  tooltip,
21
22
  validation,
23
+ maxCharacterAllowed,
24
+ inputValue,
22
25
  nativeID
23
26
  } = _ref;
24
27
  const {
25
28
  space
26
29
  } = useThemeTokens('InputSupports');
30
+ const getCopy = useCopy({
31
+ dictionary,
32
+ copy
33
+ });
34
+ const maxCharsReachedErrorMessage = (inputValue === null || inputValue === void 0 ? void 0 : inputValue.length) > maxCharacterAllowed ? getCopy('maxCharsMessage').replace(/%\{charCount\}/g, maxCharacterAllowed) : '';
35
+ const feedbackValidation = (inputValue === null || inputValue === void 0 ? void 0 : inputValue.length) > maxCharacterAllowed ? 'error' : validation;
27
36
  const {
28
37
  inputId,
29
38
  hintId,
@@ -34,7 +43,10 @@ const InputSupports = /*#__PURE__*/React.forwardRef((_ref, ref) => {
34
43
  hint,
35
44
  label,
36
45
  validation,
37
- nativeID
46
+ nativeID,
47
+ copy,
48
+ maxCharacterAllowed,
49
+ charactersCount: maxCharacterAllowed - (inputValue === null || inputValue === void 0 ? void 0 : inputValue.length)
38
50
  });
39
51
  return /*#__PURE__*/_jsxs(StackView, {
40
52
  space: space,
@@ -46,14 +58,17 @@ const InputSupports = /*#__PURE__*/React.forwardRef((_ref, ref) => {
46
58
  hintPosition: hintPosition,
47
59
  hintId: hintId,
48
60
  tooltip: tooltip,
49
- forId: inputId
61
+ forId: inputId,
62
+ characterCount: inputValue === null || inputValue === void 0 ? void 0 : inputValue.length,
63
+ maxCharacterAllowed: maxCharacterAllowed
50
64
  }), typeof children === 'function' ? children({
51
65
  inputId,
52
- ...a11yProps
53
- }) : children, feedback ? /*#__PURE__*/_jsx(Feedback, {
66
+ ...a11yProps,
67
+ validation: feedbackValidation
68
+ }) : children, feedback || maxCharsReachedErrorMessage ? /*#__PURE__*/_jsx(Feedback, {
54
69
  id: feedbackId,
55
- title: feedback,
56
- validation: validation,
70
+ title: feedback || maxCharsReachedErrorMessage,
71
+ validation: feedbackValidation,
57
72
  tokens: feedbackTokens,
58
73
  variant: {
59
74
  icon: feedbackProps.showFeedbackIcon
@@ -105,6 +120,14 @@ InputSupports.propTypes = {
105
120
  /**
106
121
  * ID for DOM element on web
107
122
  */
108
- nativeID: PropTypes.string
123
+ nativeID: PropTypes.string,
124
+ /**
125
+ * The text value of a TextArea or TextInput
126
+ */
127
+ inputValue: PropTypes.string,
128
+ /**
129
+ * Max number of characters that allows an input text.
130
+ */
131
+ maxCharacterAllowed: PropTypes.number
109
132
  };
110
133
  export default InputSupports;
@@ -0,0 +1,12 @@
1
+ export default {
2
+ en: {
3
+ maxCharacters: 'Maximum %{charCount} characters',
4
+ charactersRemaining: '%{charCount} characters remaining',
5
+ maxCharsMessage: 'Must not exceed %{charCount} characters'
6
+ },
7
+ fr: {
8
+ maxCharacters: '%{charCount} caractères maximum',
9
+ charactersRemaining: '%{charCount} caractères restants',
10
+ maxCharsMessage: 'Ne doit pas dépasser %{charCount} caractères'
11
+ }
12
+ };
@@ -1,4 +1,6 @@
1
1
  import useUniqueId from '../utils/useUniqueId';
2
+ import { useCopy } from '../utils';
3
+ import dictionary from './dictionary';
2
4
  const joinDefined = array => array.filter(item => item !== undefined).join(' ');
3
5
  const useInputSupports = _ref => {
4
6
  let {
@@ -6,19 +8,26 @@ const useInputSupports = _ref => {
6
8
  feedback,
7
9
  validation,
8
10
  hint,
9
- nativeID
11
+ nativeID,
12
+ copy,
13
+ maxCharacterAllowed,
14
+ charactersCount
10
15
  } = _ref;
16
+ const getCopy = useCopy({
17
+ dictionary,
18
+ copy
19
+ });
11
20
  const hasValidationError = validation === 'error';
12
21
  const inputId = useUniqueId('input');
13
22
  const hintId = useUniqueId('input-hint');
14
23
  const feedbackId = useUniqueId('input-feedback');
15
24
  const a11yProps = {
16
25
  accessibilityLabel: label,
17
- accessibilityHint: joinDefined([!hasValidationError && feedback, hint]),
26
+ accessibilityHint: joinDefined([!hasValidationError && feedback, hint, maxCharacterAllowed ? getCopy('maxCharacters').replace(/%\{charCount\}/g, maxCharacterAllowed) : undefined]),
18
27
  // native only -> replaced with describedBy on web
19
28
  accessibilityDescribedBy: joinDefined([!hasValidationError && feedback && feedbackId,
20
29
  // feedback receives a11yRole=alert on error, so there's no need to include it here
21
- hint && hintId]),
30
+ hint && hintId, charactersCount ? getCopy('charactersRemaining').replace(/%\{charCount\}/g, charactersCount) : undefined]),
22
31
  accessibilityInvalid: hasValidationError
23
32
  };
24
33
  return {
@@ -19,20 +19,15 @@ const selectOuterBorderStyles = _ref => {
19
19
  borderRadius,
20
20
  outerBorderOutline
21
21
  } = _ref;
22
- return (
23
- // A view wrapper with a border on native messes up inline text alignment
24
- // so for now make focus styles strictly web-only
25
- Platform.OS === 'web' ? {
26
- // Allow theme to define outline, or, turn off outline and use border if rounded corners required
27
- outline: outerBorderOutline,
28
- ...applyOuterBorder({
29
- outerBorderColor,
30
- outerBorderWidth,
31
- outerBorderGap
32
- }),
33
- borderRadius
34
- } : {}
35
- );
22
+ return {
23
+ outline: outerBorderOutline,
24
+ ...applyOuterBorder({
25
+ outerBorderColor,
26
+ outerBorderWidth,
27
+ outerBorderGap
28
+ }),
29
+ borderRadius
30
+ };
36
31
  };
37
32
  const selectTextStyles = _ref2 => {
38
33
  let {
@@ -182,7 +177,7 @@ const LinkBase = /*#__PURE__*/React.forwardRef((_ref6, ref) => {
182
177
  const themeTokens = resolveLinkTokens(linkState);
183
178
  const outerBorderStyles = selectOuterBorderStyles(themeTokens);
184
179
  const decorationStyles = selectDecorationStyles(themeTokens);
185
- return [outerBorderStyles, blockLeftStyle, decorationStyles, hasIcon && staticStyles.rowContainer];
180
+ return [outerBorderStyles, staticStyles.outerBorderStyles, blockLeftStyle, decorationStyles, hasIcon && staticStyles.rowContainer];
186
181
  },
187
182
  children: linkState => {
188
183
  const themeTokens = resolveLinkTokens(linkState);
@@ -264,6 +259,13 @@ const staticStyles = StyleSheet.create({
264
259
  pointerEvents: 'none'
265
260
  }
266
261
  })
262
+ },
263
+ outerBorderStyles: {
264
+ ...(Platform.OS !== 'web' && {
265
+ margin: 0,
266
+ marginHorizontal: 2,
267
+ padding: 0
268
+ })
267
269
  }
268
270
  });
269
271
  export default withLinkRouter(LinkBase);
@@ -29,7 +29,8 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
29
29
  overlaidPosition,
30
30
  maxWidth,
31
31
  minWidth,
32
- onLayout
32
+ onLayout,
33
+ tokens
33
34
  } = _ref;
34
35
  const systemTokens = useThemeTokens('Listbox', {}, {});
35
36
  return /*#__PURE__*/_jsx(View, {
@@ -45,7 +46,8 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
45
46
  paddingBottom: paddingVertical,
46
47
  paddingTop: paddingVertical,
47
48
  paddingLeft: paddingHorizontal,
48
- paddingRight: paddingHorizontal
49
+ paddingRight: paddingHorizontal,
50
+ ...tokens
49
51
  },
50
52
  children: children
51
53
  })
@@ -72,6 +74,7 @@ DropdownOverlay.propTypes = {
72
74
  }),
73
75
  maxWidth: PropTypes.number,
74
76
  minWidth: PropTypes.number,
75
- onLayout: PropTypes.func
77
+ onLayout: PropTypes.func,
78
+ tokens: PropTypes.object
76
79
  };
77
80
  export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay;
@@ -148,11 +148,11 @@ const Modal = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
148
148
  const manageFocus = React.useCallback(event => {
149
149
  if (event.key === 'Tab') {
150
150
  var _modalBodyRef$current;
151
- const focusableElements = Array.from(modalBodyRef === null || modalBodyRef === void 0 || (_modalBodyRef$current = modalBodyRef.current) === null || _modalBodyRef$current === void 0 ? void 0 : _modalBodyRef$current.querySelectorAll(`
151
+ const focusableElements = Array.from((modalBodyRef === null || modalBodyRef === void 0 || (_modalBodyRef$current = modalBodyRef.current) === null || _modalBodyRef$current === void 0 ? void 0 : _modalBodyRef$current.querySelectorAll(`
152
152
  a[href], button, textarea, input, select,
153
153
  [tabindex]:not([tabindex="-1"]),
154
154
  [contenteditable="true"]
155
- `));
155
+ `)) || []);
156
156
  const firstElement = focusableElements[0];
157
157
  const lastElement = focusableElements[focusableElements.length - 1];
158
158
  if (event.shiftKey && document.activeElement === firstElement) {