@transferwise/components 46.74.0 → 46.74.1
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/build/dimmer/Dimmer.js +3 -1
- package/build/dimmer/Dimmer.js.map +1 -1
- package/build/dimmer/Dimmer.mjs +3 -1
- package/build/dimmer/Dimmer.mjs.map +1 -1
- package/build/drawer/Drawer.js +2 -0
- package/build/drawer/Drawer.js.map +1 -1
- package/build/drawer/Drawer.mjs +2 -0
- package/build/drawer/Drawer.mjs.map +1 -1
- package/build/modal/Modal.js +3 -0
- package/build/modal/Modal.js.map +1 -1
- package/build/modal/Modal.mjs +3 -0
- package/build/modal/Modal.mjs.map +1 -1
- package/build/types/dimmer/Dimmer.d.ts +2 -1
- package/build/types/dimmer/Dimmer.d.ts.map +1 -1
- package/build/types/drawer/Drawer.d.ts +2 -1
- package/build/types/drawer/Drawer.d.ts.map +1 -1
- package/build/types/modal/Modal.d.ts +2 -1
- package/build/types/modal/Modal.d.ts.map +1 -1
- package/build/types/uploadInput/UploadInput.d.ts +9 -0
- package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts +16 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
- package/build/uploadInput/UploadInput.js +71 -66
- package/build/uploadInput/UploadInput.js.map +1 -1
- package/build/uploadInput/UploadInput.mjs +72 -67
- package/build/uploadInput/UploadInput.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.js +13 -4
- package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.mjs +13 -4
- package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.js +1 -0
- package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.mjs +1 -0
- package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
- package/package.json +2 -2
- package/src/dimmer/Dimmer.spec.js +8 -0
- package/src/dimmer/Dimmer.tsx +4 -0
- package/src/drawer/Drawer.spec.js +25 -6
- package/src/drawer/Drawer.tsx +3 -1
- package/src/modal/Modal.spec.js +19 -1
- package/src/modal/Modal.tsx +4 -0
- package/src/uploadInput/UploadInput.spec.tsx +121 -9
- package/src/uploadInput/UploadInput.tests.story.tsx +207 -140
- package/src/uploadInput/UploadInput.tsx +110 -77
- package/src/uploadInput/uploadItem/UploadItem.spec.tsx +1 -0
- package/src/uploadInput/uploadItem/UploadItem.tsx +30 -6
- package/src/uploadInput/uploadItem/UploadItemLink.tsx +9 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
-
import { useEffect, useRef, useState
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useIntl } from 'react-intl';
|
|
4
4
|
|
|
5
5
|
import Button from '../button';
|
|
@@ -99,16 +99,45 @@ export type UploadInputProps = {
|
|
|
99
99
|
'disabled' | 'multiple' | 'fileTypes' | 'sizeLimit' | 'description' | 'id' | 'uploadButtonTitle'
|
|
100
100
|
> & { onDownload?: UploadItemProps['onDownload'] } & CommonProps;
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Interface representing a reference to an UploadItem component.
|
|
104
|
+
* Provides a method to focus the UploadItem.
|
|
105
|
+
*/
|
|
102
106
|
interface UploadItemRef {
|
|
107
|
+
/**
|
|
108
|
+
* Focuses the UploadItem component.
|
|
109
|
+
*/
|
|
103
110
|
focus: () => void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Required id of the UploadItem component.
|
|
114
|
+
*/
|
|
115
|
+
id: string | number;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Optional status of the UploadItem component.
|
|
119
|
+
*/
|
|
120
|
+
status?: string;
|
|
104
121
|
}
|
|
105
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Generates a unique ID for a file based on its name, size, and the current timestamp
|
|
125
|
+
*/
|
|
106
126
|
function generateFileId(file: File) {
|
|
107
127
|
const { name, size } = file;
|
|
108
128
|
const uploadTimeStamp = new Date().getTime();
|
|
109
129
|
return `${name}_${size}_${uploadTimeStamp}`;
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The component allows users to upload files, manage the list of uploaded files,
|
|
134
|
+
* and handle file validation and deletion.
|
|
135
|
+
*
|
|
136
|
+
* @param {UploadInputProps} props - The properties for the UploadInput component.
|
|
137
|
+
*
|
|
138
|
+
* @see {@link UploadInput} for further information.
|
|
139
|
+
* @see {@link https://storybook.wise.design/?path=/docs/forms-uploadinput--docs|Storybook Wise Design}
|
|
140
|
+
*/
|
|
112
141
|
const UploadInput = ({
|
|
113
142
|
files = [],
|
|
114
143
|
fileInputName = 'file',
|
|
@@ -131,13 +160,11 @@ const UploadInput = ({
|
|
|
131
160
|
uploadButtonTitle,
|
|
132
161
|
}: UploadInputProps) => {
|
|
133
162
|
const inputAttributes = useInputAttributes({ nonLabelable: true });
|
|
134
|
-
|
|
135
163
|
const [markedFileForDelete, setMarkedFileForDelete] = useState<UploadedFile | null>(null);
|
|
136
|
-
const [fileToRemoveIndex, setFileToRemoveIndex] = useState<number | null>(null);
|
|
137
164
|
const [mounted, setMounted] = useState(false);
|
|
138
165
|
const { formatMessage } = useIntl();
|
|
139
|
-
const itemRefs = useRef<(HTMLDivElement | UploadItemRef | null)[]>([]);
|
|
140
166
|
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
167
|
+
let fileRefs: (HTMLDivElement | UploadItemRef | null)[] = [];
|
|
141
168
|
|
|
142
169
|
const PROGRESS_STATUSES = new Set([Status.PENDING, Status.PROCESSING]);
|
|
143
170
|
|
|
@@ -147,69 +174,57 @@ const UploadInput = ({
|
|
|
147
174
|
|
|
148
175
|
const uploadedFilesListReference = useRef(multiple || files.length === 0 ? files : [files[0]]);
|
|
149
176
|
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
setUploadedFiles(addToList);
|
|
156
|
-
uploadedFilesListReference.current = addToList(uploadedFilesListReference.current);
|
|
177
|
+
function updateFileList(updateFn: (list: readonly UploadedFile[]) => readonly UploadedFile[]) {
|
|
178
|
+
setUploadedFiles(updateFn);
|
|
179
|
+
uploadedFilesListReference.current = updateFn(uploadedFilesListReference.current);
|
|
157
180
|
}
|
|
158
181
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
(fileInList) => file !== fileInList && file.id !== fileInList.id,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
setUploadedFiles(filterOutFrom);
|
|
167
|
-
uploadedFilesListReference.current = filterOutFrom(uploadedFilesListReference.current);
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const modifyFileInList = (file: UploadedFile, updates: Partial<UploadedFile>) => {
|
|
171
|
-
const updateListItem = (listToUpdate: readonly UploadedFile[]) =>
|
|
172
|
-
listToUpdate.map((fileInList) => {
|
|
173
|
-
return fileInList === file || fileInList.id === file.id
|
|
174
|
-
? { ...file, ...updates }
|
|
175
|
-
: fileInList;
|
|
176
|
-
});
|
|
182
|
+
function addFileToList(recentUploadedFile: UploadedFile) {
|
|
183
|
+
updateFileList((list) => [...list, recentUploadedFile]);
|
|
184
|
+
}
|
|
177
185
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
function removeFileFromList(file: UploadedFile) {
|
|
187
|
+
updateFileList((list) =>
|
|
188
|
+
list.filter((fileInList) => file !== fileInList && file.id !== fileInList.id),
|
|
189
|
+
);
|
|
190
|
+
fileRefs = fileRefs.filter((ref) => ref && ref.id !== file.id);
|
|
191
|
+
}
|
|
181
192
|
|
|
182
|
-
|
|
193
|
+
function modifyFileInList(file: UploadedFile, updates: Partial<UploadedFile>) {
|
|
194
|
+
updateFileList((list) =>
|
|
195
|
+
list.map((fileInList) =>
|
|
196
|
+
fileInList === file || fileInList.id === file.id ? { ...file, ...updates } : fileInList,
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
183
200
|
|
|
184
|
-
const removeFile = (file: UploadedFile) => {
|
|
201
|
+
const removeFile = async (file: UploadedFile) => {
|
|
185
202
|
const { id, status } = file;
|
|
186
|
-
|
|
187
|
-
setFileToRemoveIndex(index);
|
|
203
|
+
fileRefs = fileRefs.filter((item) => item && item.id !== file.id);
|
|
188
204
|
|
|
189
205
|
if (status === Status.FAILED) {
|
|
190
206
|
removeFileFromList(file);
|
|
191
|
-
|
|
192
|
-
}
|
|
207
|
+
return Promise.resolve();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (onDeleteFile && id) {
|
|
193
211
|
modifyFileInList(file, { status: Status.PROCESSING, error: undefined });
|
|
194
212
|
|
|
195
|
-
onDeleteFile(id)
|
|
213
|
+
return onDeleteFile(id)
|
|
196
214
|
.then(() => {
|
|
197
215
|
removeFileFromList(file);
|
|
198
216
|
})
|
|
199
217
|
.catch((error) => {
|
|
200
218
|
modifyFileInList(file, { error: error as UploadError });
|
|
201
|
-
})
|
|
202
|
-
.finally(() => {
|
|
203
|
-
setFileToRemove(file);
|
|
204
219
|
});
|
|
205
220
|
}
|
|
206
221
|
};
|
|
207
222
|
|
|
208
223
|
function handleFileUploadFailure(file: File, failureMessage: string) {
|
|
209
224
|
const { name } = file;
|
|
210
|
-
|
|
225
|
+
|
|
211
226
|
const failedUpload = {
|
|
212
|
-
id,
|
|
227
|
+
id: generateFileId(file),
|
|
213
228
|
filename: name,
|
|
214
229
|
status: Status.FAILED,
|
|
215
230
|
error: failureMessage,
|
|
@@ -239,28 +254,20 @@ const UploadInput = ({
|
|
|
239
254
|
return numberOfValidFiles >= maxFiles;
|
|
240
255
|
}
|
|
241
256
|
|
|
242
|
-
// One or more files selected, create entries for them
|
|
243
257
|
const addFiles = (selectedFiles: FileList) => {
|
|
244
258
|
for (let i = 0; i < selectedFiles.length; i += 1) {
|
|
245
259
|
const file = selectedFiles.item(i);
|
|
246
260
|
|
|
247
|
-
// Returning a FormData[] array instead of FileList so we can filter out incorrect files
|
|
248
261
|
const formData = new FormData();
|
|
249
262
|
|
|
250
263
|
if (file) {
|
|
251
|
-
const { name } = file;
|
|
252
|
-
const id = generateFileId(file);
|
|
253
|
-
|
|
254
264
|
const allowedFileTypes = typeof fileTypes === 'string' ? fileTypes : fileTypes.join(',');
|
|
255
265
|
|
|
256
|
-
// Check if file type is valid
|
|
257
266
|
if (!isTypeValid(file, allowedFileTypes)) {
|
|
258
267
|
handleFileUploadFailure(file, formatMessage(MESSAGES.fileTypeNotSupported));
|
|
259
268
|
continue;
|
|
260
269
|
}
|
|
261
270
|
|
|
262
|
-
// Check if the filesize is valid
|
|
263
|
-
// Convert to rough bytes
|
|
264
271
|
if (!isSizeValid(file, sizeLimit * 1000)) {
|
|
265
272
|
const failureMessage = sizeLimitErrorMessage || formatMessage(MESSAGES.fileIsTooLarge);
|
|
266
273
|
handleFileUploadFailure(file, failureMessage);
|
|
@@ -275,14 +282,11 @@ const UploadInput = ({
|
|
|
275
282
|
continue;
|
|
276
283
|
}
|
|
277
284
|
|
|
278
|
-
// Check if the file is already in the list
|
|
279
285
|
const existingFile = uploadedFiles.find((f) => f.filename === file.name);
|
|
280
286
|
if (existingFile) {
|
|
281
|
-
// Remove the file from the list before adding it again
|
|
282
287
|
removeFileFromList(existingFile);
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
// Add the file to the list
|
|
286
290
|
formData.append(fileInputName, file);
|
|
287
291
|
const pendingFile = {
|
|
288
292
|
id: generateFileId(file),
|
|
@@ -292,10 +296,8 @@ const UploadInput = ({
|
|
|
292
296
|
|
|
293
297
|
addFileToList(pendingFile);
|
|
294
298
|
|
|
295
|
-
// Start uploading the file
|
|
296
299
|
onUploadFile(formData)
|
|
297
300
|
.then(({ id, url, error }: UploadResponse) => {
|
|
298
|
-
// Replace the temporary id with the final one received from the API, and also set any errors
|
|
299
301
|
modifyFileInList(pendingFile, { id, url, error, status: Status.SUCCEEDED });
|
|
300
302
|
})
|
|
301
303
|
.catch((error) => {
|
|
@@ -303,29 +305,12 @@ const UploadInput = ({
|
|
|
303
305
|
});
|
|
304
306
|
|
|
305
307
|
if (!multiple) {
|
|
306
|
-
// Only upload a single file
|
|
307
308
|
break;
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
};
|
|
312
313
|
|
|
313
|
-
useLayoutEffect(() => {
|
|
314
|
-
if (fileToRemove && fileToRemoveIndex !== null) {
|
|
315
|
-
requestAnimationFrame(() => {
|
|
316
|
-
const nextFocusIndex = Math.min(fileToRemoveIndex, uploadedFiles.length - 1);
|
|
317
|
-
if (itemRefs.current[nextFocusIndex]) {
|
|
318
|
-
itemRefs.current[nextFocusIndex].focus(); // Focus the next UploadItem
|
|
319
|
-
} else {
|
|
320
|
-
// If there's only one item left, focus the UploadButton
|
|
321
|
-
uploadInputRef.current?.focus();
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
setFileToRemove(null); // Reset the state
|
|
325
|
-
setFileToRemoveIndex(null); // Reset the index
|
|
326
|
-
}
|
|
327
|
-
}, [uploadedFiles, fileToRemove, fileToRemoveIndex, itemRefs, uploadInputRef]);
|
|
328
|
-
|
|
329
314
|
useEffect(() => {
|
|
330
315
|
setMounted(true);
|
|
331
316
|
}, []);
|
|
@@ -336,6 +321,41 @@ const UploadInput = ({
|
|
|
336
321
|
}
|
|
337
322
|
}, [onFilesChange, uploadedFiles]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
338
323
|
|
|
324
|
+
const [nextFocusable, setNextFocusable] = useState<HTMLDivElement | UploadItemRef | null>(
|
|
325
|
+
uploadInputRef.current,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const handleFocus = (fileId: string | number) => {
|
|
329
|
+
fileRefs = fileRefs.filter((ref) => {
|
|
330
|
+
return ref && ref.id !== markedFileForDelete?.id;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const filesCount = fileRefs.length;
|
|
334
|
+
let next: HTMLDivElement | UploadItemRef | null = uploadInputRef.current;
|
|
335
|
+
|
|
336
|
+
if (filesCount > 1) {
|
|
337
|
+
const currentFileIndex = fileRefs.findIndex((file) => file?.id === fileId);
|
|
338
|
+
const currentFileId = fileRefs?.[currentFileIndex]?.id;
|
|
339
|
+
const lastFileId = fileRefs?.[filesCount - 1]?.id;
|
|
340
|
+
|
|
341
|
+
// if last file, select a previous one
|
|
342
|
+
if (currentFileId === lastFileId) {
|
|
343
|
+
next = fileRefs[filesCount - 2];
|
|
344
|
+
} else {
|
|
345
|
+
next = fileRefs[currentFileIndex + 1];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
setNextFocusable(next);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const handleRefocus = () => {
|
|
352
|
+
if (nextFocusable && 'focus' in nextFocusable && typeof nextFocusable.focus === 'function') {
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
nextFocusable.focus();
|
|
355
|
+
}, 0);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
339
359
|
return (
|
|
340
360
|
<>
|
|
341
361
|
<div
|
|
@@ -353,7 +373,14 @@ const UploadInput = ({
|
|
|
353
373
|
<UploadItem
|
|
354
374
|
key={file.id}
|
|
355
375
|
ref={(el: UploadItemRef | null) => {
|
|
356
|
-
|
|
376
|
+
if (
|
|
377
|
+
el &&
|
|
378
|
+
el.id !== markedFileForDelete?.id &&
|
|
379
|
+
!fileRefs.some((ref) => ref && ref.id === el.id) &&
|
|
380
|
+
el.status !== 'processing'
|
|
381
|
+
) {
|
|
382
|
+
fileRefs.push(el);
|
|
383
|
+
}
|
|
357
384
|
}}
|
|
358
385
|
file={file}
|
|
359
386
|
singleFileUpload={!multiple}
|
|
@@ -363,10 +390,14 @@ const UploadInput = ({
|
|
|
363
390
|
}
|
|
364
391
|
onDelete={
|
|
365
392
|
file.status === Status.FAILED
|
|
366
|
-
? () =>
|
|
393
|
+
? async () => {
|
|
394
|
+
await removeFile(file);
|
|
395
|
+
handleRefocus();
|
|
396
|
+
}
|
|
367
397
|
: () => setMarkedFileForDelete(file)
|
|
368
398
|
}
|
|
369
399
|
onDownload={onDownload}
|
|
400
|
+
onFocus={() => handleFocus(file.id)}
|
|
370
401
|
/>
|
|
371
402
|
))}
|
|
372
403
|
</div>
|
|
@@ -414,9 +445,10 @@ const UploadInput = ({
|
|
|
414
445
|
block
|
|
415
446
|
priority={Priority.SECONDARY}
|
|
416
447
|
type={ControlType.NEGATIVE}
|
|
448
|
+
tabIndex={markedFileForDelete ? 0 : -1}
|
|
417
449
|
onClick={() => {
|
|
418
450
|
if (markedFileForDelete) {
|
|
419
|
-
removeFile(markedFileForDelete);
|
|
451
|
+
void removeFile(markedFileForDelete);
|
|
420
452
|
}
|
|
421
453
|
setMarkedFileForDelete(null);
|
|
422
454
|
}}
|
|
@@ -425,6 +457,7 @@ const UploadInput = ({
|
|
|
425
457
|
</Button>
|
|
426
458
|
</>
|
|
427
459
|
}
|
|
460
|
+
onUnmount={handleRefocus}
|
|
428
461
|
onClose={() => {
|
|
429
462
|
setMarkedFileForDelete(null);
|
|
430
463
|
}}
|
|
@@ -4,9 +4,8 @@ import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
|
4
4
|
import { useIntl } from 'react-intl';
|
|
5
5
|
|
|
6
6
|
import Body from '../../body';
|
|
7
|
-
import { Size, Status, Typography
|
|
7
|
+
import { Size, Status, Typography } from '../../common';
|
|
8
8
|
import ProcessIndicator from '../../processIndicator/ProcessIndicator';
|
|
9
|
-
import StatusIcon from '../../statusIcon/StatusIcon';
|
|
10
9
|
import { UploadedFile, UploadError } from '../types';
|
|
11
10
|
|
|
12
11
|
import MESSAGES from './UploadItem.messages';
|
|
@@ -20,6 +19,7 @@ export type UploadItemProps = React.JSX.IntrinsicAttributes & {
|
|
|
20
19
|
singleFileUpload: boolean;
|
|
21
20
|
canDelete: boolean;
|
|
22
21
|
onDelete: () => void;
|
|
22
|
+
onFocus: () => void;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Callback to be called when the file link is clicked.
|
|
@@ -30,18 +30,33 @@ export type UploadItemProps = React.JSX.IntrinsicAttributes & {
|
|
|
30
30
|
onDownload?: (file: UploadedFile) => void;
|
|
31
31
|
ref?: React.Ref<UploadItemRef>;
|
|
32
32
|
};
|
|
33
|
-
|
|
34
33
|
interface UploadItemRef {
|
|
34
|
+
/**
|
|
35
|
+
* A method to set focus on the upload item.
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
35
38
|
focus: () => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A required unique identifier for the upload item.
|
|
42
|
+
*/
|
|
43
|
+
id: string | number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An optional status of the upload item.
|
|
47
|
+
*/
|
|
48
|
+
status?: string;
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
export enum TEST_IDS {
|
|
39
52
|
uploadItem = 'uploadItem',
|
|
40
53
|
mediaBody = 'mediaBody',
|
|
54
|
+
link = 'link',
|
|
55
|
+
action = 'action',
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
|
|
44
|
-
({ file, canDelete, onDelete, onDownload, singleFileUpload }, ref) => {
|
|
59
|
+
({ file, canDelete, onDelete, onDownload, singleFileUpload, onFocus: handleFocus }, ref) => {
|
|
45
60
|
const { formatMessage } = useIntl();
|
|
46
61
|
const { status, filename, error, errors, url } = file;
|
|
47
62
|
const linkRef = useRef<HTMLAnchorElement>(null);
|
|
@@ -55,6 +70,8 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
|
|
|
55
70
|
buttonRef.current?.focus();
|
|
56
71
|
}
|
|
57
72
|
},
|
|
73
|
+
id: file.id,
|
|
74
|
+
status: file.status,
|
|
58
75
|
}));
|
|
59
76
|
|
|
60
77
|
const isSucceeded = [Status.SUCCEEDED, undefined].includes(status) && !!url;
|
|
@@ -152,16 +169,20 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
|
|
|
152
169
|
return (
|
|
153
170
|
<div
|
|
154
171
|
className={clsx('np-upload-input__item', { 'is-interactive': isSucceeded && url })}
|
|
155
|
-
data-testid={TEST_IDS.uploadItem}
|
|
172
|
+
data-testid={`${file.id}-${TEST_IDS.uploadItem}`}
|
|
156
173
|
>
|
|
157
174
|
<UploadItemLink
|
|
158
175
|
ref={linkRef}
|
|
159
176
|
url={isSucceeded ? url : undefined}
|
|
160
177
|
singleFileUpload={singleFileUpload}
|
|
178
|
+
data-testid={`${file.id}-${TEST_IDS.link}`}
|
|
161
179
|
onDownload={onDownloadFile}
|
|
162
180
|
>
|
|
163
181
|
<span className="np-upload-input__icon">{getIcon()}</span>
|
|
164
|
-
<div
|
|
182
|
+
<div
|
|
183
|
+
className="np-upload-input__item-content"
|
|
184
|
+
data-testid={`${file.id}-${TEST_IDS.mediaBody}`}
|
|
185
|
+
>
|
|
165
186
|
<Body type={Typography.BODY_LARGE} className="np-upload-input__title text-word-break">
|
|
166
187
|
{getTitle()}
|
|
167
188
|
</Body>
|
|
@@ -175,7 +196,10 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
|
|
|
175
196
|
aria-label={formatMessage(MESSAGES.removeFile, { filename })}
|
|
176
197
|
className="np-upload-input__item-button"
|
|
177
198
|
type="button"
|
|
199
|
+
tabIndex={0}
|
|
200
|
+
data-testid={`${file.id}-${TEST_IDS.action}`}
|
|
178
201
|
onClick={() => onDelete()}
|
|
202
|
+
onFocus={handleFocus}
|
|
179
203
|
>
|
|
180
204
|
<Bin size={16} />
|
|
181
205
|
</button>
|
|
@@ -10,7 +10,14 @@ type UploadItemLinkProps = PropsWithChildren<{
|
|
|
10
10
|
export const UploadItemLink = forwardRef<HTMLAnchorElement | HTMLDivElement, UploadItemLinkProps>(
|
|
11
11
|
({ children, url, onDownload, singleFileUpload }, ref) => {
|
|
12
12
|
if (!url) {
|
|
13
|
-
return
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
ref={ref as React.RefObject<HTMLDivElement>}
|
|
16
|
+
className={clsx('np-upload-input__item-container')}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
return (
|
|
@@ -23,6 +30,7 @@ export const UploadItemLink = forwardRef<HTMLAnchorElement | HTMLDivElement, Upl
|
|
|
23
30
|
'np-upload-input__item-link',
|
|
24
31
|
singleFileUpload ? 'np-upload-input__item-link--single-file' : '',
|
|
25
32
|
)}
|
|
33
|
+
tabIndex={0}
|
|
26
34
|
onClick={onDownload}
|
|
27
35
|
>
|
|
28
36
|
{children}
|