@telus-uds/components-base 2.1.0 → 2.3.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/CHANGELOG.md CHANGED
@@ -1,12 +1,40 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- <!-- This log was last generated on Mon, 02 Dec 2024 20:23:48 GMT and should not be manually modified. -->
3
+ This log was last generated on Thu, 19 Dec 2024 04:54:39 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 2.3.0
8
+
9
+ Thu, 19 Dec 2024 04:54:39 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `Autocomplete`: Enable `tokens` prop to set custom styles for the `maxHeight` and `scroll` behavior. (jaime.tuyuc@telus.com)
14
+ - `FileUpload`: add minFilesCount and minFileSize props (guillermo.peitzner@telus.com)
15
+ - Bump @telus-uds/system-theme-tokens to v3.2.0
16
+
17
+ ### Patches
18
+
19
+ - `Card`: Include additional `tokenKeys` to remove warnings. (jaime.tuyuc@telus.com)
20
+ - `StepTracker`: elements must only use supported ARIA attributes (sergio.ramirez@telus.com)
21
+
22
+ ## 2.2.0
23
+
24
+ Fri, 06 Dec 2024 02:06:03 GMT
25
+
26
+ ### Minor changes
27
+
28
+ - `PressableItem`: pass event to on-press function (guillermo.peitzner@telus.com)
29
+ - `ChevronLink`, `Search` & `ResponsiveImage`: new `dataSet` prop added to the components to allow the capability to pass data (35577399+JoshHC@users.noreply.github.com)
30
+
31
+ ### Patches
32
+
33
+ - `Typography`: fix gradient when enableMediaQueryStyleSheet is true (Mauricio.BatresMontejo@telus.com)
34
+
7
35
  ## 2.1.0
8
36
 
9
- Mon, 02 Dec 2024 20:23:48 GMT
37
+ Mon, 02 Dec 2024 20:29:26 GMT
10
38
 
11
39
  ### Minor changes
12
40
 
@@ -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
  };
@@ -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;
@@ -113,9 +113,9 @@ const PressableItem = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
113
113
  onClick: onPress
114
114
  }),
115
115
  ...selectProps(rest),
