@transferwise/components 46.63.0 → 46.65.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 (93) hide show
  1. package/build/card/Card.js.map +1 -1
  2. package/build/card/Card.mjs.map +1 -1
  3. package/build/circularButton/CircularButton.js.map +1 -1
  4. package/build/circularButton/CircularButton.mjs.map +1 -1
  5. package/build/common/bottomSheet/BottomSheet.js +8 -2
  6. package/build/common/bottomSheet/BottomSheet.js.map +1 -1
  7. package/build/common/bottomSheet/BottomSheet.mjs +8 -2
  8. package/build/common/bottomSheet/BottomSheet.mjs.map +1 -1
  9. package/build/common/locale/index.js.map +1 -1
  10. package/build/common/locale/index.mjs.map +1 -1
  11. package/build/dateLookup/tableLink/TableLink.js.map +1 -1
  12. package/build/dateLookup/tableLink/TableLink.mjs.map +1 -1
  13. package/build/drawer/Drawer.js +5 -3
  14. package/build/drawer/Drawer.js.map +1 -1
  15. package/build/drawer/Drawer.mjs +5 -3
  16. package/build/drawer/Drawer.mjs.map +1 -1
  17. package/build/flowNavigation/FlowNavigation.js +1 -1
  18. package/build/flowNavigation/FlowNavigation.js.map +1 -1
  19. package/build/flowNavigation/FlowNavigation.mjs +1 -1
  20. package/build/flowNavigation/FlowNavigation.mjs.map +1 -1
  21. package/build/flowNavigation/animatedLabel/AnimatedLabel.js +89 -15
  22. package/build/flowNavigation/animatedLabel/AnimatedLabel.js.map +1 -1
  23. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs +90 -16
  24. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs.map +1 -1
  25. package/build/instructionsList/InstructionsList.js.map +1 -1
  26. package/build/instructionsList/InstructionsList.mjs.map +1 -1
  27. package/build/main.css +10 -1
  28. package/build/styles/flowNavigation/animatedLabel/AnimatedLabel.css +10 -1
  29. package/build/styles/main.css +10 -1
  30. package/build/switch/Switch.js +3 -27
  31. package/build/switch/Switch.js.map +1 -1
  32. package/build/switch/Switch.mjs +3 -27
  33. package/build/switch/Switch.mjs.map +1 -1
  34. package/build/types/card/Card.d.ts.map +1 -1
  35. package/build/types/circularButton/CircularButton.d.ts.map +1 -1
  36. package/build/types/common/bottomSheet/BottomSheet.d.ts +3 -3
  37. package/build/types/common/bottomSheet/BottomSheet.d.ts.map +1 -1
  38. package/build/types/drawer/Drawer.d.ts +4 -3
  39. package/build/types/drawer/Drawer.d.ts.map +1 -1
  40. package/build/types/flowNavigation/FlowNavigation.d.ts.map +1 -1
  41. package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts +3 -3
  42. package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts.map +1 -1
  43. package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
  44. package/build/types/switch/Switch.d.ts.map +1 -1
  45. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  46. package/build/types/uploadInput/uploadButton/UploadButton.d.ts +1 -1
  47. package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
  48. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +5 -1
  49. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  50. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +5 -5
  51. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
  52. package/build/uploadInput/UploadInput.js +42 -11
  53. package/build/uploadInput/UploadInput.js.map +1 -1
  54. package/build/uploadInput/UploadInput.mjs +43 -12
  55. package/build/uploadInput/UploadInput.mjs.map +1 -1
  56. package/build/uploadInput/uploadButton/UploadButton.js +14 -7
  57. package/build/uploadInput/uploadButton/UploadButton.js.map +1 -1
  58. package/build/uploadInput/uploadButton/UploadButton.mjs +15 -8
  59. package/build/uploadInput/uploadButton/UploadButton.mjs.map +1 -1
  60. package/build/uploadInput/uploadItem/UploadItem.js +18 -3
  61. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  62. package/build/uploadInput/uploadItem/UploadItem.mjs +18 -3
  63. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  64. package/build/uploadInput/uploadItem/UploadItemLink.js +6 -3
  65. package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
  66. package/build/uploadInput/uploadItem/UploadItemLink.mjs +6 -3
  67. package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
  68. package/package.json +3 -3
  69. package/src/card/Card.spec.tsx +4 -5
  70. package/src/card/Card.story.tsx +4 -6
  71. package/src/card/Card.tsx +3 -2
  72. package/src/circularButton/CircularButton.tsx +1 -1
  73. package/src/common/bottomSheet/BottomSheet.tsx +13 -4
  74. package/src/common/locale/index.ts +1 -1
  75. package/src/dateLookup/tableLink/TableLink.tsx +15 -15
  76. package/src/drawer/Drawer.tsx +7 -5
  77. package/src/flowNavigation/FlowNavigation.story.js +69 -17
  78. package/src/flowNavigation/FlowNavigation.tsx +1 -5
  79. package/src/flowNavigation/animatedLabel/AnimatedLabel.css +10 -1
  80. package/src/flowNavigation/animatedLabel/AnimatedLabel.less +10 -1
  81. package/src/flowNavigation/animatedLabel/AnimatedLabel.spec.js +64 -27
  82. package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +102 -20
  83. package/src/instructionsList/InstructionsList.tsx +1 -4
  84. package/src/main.css +10 -1
  85. package/src/switch/Switch.story.tsx +4 -7
  86. package/src/switch/Switch.tsx +1 -24
  87. package/src/switch/__snapshots__/Switch.spec.tsx.snap +2 -44
  88. package/src/uploadInput/UploadInput.tests.story.tsx +7 -3
  89. package/src/uploadInput/UploadInput.tsx +50 -8
  90. package/src/uploadInput/uploadButton/UploadButton.tsx +163 -141
  91. package/src/uploadInput/uploadItem/UploadItem.tsx +146 -124
  92. package/src/uploadInput/uploadItem/UploadItemLink.tsx +23 -25
  93. package/src/flowNavigation/animatedLabel/__snapshots__/AnimatedLabel.spec.js.snap +0 -25