116
- onPress: () => {
116
+ onPress: event => {
117
117
  setSelectedId(id);
118
- onPress();
118
+ onPress(event);
119
119
  },
120
120
  children: pressableState => {
121
121
  return /*#__PURE__*/_jsxs(Text, {
@@ -73,6 +73,7 @@ const Search = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
73
73
  tokens,
74
74
  variant,
75
75
  nativeSubmitBtnID,
76
+ dataSet,
76
77
  ...rest
77
78
  } = _ref3;
78
79
  const {
@@ -132,6 +133,7 @@ const Search = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
132
133
  } = selectContainerProps(rest);
133
134
  return /*#__PURE__*/_jsx(View, {
134
135
  ...containerProps,
136
+ dataSet: dataSet,
135
137
  children: /*#__PURE__*/_jsx(TextInputBase, {
136
138
  nativeID: nativeID,
137
139
  testID: testID,
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import Platform from "react-native-web/dist/exports/Platform";
3
4
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
4
5
  import Text from "react-native-web/dist/exports/Text";
5
6
  import View from "react-native-web/dist/exports/View";
@@ -108,7 +109,7 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
108
109
  dictionary,
109
110
  copy
110
111
  });
111
- const stepTrackerLabel = showStepTrackerLabel ? (typeof copy === 'string' ? getCopy(textStepTrackerLabel ?? 1).stepTrackerLabel : getCopy('stepTrackerLabel')).replace('%{stepNumber}', current < steps.length ? current + 1 : steps.length).replace('%{stepCount}', steps.length).replace('%{stepLabel}', current < steps.length ? steps[current] : steps[steps.length - 1]) : '';
112
+ const stepTrackerLabel = showStepTrackerLabel ? (typeof copy === 'string' ? getCopy(textStepTrackerLabel ?? 1).stepTrackerLabel : getCopy('stepTrackerLabel')).replace('%{stepNumber}', current < steps.length ? current + 1 : steps.length).replace('%{stepCount}', steps.length).replace('%{stepLabel}', current < steps.length ? steps[current] : steps[steps.length - 1]) : getCopy('stepTrackerLabel');
112
113
  const getStepLabel = index => {
113
114
  if (themeTokens.showStepLabel) {
114
115
  var _getCopy;
@@ -122,7 +123,6 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
122
123
  if (!steps.length) return null;
123
124
  const selectedProps = selectProps({
124
125
  accessibilityLabel: stepTrackerLabel,
125
- accessibilityLevel: 1,
126
126
  accessibilityRole: 'progressbar',
127
127
  accessibilityValue: {
128
128
  min: 0,
@@ -132,6 +132,8 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
132
132
  },
133
133
  ...rest
134
134
  });
135
+ const stepsContainerAccessibilityRole = Platform.OS === 'web' ? 'list' : 'tablist';
136
+ const stepAccessibilityRole = Platform.OS === 'web' ? 'listitem' : 'tab';
135
137
  return /*#__PURE__*/_jsx(View, {
136
138
  ref: ref,
137
139
  style: selectContainerStyles(themeTokens),
@@ -140,6 +142,7 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
140
142
  space: 0,
141
143
  children: [/*#__PURE__*/_jsx(View, {
142
144
  style: staticStyles.stepsContainer,
145
+ accessibilityRole: stepsContainerAccessibilityRole,
143
146
  children: steps.map((label, index) => {
144
147
  return /*#__PURE__*/_jsx(Step, {
145
148
  status: current,
@@ -147,13 +150,17 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
147
150
  name: getStepLabel(index),
148
151
  stepIndex: index,
149
152
  stepCount: steps.length,
150
- tokens: themeTokens
153
+ tokens: themeTokens,
154
+ accessibilityRole: stepAccessibilityRole,
155
+ accessibilityCurrent: current === index && Platform.OS === 'web' && 'step'
151
156
  }, label);
152
157
  })
153
158
  }), showStepTrackerLabel && /*#__PURE__*/_jsx(View, {
154
159
  style: [staticStyles.stepTrackerLabelContainer, selectStepTrackerLabelContainerStyles(themeTokens)],
155
160
  children: /*#__PURE__*/_jsx(Text, {
156
161
  style: selectStepTrackerLabelStyles(themeTokens, themeOptions),
162
+ accessibilityRole: "header",
163
+ accessibilityLevel: 2,
157
164
  children: stepTrackerLabel
158
165
  })
159
166
  })]
@@ -122,6 +122,18 @@ const Typography = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
122
122
  textDecorationLine,
123
123
  ...viewportTokens
124
124
  }, themeOptions);
125
+ if (themeTokens[vp].gradient) {
126
+ const tokensWithGradient = {
127
+ ...acc[vp],
128
+ color: 'transparent',
129
+ backgroundImage: generateGradientString(themeTokens[vp].gradient),
130
+ backgroundClip: 'text'
131
+ };
132
+ return {
133
+ ...acc,
134
+ [vp]: tokensWithGradient
135
+ };
136
+ }
125
137
  return acc;
126
138
  }, {});
127
139
  const mediaQueryStyles = createMediaQueryStyles(transformedThemeTokens);
package/package.json CHANGED
@@ -13,10 +13,10 @@
13
13
  "@gorhom/portal": "^1.0.14",
14
14
  "@react-native-picker/picker": "^2.9.0",
15
15
  "@telus-uds/system-constants": "^2.0.0",
16
- "@telus-uds/system-theme-tokens": "^3.1.0",
16
+ "@telus-uds/system-theme-tokens": "^3.2.0",
17
17
  "airbnb-prop-types": "^2.16.0",
18
18
  "css-mediaquery": "^0.1.2",
19
- "expo-document-picker": "~10.3.0",
19
+ "expo-document-picker": "^13.0.1",
20
20
  "expo-linear-gradient": "^12.5.0",
21
21
  "expo-modules-core": "^1.12.24",
22
22
  "lodash.debounce": "^4.0.8",
@@ -81,6 +81,6 @@
81
81
  "standard-engine": {
82
82
  "skip": true
83
83
  },
84
- "version": "2.1.0",
84
+ "version": "2.3.0",
85
85
  "types": "types/index.d.ts"
86
86
  }
@@ -11,7 +11,8 @@ import {
11
11
  useCopy,
12
12
  htmlAttrs,
13
13
  useOverlaidPosition,
14
- useSafeLayoutEffect
14
+ useSafeLayoutEffect,
15
+ getTokensPropType
15
16
  } from '../utils'
16
17
  import { useThemeTokens } from '../ThemeProvider'
17
18
  import Listbox from '../Listbox'
@@ -103,11 +104,15 @@ const Autocomplete = React.forwardRef(
103
104
  value,
104
105
  helpText = '',
105
106
  loadingLabel,
107
+ tokens,
106
108
  ...rest
107
109
  },
108
110
  ref
109
111
  ) => {
110
- const { color: resultsTextColor } = useThemeTokens('Search', {}, { focus: true })
112
+ const { color: resultsTextColor, ...restOfTokens } = useThemeTokens('Autocomplete', tokens, {
113
+ focus: true
114
+ })
115
+
111
116
  // The wrapped input is mostly responsible for controlled vs uncontrolled handling,
112
117
  // but we also need to adjust suggestions based on the mode:
113
118
  // - in controlled mode we rely entirely on the suggestions passed via the `items` prop,
@@ -371,6 +376,7 @@ const Autocomplete = React.forwardRef(
371
376
  minWidth={fullWidth ? inputWidth : MIN_LISTBOX_WIDTH}
372
377
  maxWidth={inputWidth}
373
378
  onLayout={handleMeasure}
379
+ tokens={restOfTokens}
374
380
  ref={openOverlayRef}
375
381
  >
376
382
  {isLoading ? (
@@ -433,6 +439,7 @@ Autocomplete.propTypes = {
433
439
  * </Autocomplete>
434
440
  */
435
441
  children: PropTypes.func,
442
+ tokens: getTokensPropType('Autocomplete'),
436
443
  /**
437
444
  * Copy language identifier
438
445
  */
@@ -22,7 +22,9 @@ const selectStyles = ({
22
22
  minWidth,
23
23
  shadow,
24
24
  backgroundGradient,
25
- gradient
25
+ gradient,
26
+ maxHeight,
27
+ overflowY
26
28
  }) => {
27
29
  return {
28
30
  flex,
@@ -48,7 +50,8 @@ const selectStyles = ({
48
50
  ? {
49
51
  backgroundImage: `linear-gradient(${backgroundGradient.angle}deg, ${backgroundGradient.stops[0].color}, ${backgroundGradient.stops[1].color})`
50
52
  }
51
- : {})
53
+ : {}),
54
+ ...(Platform.OS === 'web' ? { maxHeight, overflowY } : {})
52
55
  }
53
56
  }
54
57
 
@@ -48,7 +48,18 @@ const tokenKeys = [
48
48
  // https://github.com/telus/universal-design-system/issues/782
49
49
  'outerBorderColor',
50
50
  'outerBorderWidth',
51
- 'outerBorderGap'
51
+ 'outerBorderGap',
52
+ 'icon',
53
+ 'iconBackgroundColor',
54
+ 'iconColor',
55
+ 'iconSize',
56
+ 'inputBackgroundColor',
57
+ 'inputBorderColor',
58
+ 'inputBorderRadius',
59
+ 'inputBorderWidth',
60
+ 'inputHeight',
61
+ 'inputShadow',
62
+ 'inputWidth'
52
63
  ]
53
64
  export const selectPressableCardTokens = (tokens) =>
54
65
  Object.fromEntries(tokenKeys.map((key) => [key, tokens[key]]))
@@ -90,6 +90,8 @@ const selectComponentTokens = (tokens, componentIdentifier) => {
90
90
  * @param {boolean} props.allowMultipleFiles - Whether multiple files can be uploaded.
91
91
  * @param {number} props.maxFileSize - The maximum file size in megabytes.
92
92
  * @param {number} props.maxFilesCount - The maximum number of files that can be uploaded.
93
+ * @param {number} props.minFilesCount - The minimum number of files that can be uploaded.
94
+ * @param {number} props.minFileSize - The minimum file size in megabytes.
93
95
  * @param {Function} props.onUpload - The callback function for file upload.
94
96
  * @param {Function} props.onDelete - The callback function for file deletion.
95
97
  * @param {Object} props.documentPicker - The document picker object.
@@ -106,6 +108,8 @@ const FileUpload = React.forwardRef(
106
108
  allowMultipleFiles = false,
107
109
  maxFileSize,
108
110
  maxFilesCount = 1,
111
+ minFilesCount = 1,
112
+ minFileSize = 0,
109
113
  onUpload,
110
114
  onDelete,
111
115
  documentPicker,
@@ -113,6 +117,10 @@ const FileUpload = React.forwardRef(
113
117
  },
114
118
  ref
115
119
  ) => {
120
+ if (minFilesCount <= 0) {
121
+ throw new Error('minFilesCount should be greater than 0')
122
+ }
123
+
116
124
  const themeTokens = useThemeTokens('FileUpload', tokens, variant)
117
125
  const getCopy = useCopy({ dictionary, copy })
118
126
 
@@ -125,50 +133,78 @@ const FileUpload = React.forwardRef(
125
133
  setRenderTrigger((prev) => prev + 1)
126
134
  }, [filesStatus])
127
135
 
128
- const areFileTypesExtensionsValid = (files) =>
129
- files.some((file) => {
136
+ const areFileTypesExtensionsValid = (files) => {
137
+ const invalidFiles = files.filter((file) => {
130
138
  const fileExtension = file.name.split('.').pop().toLowerCase()
139
+ return !fileTypes.includes(fileExtension)
140
+ })
141
+ if (invalidFiles.length > 0) {
142
+ setFilesStatus([
143
+ {
144
+ description: getCopy('wrongFileType').replace(
145
+ '%{fileType}',
146
+ stringifyFileTypesList(fileTypes, getCopy)
147
+ ),
148
+ style: 'error'
149
+ }
150
+ ])
151
+ return false
152
+ }
153
+ return true
154
+ }
131
155
 
132
- if (!fileTypes.includes(fileExtension)) {
156
+ const areFileSizesWithinLimit = (files) => {
157
+ const invalidFiles = files.filter((file) => {
158
+ const fileSize = file.size
159
+ return (
160
+ fileSize < convertFromMegaByteToByte(minFileSize) ||
161
+ fileSize > convertFromMegaByteToByte(maxFileSize)
162
+ )
163
+ })
164
+
165
+ if (invalidFiles.length > 0) {
166
+ const invalidFile = invalidFiles[0]
167
+ const fileSize = invalidFile.size
168
+ if (fileSize < convertFromMegaByteToByte(minFileSize)) {
133
169
  setFilesStatus([
134
170
  {
135
- description: getCopy('wrongFileType').replace(
136
- '%{fileType}',
137
- stringifyFileTypesList(fileTypes, getCopy)
138
- ),
171
+ description: getCopy('fileTooSmall').replace('%{size}', minFileSize),
139
172
  style: 'error'
140
173
  }
141
174
  ])
142
- return false
143
- }
144
- return true
145
- })
146
-
147
- const areFileSizesWithinLimit = (files) =>
148
- files.some((file) => {
149
- if (file.size > convertFromMegaByteToByte(maxFileSize)) {
175
+ } else {
150
176
  setFilesStatus([
151
177
  {
152
178
  description: getCopy('fileTooBig').replace('%{size}', maxFileSize),
153
179
  style: 'error'
154
180
  }
155
181
  ])
156
- return false
157
182
  }
158
- return true
159
- })
183
+ return false
184
+ }
185
+ return true
186
+ }
160
187
 
161
- const areMoreFilesUploadedThanAllowed = (files) => {
162
- if (files.length > maxFilesCount) {
188
+ const areFilesCountWithinLimit = (files) => {
189
+ if (maxFilesCount && files.length > maxFilesCount) {
163
190
  setFilesStatus([
164
191
  {
165
192
  description: getCopy('tooManyFiles').replace('%{maxFilesCount}', maxFilesCount),
166
193
  style: 'error'
167
194
  }
168
195
  ])
169
- return true
196
+ return false
170
197
  }
171
- return false
198
+ if (minFilesCount && files.length < minFilesCount) {
199
+ setFilesStatus([
200
+ {
201
+ description: getCopy('fewFiles').replace('%{minFilesCount}', minFilesCount),
202
+ style: 'error'
203
+ }
204
+ ])
205
+ return false
206
+ }
207
+ return true
172
208
  }
173
209
 
174
210
  const handleFilesUpload = async (files) => {
@@ -212,9 +248,8 @@ const FileUpload = React.forwardRef(
212
248
  }
213
249
 
214
250
  const handleFileDeletion = async (file) => {
215
- const copyFile = { ...file }
216
251
  try {
217
- const result = await onDelete(copyFile)
252
+ const result = await onDelete(file)
218
253
  setFilesStatus((prevStatus) => {
219
254
  const filteredFilesStatus = prevStatus.filter((f) => f.file && f.file.name !== file.name)
220
255
  return result.error
@@ -269,7 +304,7 @@ const FileUpload = React.forwardRef(
269
304
 
270
305
  if (maxFileSize && !areFileSizesWithinLimit(files)) return
271
306
 
272
- if (maxFilesCount && areMoreFilesUploadedThanAllowed(files)) return
307
+ if ((maxFilesCount || minFilesCount) && !areFilesCountWithinLimit(files)) return
273
308
 
274
309
  await handleFilesUpload(files)
275
310
 
@@ -285,8 +320,8 @@ const FileUpload = React.forwardRef(
285
320
  multiple: allowMultipleFiles
286
321
  })
287
322
  if (result) {
288
- const files = Array.isArray(result) ? result : [result]
289
- if (files.find((file) => file.type === 'cancel')) return
323
+ if (result.canceled) return
324
+ const files = Array.isArray(result.assets) ? result.assets : [result.assets]
290
325
  const event = { target: { files } }
291
326
  await handleOnChange(event)
292
327
  }
@@ -390,7 +425,15 @@ FileUpload.propTypes = {
390
425
  /**
391
426
  * The document picker to use for mobile
392
427
  */
393
- documentPicker: PropTypes.object
428
+ documentPicker: PropTypes.object,
429
+ /**
430
+ * The minimum number of files allowed for upload.
431
+ */
432
+ minFilesCount: PropTypes.number,
433
+ /**
434
+ * The minimum file size allowed for upload in MB.
435
+ */
436
+ minFileSize: PropTypes.number
394
437
  }
395
438
 
396
439
  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
  }
@@ -22,7 +22,7 @@ const paddingVertical = 0
22
22
  const paddingHorizontal = 0
23
23
 
24
24
  const DropdownOverlay = React.forwardRef(
25
- ({ children, isReady = false, overlaidPosition, maxWidth, minWidth, onLayout }, ref) => {
25
+ ({ children, isReady = false, overlaidPosition, maxWidth, minWidth, onLayout, tokens }, ref) => {
26
26
  const systemTokens = useThemeTokens('Listbox', {}, {})
27
27
 
28
28
  return (
@@ -42,7 +42,8 @@ const DropdownOverlay = React.forwardRef(
42
42
  paddingBottom: paddingVertical,
43
43
  paddingTop: paddingVertical,
44
44
  paddingLeft: paddingHorizontal,
45
- paddingRight: paddingHorizontal
45
+ paddingRight: paddingHorizontal,
46
+ ...tokens
46
47
  }}
47
48
  >
48
49
  {children}
@@ -75,7 +76,8 @@ DropdownOverlay.propTypes = {
75
76
  }),
76
77
  maxWidth: PropTypes.number,
77
78
  minWidth: PropTypes.number,
78
- onLayout: PropTypes.func
79
+ onLayout: PropTypes.func,
80
+ tokens: PropTypes.object
79
81
  }
80
82
 
81
83
  export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay
@@ -109,9 +109,9 @@ const PressableItem = React.forwardRef(
109
109
  {...(href && { href })}
110
110
  {...(onPress && { onClick: onPress })}
111
111
  {...selectProps(rest)}
112
- onPress={() => {
112
+ onPress={(event) => {
113
113
  setSelectedId(id)
114
- onPress()
114
+ onPress(event)
115
115
  }}
116
116
  >
117
117
  {(pressableState) => {
@@ -77,6 +77,7 @@ const Search = React.forwardRef(
77
77
  tokens,
78
78
  variant,
79
79
  nativeSubmitBtnID,
80
+ dataSet,
80
81
  ...rest
81
82
  },
82
83
  ref
@@ -130,7 +131,7 @@ const Search = React.forwardRef(
130
131
  const { nativeID, testID, ...containerProps } = selectContainerProps(rest)
131
132
 
132
133
  return (
133
- <View {...containerProps}>
134
+ <View {...containerProps} dataSet={dataSet}>
134
135
  <TextInputBase
135
136
  nativeID={nativeID}
136
137
  testID={testID}
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { StyleSheet, Text, View } from 'react-native'
3
+ import { Platform, StyleSheet, Text, View } from 'react-native'
4
4
  import StackView from '../StackView'
5
5
  import { applyTextStyles, useTheme, useThemeTokens } from '../ThemeProvider'
6
6
  import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
@@ -105,7 +105,7 @@ const StepTracker = React.forwardRef(
105
105
  '%{stepLabel}',
106
106
  current < steps.length ? steps[current] : steps[steps.length - 1]
107
107
  )
108
- : ''
108
+ : getCopy('stepTrackerLabel')
109
109
 
110
110
  const getStepLabel = (index) => {
111
111
  if (themeTokens.showStepLabel) {
@@ -118,7 +118,6 @@ const StepTracker = React.forwardRef(
118
118
  if (!steps.length) return null
119
119
  const selectedProps = selectProps({
120
120
  accessibilityLabel: stepTrackerLabel,
121
- accessibilityLevel: 1,
122
121
  accessibilityRole: 'progressbar',
123
122
  accessibilityValue: {
124
123
  min: 0,
@@ -129,10 +128,16 @@ const StepTracker = React.forwardRef(
129
128
  ...rest
130
129
  })
131
130
 
131
+ const stepsContainerAccessibilityRole = Platform.OS === 'web' ? 'list' : 'tablist'
132
+ const stepAccessibilityRole = Platform.OS === 'web' ? 'listitem' : 'tab'
133
+
132
134
  return (
133
135
  <View ref={ref} style={selectContainerStyles(themeTokens)} {...selectedProps}>
134
136
  <StackView space={0}>
135
- <View style={staticStyles.stepsContainer}>
137
+ <View
138
+ style={staticStyles.stepsContainer}
139
+ accessibilityRole={stepsContainerAccessibilityRole}
140
+ >
136
141
  {steps.map((label, index) => {
137
142
  return (
138
143
  <Step
@@ -143,6 +148,8 @@ const StepTracker = React.forwardRef(
143
148
  stepIndex={index}
144
149
  stepCount={steps.length}
145
150
  tokens={themeTokens}
151
+ accessibilityRole={stepAccessibilityRole}
152
+ accessibilityCurrent={current === index && Platform.OS === 'web' && 'step'}
146
153
  />
147
154
  )
148
155
  })}
@@ -154,7 +161,11 @@ const StepTracker = React.forwardRef(
154
161
  selectStepTrackerLabelContainerStyles(themeTokens)
155
162
  ]}
156
163
  >
157
- <Text style={selectStepTrackerLabelStyles(themeTokens, themeOptions)}>
164
+ <Text
165
+ style={selectStepTrackerLabelStyles(themeTokens, themeOptions)}
166
+ accessibilityRole="header"
167
+ accessibilityLevel={2}
168
+ >
158
169
  {stepTrackerLabel}
159
170
  </Text>
160
171
  </View>
@@ -152,6 +152,18 @@ const Typography = React.forwardRef(
152
152
  },
153
153
  themeOptions
154
154
  )
155
+ if (themeTokens[vp].gradient) {
156
+ const tokensWithGradient = {
157
+ ...acc[vp],
158
+ color: 'transparent',
159
+ backgroundImage: generateGradientString(themeTokens[vp].gradient),
160
+ backgroundClip: 'text'
161
+ }
162
+ return {
163
+ ...acc,
164
+ [vp]: tokensWithGradient
165
+ }
166
+ }
155
167
  return acc
156
168
  },
157
169
  {}