@@ -1,6 +1,14 @@
1
1
  import { PlusCircle as PlusIcon, Upload as UploadIcon } from '@transferwise/icons';
2
2
  import { clsx } from 'clsx';
3
- import { ChangeEvent, DragEvent, useRef, useState } from 'react';
3
+ import {
4
+ ChangeEvent,
5
+ DragEvent,
6
+ useRef,
7
+ useState,
8
+ forwardRef,
9
+ useImperativeHandle,
10
+ ForwardedRef,
11
+ } from 'react';
4
12
  import { useIntl } from 'react-intl';
5
13
 
6
14
  import Body from '../../body';
@@ -70,169 +78,183 @@ const onDragOver = (event: DragEvent): void => {
70
78
  };
71
79
 
72
80
  const DEFAULT_FILE_INPUT_ID = 'np-upload-button';
73
- const UploadButton = ({
74
- disabled,
75
- multiple,
76
- description,
77
- fileTypes = imageFileTypes,
78
- sizeLimit = DEFAULT_SIZE_LIMIT,
79
- maxFiles,
80
- onChange,
81
- id = DEFAULT_FILE_INPUT_ID,
82
- uploadButtonTitle,
83
- }: UploadButtonProps) => {
84
- const { formatMessage } = useIntl();
85
- const inputReference = useRef<HTMLInputElement>(null);
86
-
87
- const [isDropping, setIsDropping] = useState(false);
88
-
89
- const dragCounter = useRef(0);
90
-
91
- const reset = (): void => {
92
- dragCounter.current = 0;
93
- setIsDropping(false);
94
- };
95
-
96
- const onDragLeave = (event: DragEvent): void => {
97
- event.preventDefault();
98
- dragCounter.current -= 1;
99
- if (dragCounter.current === 0) {
81
+ const UploadButton = forwardRef<HTMLInputElement | null, UploadButtonProps>(
82
+ (
83
+ {
84
+ disabled,
85
+ multiple,
86
+ description,
87
+ fileTypes = imageFileTypes,
88
+ sizeLimit = DEFAULT_SIZE_LIMIT,
89
+ maxFiles,
90
+ onChange,
91
+ id = DEFAULT_FILE_INPUT_ID,
92
+ uploadButtonTitle,
93
+ },
94
+ ref: ForwardedRef<HTMLInputElement | null>,
95
+ ) => {
96
+ const { formatMessage } = useIntl();
97
+ const inputRef = useRef<HTMLInputElement | null>(null);
98
+
99
+ useImperativeHandle(ref, () => {
100
+ if (!inputRef.current) {
101
+ throw new Error('inputRef.current is null');
102
+ }
103
+ return inputRef.current;
104
+ }, []);
105
+
106
+ const [isDropping, setIsDropping] = useState(false);
107
+
108
+ const dragCounter = useRef(0);
109
+
110
+ const reset = (): void => {
111
+ dragCounter.current = 0;
100
112
  setIsDropping(false);
101
- }
102
- };
113
+ };
103
114
 
104
- const onDragEnter = (event: DragEvent): void => {
105
- event.preventDefault();
106
- dragCounter.current += 1;
107
- if (dragCounter.current === 1) {
108
- setIsDropping(true);
109
- }
110
- };
115
+ const onDragLeave = (event: DragEvent): void => {
116
+ event.preventDefault();
117
+ dragCounter.current -= 1;
118
+ if (dragCounter.current === 0) {
119
+ setIsDropping(false);
120
+ }
121
+ };
111
122
 
112
- const onDrop = (event: DragEvent): void => {
113
- event.preventDefault();
114
- reset();
115
- if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
116
- onChange(event.dataTransfer.files);
117
- }
118
- };
123
+ const onDragEnter = (event: DragEvent): void => {
124
+ event.preventDefault();
125
+ dragCounter.current += 1;
126
+ if (dragCounter.current === 1) {
127
+ setIsDropping(true);
128
+ }
129
+ };
130
+
131
+ const onDrop = (event: DragEvent): void => {
132
+ event.preventDefault();
133
+ reset();
134
+ if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
135
+ onChange(event.dataTransfer.files);
136
+ }
137
+ };
119
138
 
120
- const filesSelected = (event: ChangeEvent<HTMLInputElement>): void => {
121
- const { files } = event.target;
139
+ const filesSelected = (event: ChangeEvent<HTMLInputElement>): void => {
140
+ const { files } = event.target;
122
141
 
123
- if (files) {
124
- onChange(files);
142
+ if (files) {
143
+ onChange(files);
125
144
 
126
- if (inputReference.current) {
127
- inputReference.current.value = '';
145
+ if (inputRef.current) {
146
+ inputRef.current.value = '';
147
+ }
128
148
  }
129
- }
130
- };
149
+ };
131
150
 
132
- const getFileTypesDescription = (): string => {
133
- if (fileTypes === '*') {
134
- return fileTypes;
135
- }
151
+ const getFileTypesDescription = (): string => {
152
+ if (fileTypes === '*') {
153
+ return fileTypes;
154
+ }
136
155
 
137
- return getAllowedFileTypes(Array.isArray(fileTypes) ? fileTypes : [fileTypes]).join(', ');
138
- };
156
+ return getAllowedFileTypes(Array.isArray(fileTypes) ? fileTypes : [fileTypes]).join(', ');
157
+ };
139
158
 
140
- function getDescription() {
141
- if (description) {
142
- return description;
143
- }
159
+ function getDescription() {
160
+ if (description) {
161
+ return description;
162
+ }
163
+
164
+ const fileTypesDescription = getFileTypesDescription();
144
165
 
145
- const fileTypesDescription = getFileTypesDescription();
166
+ const derivedFileDescription =
167
+ fileTypesDescription === '*' ? formatMessage(MESSAGES.allFileTypes) : fileTypesDescription;
146
168
 
147
- const derivedFileDescription =
148
- fileTypesDescription === '*' ? formatMessage(MESSAGES.allFileTypes) : fileTypesDescription;
169
+ return formatMessage(MESSAGES.instructions, {
170
+ fileTypes: derivedFileDescription,
171
+ size: Math.round(sizeLimit / 1000),
172
+ });
173
+ }
149
174
 
150
- return formatMessage(MESSAGES.instructions, {
151
- fileTypes: derivedFileDescription,
152
- size: Math.round(sizeLimit / 1000),
153
- });
154
- }
175
+ function getAcceptedTypes(): Pick<React.ComponentPropsWithoutRef<'input'>, 'accept'> {
176
+ const areAllFilesAllowed = getFileTypesDescription() === '*';
155
177
 
156
- function getAcceptedTypes(): Pick<React.ComponentPropsWithoutRef<'input'>, 'accept'> {
157
- const areAllFilesAllowed = getFileTypesDescription() === '*';
178
+ if (areAllFilesAllowed) {
179
+ return {}; // file input by default allows all files
180
+ }
158
181
 
159
- if (areAllFilesAllowed) {
160
- return {}; // file input by default allows all files
182
+ if (Array.isArray(fileTypes)) {
183
+ return { accept: fileTypes.join(',') };
184
+ }
185
+
186
+ return { accept: fileTypes as string };
161
187
  }
162
188
 
163
- if (Array.isArray(fileTypes)) {
164
- return { accept: fileTypes.join(',') };
189
+ function renderDescription() {
190
+ return (
191
+ <Body className={clsx({ 'text-primary': !disabled })}>
192
+ {getDescription()}
193
+ {maxFiles && (
194
+ <>
195
+ <br />
196
+ {`Maximum ${maxFiles} files.`}
197
+ </>
198
+ )}
199
+ </Body>
200
+ );
165
201
  }
166
202
 
167
- return { accept: fileTypes as string };
168
- }
203
+ function renderButtonTitle() {
204
+ if (uploadButtonTitle) {
205
+ return uploadButtonTitle;
206
+ }
207
+ return formatMessage(multiple ? MESSAGES.uploadFiles : MESSAGES.uploadFile);
208
+ }
169
209
 
170
- function renderDescription() {
171
210
  return (
172
- <Body className={clsx({ 'text-primary': !disabled })}>
173
- {getDescription()}
174
- {maxFiles && (
175
- <>
176
- <br />
177
- {`Maximum ${maxFiles} files.`}
178
- </>
211
+ <div
212
+ className={clsx('np-upload-button-container', 'droppable', {
213
+ 'droppable-dropping': isDropping,
214
+ })}
215
+ {...(!disabled && { onDragEnter, onDragLeave, onDrop, onDragOver })}
216
+ >
217
+ <input
218
+ ref={inputRef}
219
+ id={id}
220
+ type="file"
221
+ {...getAcceptedTypes()}
222
+ {...(multiple && { multiple: true })}
223
+ className="tw-droppable-input"
224
+ disabled={disabled}
225
+ name="file-upload"
226
+ data-testid={TEST_IDS.uploadInput}
227
+ onChange={filesSelected}
228
+ />
229
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
230
+ <label htmlFor={id} className={clsx('btn', 'np-upload-button')}>
231
+ <div className="media">
232
+ <div className="np-upload-icon media-middle media-left">
233
+ <UploadIcon size={24} className="text-link" />
234
+ </div>
235
+ <div className="media-body text-xs-left" data-testid={TEST_IDS.mediaBody}>
236
+ <Body type={Typography.BODY_LARGE_BOLD} className="d-block">
237
+ {renderButtonTitle()}
238
+ </Body>
239
+ {renderDescription()}
240
+ </div>
241
+ </div>
242
+ </label>
243
+
244
+ {/* Drop area overlay */}
245
+ {isDropping && (
246
+ <div
247
+ className={clsx('droppable-card', 'droppable-dropping-card', 'droppable-card-content')}
248
+ >
249
+ <PlusIcon className="m-x-1" size={24} />
250
+ <div>{formatMessage(MESSAGES.dropFile)}</div>
251
+ </div>
179
252
  )}
180
- </Body>
253
+ </div>
181
254
  );
182
- }
255
+ },
256
+ );
183
257
 
184
- function renderButtonTitle() {
185
- if (uploadButtonTitle) {
186
- return uploadButtonTitle;
187
- }
188
- return formatMessage(multiple ? MESSAGES.uploadFiles : MESSAGES.uploadFile);
189
- }
190
-
191
- return (
192
- <div
193
- className={clsx('np-upload-button-container', 'droppable', {
194
- 'droppable-dropping': isDropping,
195
- })}
196
- {...(!disabled && { onDragEnter, onDragLeave, onDrop, onDragOver })}
197
- >
198
- <input
199
- ref={inputReference}
200
- id={id}
201
- type="file"
202
- {...getAcceptedTypes()}
203
- {...(multiple && { multiple: true })}
204
- className="tw-droppable-input"
205
- disabled={disabled}
206
- name="file-upload"
207
- data-testid={TEST_IDS.uploadInput}
208
- onChange={filesSelected}
209
- />
210
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
211
- <label htmlFor={id} className={clsx('btn', 'np-upload-button')}>
212
- <div className="media">
213
- <div className="np-upload-icon media-middle media-left">
214
- <UploadIcon size={24} className="text-link" />
215
- </div>
216
- <div className="media-body text-xs-left" data-testid={TEST_IDS.mediaBody}>
217
- <Body type={Typography.BODY_LARGE_BOLD} className="d-block">
218
- {renderButtonTitle()}
219
- </Body>
220
- {renderDescription()}
221
- </div>
222
- </div>
223
- </label>
224
-
225
- {/* Drop area overlay */}
226
- {isDropping && (
227
- <div
228
- className={clsx('droppable-card', 'droppable-dropping-card', 'droppable-card-content')}
229
- >
230
- <PlusIcon className="m-x-1" size={24} />
231
- <div>{formatMessage(MESSAGES.dropFile)}</div>
232
- </div>
233
- )}
234
- </div>
235
- );
236
- };
258
+ UploadButton.displayName = 'UploadButton';
237
259
 
238
260
  export default UploadButton;
@@ -1,5 +1,6 @@
1
1
  import { Bin, CheckCircleFill, CrossCircleFill } from '@transferwise/icons';
2
2
  import { clsx } from 'clsx';
3
+ import { forwardRef, useImperativeHandle, useRef } from 'react';
3
4
  import { useIntl } from 'react-intl';
4
5
 
5
6
  import Body from '../../body';
@@ -27,145 +28,166 @@ export type UploadItemProps = React.JSX.IntrinsicAttributes & {
27
28
  * @param file
28
29
  */
29
30
  onDownload?: (file: UploadedFile) => void;
31
+ ref?: React.Ref<UploadItemRef>;
30
32
  };
31
33
 
34
+ interface UploadItemRef {
35
+ focus: () => void;
36
+ }
37
+
32
38
  export enum TEST_IDS {
33
39
  uploadItem = 'uploadItem',
34
40
  mediaBody = 'mediaBody',
35
41
  }
36
42
 
37
- const UploadItem = ({
38
- file,
39
- canDelete,
40
- onDelete,
41
- onDownload,
42
- singleFileUpload,
43
- }: UploadItemProps) => {
44
- const { formatMessage } = useIntl();
45
- const { status, filename, error, errors, url } = file;
46
-
47
- const isSucceeded = [Status.SUCCEEDED, undefined].includes(status) && !!url;
48
-
49
- /**
50
- * We're temporarily reverting to the regular icon components,
51
- * until the StatusIcon receives 24px sizing. Some misalignment
52
- * to be expected.
53
- */
54
- const getIcon = () => {
55
- if (error || errors?.length || status === Status.FAILED) {
56
- return <CrossCircleFill size={24} className="emphasis--negative" />;
57
- }
58
-
59
- let processIndicator: React.ReactNode;
60
-
61
- switch (status) {
62
- case Status.PROCESSING:
63
- case Status.PENDING:
64
- processIndicator = <ProcessIndicator size={Size.EXTRA_SMALL} status={Status.PROCESSING} />;
65
- break;
66
- case Status.SUCCEEDED:
67
- case Status.DONE:
68
- default:
69
- processIndicator = <CheckCircleFill size={24} className="emphasis--positive" />;
70
- }
71
-
72
- return processIndicator;
73
- };
74
-
75
- const getErrorMessage = (error?: UploadError) =>
76
- typeof error === 'object' ? error.message : error || formatMessage(MESSAGES.uploadingFailed);
77
-
78
- const getMultipleErrors = (errors?: UploadError[]) => {
79
- if (!errors?.length) {
80
- return null;
81
- }
82
-
83
- if (errors?.length === 1) {
84
- return getErrorMessage(errors[0]);
85
- }
86
-
87
- return (
88
- <ul className="np-upload-input-errors m-b-0">
89
- {errors.map((error, index) => {
90
- // eslint-disable-next-line react/no-array-index-key
91
- return <li key={index}>{getErrorMessage(error)}</li>;
92
- })}
93
- </ul>
94
- );
95
- };
43
+ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
44
+ ({ file, canDelete, onDelete, onDownload, singleFileUpload }, ref) => {
45
+ const { formatMessage } = useIntl();
46
+ const { status, filename, error, errors, url } = file;
47
+ const linkRef = useRef<HTMLAnchorElement>(null);
48
+ const buttonRef = useRef<HTMLButtonElement>(null);
49
+
50
+ useImperativeHandle<UploadItemRef, UploadItemRef>(ref, () => ({
51
+ focus: (): void => {
52
+ if (url) {
53
+ linkRef.current?.focus();
54
+ } else {
55
+ buttonRef.current?.focus();
56
+ }
57
+ },
58
+ }));
59
+
60
+ const isSucceeded = [Status.SUCCEEDED, undefined].includes(status) && !!url;
61
+
62
+ /**
63
+ * We're temporarily reverting to the regular icon components,
64
+ * until the StatusIcon receives 24px sizing. Some misalignment
65
+ * to be expected.
66
+ */
67
+ const getIcon = () => {
68
+ if (error || errors?.length || status === Status.FAILED) {
69
+ return <CrossCircleFill size={24} className="emphasis--negative" />;
70
+ }
71
+
72
+ let processIndicator: React.ReactNode;
73
+
74
+ switch (status) {
75
+ case Status.PROCESSING:
76
+ case Status.PENDING:
77
+ processIndicator = (
78
+ <ProcessIndicator size={Size.EXTRA_SMALL} status={Status.PROCESSING} />
79
+ );
80
+ break;
81
+ case Status.SUCCEEDED:
82
+ case Status.DONE:
83
+ default:
84
+ processIndicator = <CheckCircleFill size={24} className="emphasis--positive" />;
85
+ }
86
+
87
+ return processIndicator;
88
+ };
89
+
90
+ const getErrorMessage = (error?: UploadError) =>
91
+ typeof error === 'object' ? error.message : error || formatMessage(MESSAGES.uploadingFailed);
92
+
93
+ const getMultipleErrors = (errors?: UploadError[]) => {
94
+ if (!errors?.length) {
95
+ return null;
96
+ }
97
+
98
+ if (errors?.length === 1) {
99
+ return getErrorMessage(errors[0]);
100
+ }
96
101
 
97
- const getDescription = () => {
98
- if (error || errors?.length || status === Status.FAILED) {
99
102
  return (
100
- <Body type={Typography.BODY_DEFAULT_BOLD} className="text-negative">
101
- {errors?.length ? getMultipleErrors(errors) : getErrorMessage(error)}
102
- </Body>
103
+ <ul className="np-upload-input-errors m-b-0">
104
+ {errors.map((error, index) => {
105
+ // eslint-disable-next-line react/no-array-index-key
106
+ return <li key={index}>{getErrorMessage(error)}</li>;
107
+ })}
108
+ </ul>
103
109
  );
104
- }
105
-
106
- switch (status) {
107
- case Status.PENDING:
108
- return <Body type={Typography.BODY_DEFAULT_BOLD}>{formatMessage(MESSAGES.uploading)}</Body>;
109
- case Status.PROCESSING:
110
- return <Body>{formatMessage(MESSAGES.deleting)}</Body>;
111
- case Status.SUCCEEDED:
112
- case Status.DONE:
113
- default:
110
+ };
111
+
112
+ const getDescription = () => {
113
+ if (error || errors?.length || status === Status.FAILED) {
114
114
  return (
115
- <Body type={Typography.BODY_DEFAULT_BOLD} className="text-positive">
116
- {formatMessage(MESSAGES.uploaded)}
115
+ <Body type={Typography.BODY_DEFAULT_BOLD} className="text-negative">
116
+ {errors?.length ? getMultipleErrors(errors) : getErrorMessage(error)}
117
117
  </Body>
118
118
  );
119
- }
120
- };
121
-
122
- const getTitle = () => {
123
- return filename || formatMessage(MESSAGES.uploadedFile);
124
- };
125
-
126
- const onDownloadFile = (event: React.MouseEvent): void => {
127
- if (onDownload) {
128
- event.preventDefault();
129
- onDownload(file);
130
- }
131
- };
132
-
133
- return (
134
- <div
135
- className={clsx('np-upload-item', { 'np-upload-item--link': isSucceeded })}
136
- data-testid={TEST_IDS.uploadItem}
137
- >
138
- <div className="np-upload-item__body">
139
- <UploadItemLink
140
- url={isSucceeded ? url : undefined}
141
- singleFileUpload={singleFileUpload}
142
- onDownload={onDownloadFile}
143
- >
144
- <div className="np-upload-button" aria-live="polite">
145
- <div className="media">
146
- <div className="np-upload-icon media-left">{getIcon()}</div>
147
- <div className="media-body text-xs-left" data-testid={TEST_IDS.mediaBody}>
148
- <Body className="text-word-break d-block text-primary">{getTitle()}</Body>
149
- {getDescription()}
119
+ }
120
+
121
+ switch (status) {
122
+ case Status.PENDING:
123
+ return (
124
+ <Body type={Typography.BODY_DEFAULT_BOLD}>{formatMessage(MESSAGES.uploading)}</Body>
125
+ );
126
+ case Status.PROCESSING:
127
+ return <Body>{formatMessage(MESSAGES.deleting)}</Body>;
128
+ case Status.SUCCEEDED:
129
+ case Status.DONE:
130
+ default:
131
+ return (
132
+ <Body type={Typography.BODY_DEFAULT_BOLD} className="text-positive">
133
+ {formatMessage(MESSAGES.uploaded)}
134
+ </Body>
135
+ );
136
+ }
137
+ };
138
+
139
+ const getTitle = () => {
140
+ return filename || formatMessage(MESSAGES.uploadedFile);
141
+ };
142
+
143
+ const onDownloadFile = (event: React.MouseEvent): void => {
144
+ if (onDownload) {
145
+ event.preventDefault();
146
+ onDownload(file);
147
+ }
148
+ };
149
+
150
+ return (
151
+ <div
152
+ className={clsx('np-upload-item', { 'np-upload-item--link': isSucceeded })}
153
+ data-testid={TEST_IDS.uploadItem}
154
+ >
155
+ <div className="np-upload-item__body">
156
+ <UploadItemLink
157
+ ref={linkRef}
158
+ url={isSucceeded ? url : undefined}
159
+ singleFileUpload={singleFileUpload}
160
+ onDownload={onDownloadFile}
161
+ >
162
+ <div className="np-upload-button" aria-live="polite">
163
+ <div className="media">
164
+ <div className="np-upload-icon media-left">{getIcon()}</div>
165
+ <div className="media-body text-xs-left" data-testid={TEST_IDS.mediaBody}>
166
+ <Body className="text-word-break d-block text-primary">{getTitle()}</Body>
167
+ {getDescription()}
168
+ </div>
150
169
  </div>
151
170
  </div>
152
- </div>
153
- </UploadItemLink>
154
- {canDelete && (
155
- <button
156
- aria-label={formatMessage(MESSAGES.removeFile, { filename })}
157
- className={clsx('btn', 'np-upload-item__remove-button', 'media-left', {
158
- 'np-upload-item--single-file': singleFileUpload,
159
- })}
160
- type="button"
161
- onClick={() => onDelete()}
162
- >
163
- <Bin size={16} />
164
- </button>
165
- )}
171
+ </UploadItemLink>
172
+ {canDelete && (
173
+ <button
174
+ ref={buttonRef}
175
+ aria-label={formatMessage(MESSAGES.removeFile, { filename })}
176
+ className={clsx('btn', 'np-upload-item__remove-button', 'media-left', {
177
+ 'np-upload-item--single-file': singleFileUpload,
178
+ })}
179
+ type="button"
180
+ onClick={() => onDelete()}
181
+ >
182
+ <Bin size={16} />
183
+ </button>
184
+ )}
185
+ </div>
166
186
  </div>
167
- </div>
168
- );
169
- };
187
+ );
188
+ },
189
+ );
190
+
191
+ UploadItem.displayName = 'UploadItem';
170
192
 
171
193
  export default UploadItem;