@transferwise/components 0.0.0-experimental-05bf45b → 0.0.0-experimental-62a0fe9

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 (29) hide show
  1. package/build/button/Button.mjs +1 -1
  2. package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.js +5 -4
  3. package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.js.map +1 -1
  4. package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.mjs +5 -4
  5. package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.mjs.map +1 -1
  6. package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts.map +1 -1
  7. package/build/types/uploadInput/UploadInput.d.ts +0 -9
  8. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  9. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +1 -3
  10. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  11. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
  12. package/build/uploadInput/UploadInput.js +51 -57
  13. package/build/uploadInput/UploadInput.js.map +1 -1
  14. package/build/uploadInput/UploadInput.mjs +51 -57
  15. package/build/uploadInput/UploadInput.mjs.map +1 -1
  16. package/build/uploadInput/uploadItem/UploadItem.js +3 -8
  17. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  18. package/build/uploadInput/uploadItem/UploadItem.mjs +3 -8
  19. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  20. package/build/uploadInput/uploadItem/UploadItemLink.js +0 -1
  21. package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
  22. package/build/uploadInput/uploadItem/UploadItemLink.mjs +0 -1
  23. package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
  24. package/package.json +6 -6
  25. package/src/common/polymorphicWithOverrides/PolymorphicWithOverrides.tsx +10 -6
  26. package/src/uploadInput/UploadInput.spec.tsx +2 -90
  27. package/src/uploadInput/UploadInput.tsx +60 -81
  28. package/src/uploadInput/uploadItem/UploadItem.tsx +6 -12
  29. package/src/uploadInput/uploadItem/UploadItemLink.tsx +1 -9
@@ -1 +1 @@
1
- {"version":3,"file":"UploadItemLink.mjs","sources":["../../../src/uploadInput/uploadItem/UploadItemLink.tsx"],"sourcesContent":["import { PropsWithChildren, MouseEvent, forwardRef } from 'react';\nimport { clsx } from 'clsx';\n\ntype UploadItemLinkProps = PropsWithChildren<{\n url?: string;\n onDownload?: (event: MouseEvent) => void;\n singleFileUpload: boolean;\n}>;\n\nexport const UploadItemLink = forwardRef<HTMLAnchorElement | HTMLDivElement, UploadItemLinkProps>(\n ({ children, url, onDownload, singleFileUpload }, ref) => {\n if (!url) {\n return (\n <div\n ref={ref as React.RefObject<HTMLDivElement>}\n className={clsx('np-upload-input__item-container')}\n >\n {children}\n </div>\n );\n }\n\n return (\n <a\n ref={ref as React.RefObject<HTMLAnchorElement>}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={clsx(\n 'np-upload-input__item-link',\n singleFileUpload ? 'np-upload-input__item-link--single-file' : '',\n )}\n tabIndex={0}\n onClick={onDownload}\n >\n {children}\n </a>\n );\n },\n);\n"],"names":["UploadItemLink","forwardRef","children","url","onDownload","singleFileUpload","ref","_jsx","className","clsx","href","target","rel","tabIndex","onClick"],"mappings":";;;;AASaA,MAAAA,cAAc,gBAAGC,UAAU,CACtC,CAAC;EAAEC,QAAQ;EAAEC,GAAG;EAAEC,UAAU;AAAEC,EAAAA,gBAAAA;CAAkB,EAAEC,GAAG,KAAI;EACvD,IAAI,CAACH,GAAG,EAAE;AACR,IAAA,oBACEI,GAAA,CAAA,KAAA,EAAA;AACED,MAAAA,GAAG,EAAEA,GAAuC;AAC5CE,MAAAA,SAAS,EAAEC,IAAI,CAAC,iCAAiC,CAAE;AAAAP,MAAAA,QAAA,EAElDA,QAAAA;AAAQ,KACN,CAAC,CAAA;AAEV,GAAA;AAEA,EAAA,oBACEK,GAAA,CAAA,GAAA,EAAA;AACED,IAAAA,GAAG,EAAEA,GAA0C;AAC/CI,IAAAA,IAAI,EAAEP,GAAI;AACVQ,IAAAA,MAAM,EAAC,QAAQ;AACfC,IAAAA,GAAG,EAAC,qBAAqB;IACzBJ,SAAS,EAAEC,IAAI,CACb,4BAA4B,EAC5BJ,gBAAgB,GAAG,yCAAyC,GAAG,EAAE,CACjE;AACFQ,IAAAA,QAAQ,EAAE,CAAE;AACZC,IAAAA,OAAO,EAAEV,UAAW;AAAAF,IAAAA,QAAA,EAEnBA,QAAAA;AAAQ,GACR,CAAC,CAAA;AAER,CAAC;;;;"}
1
+ {"version":3,"file":"UploadItemLink.mjs","sources":["../../../src/uploadInput/uploadItem/UploadItemLink.tsx"],"sourcesContent":["import { PropsWithChildren, MouseEvent, forwardRef } from 'react';\nimport { clsx } from 'clsx';\n\ntype UploadItemLinkProps = PropsWithChildren<{\n url?: string;\n onDownload?: (event: MouseEvent) => void;\n singleFileUpload: boolean;\n}>;\n\nexport const UploadItemLink = forwardRef<HTMLAnchorElement | HTMLDivElement, UploadItemLinkProps>(\n ({ children, url, onDownload, singleFileUpload }, ref) => {\n if (!url) {\n return <div ref={ref as React.RefObject<HTMLDivElement>} className={clsx('np-upload-input__item-container')}>{children}</div>;\n }\n\n return (\n <a\n ref={ref as React.RefObject<HTMLAnchorElement>}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={clsx(\n 'np-upload-input__item-link',\n singleFileUpload ? 'np-upload-input__item-link--single-file' : '',\n )}\n onClick={onDownload}\n >\n {children}\n </a>\n );\n },\n);\n"],"names":["UploadItemLink","forwardRef","children","url","onDownload","singleFileUpload","ref","_jsx","className","clsx","href","target","rel","onClick"],"mappings":";;;;AASaA,MAAAA,cAAc,gBAAGC,UAAU,CACtC,CAAC;EAAEC,QAAQ;EAAEC,GAAG;EAAEC,UAAU;AAAEC,EAAAA,gBAAAA;CAAkB,EAAEC,GAAG,KAAI;EACvD,IAAI,CAACH,GAAG,EAAE;AACR,IAAA,oBAAOI,GAAA,CAAA,KAAA,EAAA;AAAKD,MAAAA,GAAG,EAAEA,GAAuC;AAACE,MAAAA,SAAS,EAAEC,IAAI,CAAC,iCAAiC,CAAE;AAAAP,MAAAA,QAAA,EAAEA,QAAAA;AAAQ,KAAM,CAAC,CAAA;AAC/H,GAAA;AAEA,EAAA,oBACEK,GAAA,CAAA,GAAA,EAAA;AACED,IAAAA,GAAG,EAAEA,GAA0C;AAC/CI,IAAAA,IAAI,EAAEP,GAAI;AACVQ,IAAAA,MAAM,EAAC,QAAQ;AACfC,IAAAA,GAAG,EAAC,qBAAqB;IACzBJ,SAAS,EAAEC,IAAI,CACb,4BAA4B,EAC5BJ,gBAAgB,GAAG,yCAAyC,GAAG,EAAE,CACjE;AACFQ,IAAAA,OAAO,EAAET,UAAW;AAAAF,IAAAA,QAAA,EAEnBA,QAAAA;AAAQ,GACR,CAAC,CAAA;AAER,CAAC;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "0.0.0-experimental-05bf45b",
3
+ "version": "0.0.0-experimental-62a0fe9",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -76,8 +76,8 @@
76
76
  "@types/lodash": "4.17.6",
77
77
  "@types/lodash.clamp": "^4.0.9",
78
78
  "@types/lodash.debounce": "^4.0.9",
79
- "@types/react": "^18.3.3",
80
- "@types/react-dom": "^18.3.0",
79
+ "@types/react": "^18.3.11",
80
+ "@types/react-dom": "^18.3.1",
81
81
  "@types/react-transition-group": "4.4.10",
82
82
  "@wise/art": "^2.7.0",
83
83
  "babel-plugin-formatjs": "^10.5.16",
@@ -92,14 +92,14 @@
92
92
  "rollup-preserve-directives": "^1.1.1",
93
93
  "storybook": "^8.2.2",
94
94
  "@transferwise/less-config": "3.1.0",
95
- "@wise/components-theming": "1.6.0",
96
- "@transferwise/neptune-css": "14.19.1"
95
+ "@transferwise/neptune-css": "14.19.1",
96
+ "@wise/components-theming": "0.0.0-experimental-62a0fe9"
97
97
  },
98
98
  "peerDependencies": {
99
99
  "@transferwise/icons": "^3.13.1",
100
100
  "@transferwise/neptune-css": "^14.9.6",
101
101
  "@wise/art": "^2.7.0",
102
- "@wise/components-theming": "^1.0.0",
102
+ "@wise/components-theming": "0.0.0-experimental-62a0fe9",
103
103
  "react": ">=18",
104
104
  "react-dom": ">=18",
105
105
  "react-intl": "^5.10.0 || ^6"
@@ -11,9 +11,13 @@ export interface PolymorphicWithOverridesProps {
11
11
  /**
12
12
  * For overcoming the limitations of third-party components with an `as` prop.
13
13
  */
14
- export const PolymorphicWithOverrides = forwardRef(function PolymorphicWithOverrides(
15
- { __overrides: { as: Element, ...restOverrides }, ...restProps }: PolymorphicWithOverridesProps,
16
- ref: React.ForwardedRef<Element | null>,
17
- ) {
18
- return <Element ref={ref} {...restProps} {...restOverrides} />;
19
- });
14
+ export const PolymorphicWithOverrides = forwardRef<Element | null, PolymorphicWithOverridesProps>(
15
+ function PolymorphicWithOverrides(
16
+ { __overrides, ...restProps },
17
+ ref: React.ForwardedRef<Element | null>,
18
+ ) {
19
+ const { as: Element, ...restOverrides } =
20
+ __overrides as PolymorphicWithOverridesProps['__overrides'];
21
+ return <Element ref={ref} {...restProps} {...restOverrides} />;
22
+ },
23
+ );
@@ -139,7 +139,7 @@ describe('UploadInput', () => {
139
139
  onFilesChange,
140
140
  });
141
141
 
142
- const fileToDelete = screen.getByTestId('1-uploadItem');
142
+ const fileToDelete = screen.getAllByTestId(UPLOAD_ITEM_TEST_IDS.uploadItem)[0];
143
143
  within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
144
144
  await act(async () => {
145
145
  await jest.runOnlyPendingTimersAsync();
@@ -190,7 +190,7 @@ describe('UploadInput', () => {
190
190
  onFilesChange,
191
191
  });
192
192
 
193
- const fileToDelete = screen.getByTestId('1-uploadItem');
193
+ const fileToDelete = screen.getAllByTestId(UPLOAD_ITEM_TEST_IDS.uploadItem)[0];
194
194
  within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
195
195
  await act(async () => {
196
196
  await jest.runOnlyPendingTimersAsync();
@@ -212,94 +212,6 @@ describe('UploadInput', () => {
212
212
 
213
213
  expect(screen.queryByLabelText('Remove file ', { exact: false })).not.toBeInTheDocument();
214
214
  });
215
-
216
- it('should focus the next item after a file is deleted', async () => {
217
- const files = [
218
- {
219
- id: 1,
220
- filename: 'Sales-2024-invoice.pdf',
221
- status: Status.DONE,
222
- },
223
- {
224
- id: 2,
225
- filename: 'CoWork-0317-invoice.pdf',
226
- status: Status.DONE,
227
- },
228
- {
229
- id: 3,
230
- filename: 'purchase-receipt.pdf',
231
- status: Status.DONE,
232
- },
233
- ];
234
-
235
- renderComponent({
236
- ...props,
237
- files,
238
- multiple: true,
239
- onFilesChange,
240
- });
241
-
242
- const fileToDelete = screen.getByTestId('1-uploadItem');
243
- const nextFileAction = screen.getByTestId('3-action');
244
-
245
- within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
246
- await act(async () => {
247
- await jest.runOnlyPendingTimersAsync();
248
- });
249
-
250
- screen.getByText('Remove').click();
251
-
252
- await waitForElementToBeRemoved(fileToDelete);
253
- expect(props.onDeleteFile).toHaveBeenCalledWith(files[0].id);
254
-
255
- await waitFor(() => {
256
- expect(nextFileAction).toHaveFocus();
257
- });
258
- });
259
-
260
- it('should focus the Upload Input Button if the last item is removed', async () => {
261
- const files = [
262
- {
263
- id: 1,
264
- filename: 'Sales-2024-invoice.pdf',
265
- status: Status.DONE,
266
- },
267
- {
268
- id: 2,
269
- filename: 'CoWork-0317-invoice.pdf',
270
- status: Status.DONE,
271
- },
272
- {
273
- id: 3,
274
- filename: 'purchase-receipt.pdf',
275
- status: Status.DONE,
276
- },
277
- ];
278
-
279
- renderComponent({
280
- ...props,
281
- files,
282
- multiple: true,
283
- onFilesChange,
284
- });
285
-
286
- const fileToDelete = screen.getByTestId('3-uploadItem');
287
- const uploadInput = screen.getByTestId('uploadInput');
288
-
289
- within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
290
- await act(async () => {
291
- await jest.runOnlyPendingTimersAsync();
292
- });
293
-
294
- screen.getByText('Remove').click();
295
-
296
- await waitForElementToBeRemoved(fileToDelete);
297
- expect(props.onDeleteFile).toHaveBeenCalledWith(files[2].id);
298
-
299
- await waitFor(() => {
300
- expect(uploadInput).toHaveFocus();
301
- });
302
- });
303
215
  });
304
216
 
305
217
  describe('Max File Upload limit', () => {
@@ -1,5 +1,5 @@
1
1
  import { clsx } from 'clsx';
2
- import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState, useLayoutEffect } from 'react';
3
3
  import { useIntl } from 'react-intl';
4
4
 
5
5
  import Button from '../button';
@@ -99,45 +99,16 @@ 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
- */
106
102
  interface UploadItemRef {
107
- /**
108
- * Focuses the UploadItem component.
109
- */
110
103
  focus: () => void;
111
104
  }
112
105
 
113
- /**
114
- * Represents the next item to focus on after an action is performed.
115
- */
116
- type NextFocusItem = {
117
- /** The ID of the next item to focus on. If null, no specific item is targeted. */
118
- focusId: number | null;
119
- /** Indicates if the next item to focus on is the last item in the list. */
120
- isLastItem: boolean;
121
- };
122
-
123
- /**
124
- * Generates a unique ID for a file based on its name, size, and the current timestamp
125
- */
126
106
  function generateFileId(file: File) {
127
107
  const { name, size } = file;
128
108
  const uploadTimeStamp = new Date().getTime();
129
109
  return `${name}_${size}_${uploadTimeStamp}`;
130
110
  }
131
111
 
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
- */
141
112
  const UploadInput = ({
142
113
  files = [],
143
114
  fileInputName = 'file',
@@ -160,15 +131,15 @@ const UploadInput = ({
160
131
  uploadButtonTitle,
161
132
  }: UploadInputProps) => {
162
133
  const inputAttributes = useInputAttributes({ nonLabelable: true });
134
+
163
135
  const [markedFileForDelete, setMarkedFileForDelete] = useState<UploadedFile | null>(null);
164
- const [nextFocusItem, setNextFocusItem] = useState<NextFocusItem | null>(null);
136
+ const [fileToRemoveIndex, setFileToRemoveIndex] = useState<number | null>(null);
165
137
  const [mounted, setMounted] = useState(false);
166
138
  const { formatMessage } = useIntl();
167
139
  const itemRefs = useRef<(HTMLDivElement | UploadItemRef | null)[]>([]);
168
140
  const uploadInputRef = useRef<HTMLInputElement | null>(null);
169
141
 
170
142
  const PROGRESS_STATUSES = new Set([Status.PENDING, Status.PROCESSING]);
171
- const FOCUS_TIMEOUT = 400;
172
143
 
173
144
  const [uploadedFiles, setUploadedFiles] = useState<readonly UploadedFile[]>(
174
145
  multiple || files.length === 0 ? files : [files[0]],
@@ -176,64 +147,50 @@ const UploadInput = ({
176
147
 
177
148
  const uploadedFilesListReference = useRef(multiple || files.length === 0 ? files : [files[0]]);
178
149
 
179
- function updateFileList(updateFn: (list: readonly UploadedFile[]) => readonly UploadedFile[]) {
180
- setUploadedFiles(updateFn);
181
- uploadedFilesListReference.current = updateFn(uploadedFilesListReference.current);
182
- }
183
-
184
150
  function addFileToList(recentUploadedFile: UploadedFile) {
185
- updateFileList((list) => [...list, recentUploadedFile]);
186
- }
151
+ function addToList(listToAddTo: readonly UploadedFile[]) {
152
+ return [...listToAddTo, recentUploadedFile];
153
+ }
187
154
 
188
- function removeFileFromList(file: UploadedFile) {
189
- updateFileList((list) =>
190
- list.filter((fileInList) => file !== fileInList && file.id !== fileInList.id),
191
- );
155
+ setUploadedFiles(addToList);
156
+ uploadedFilesListReference.current = addToList(uploadedFilesListReference.current);
192
157
  }
193
158
 
194
- function modifyFileInList(file: UploadedFile, updates: Partial<UploadedFile>) {
195
- updateFileList((list) =>
196
- list.map((fileInList) =>
197
- fileInList === file || fileInList.id === file.id ? { ...file, ...updates } : fileInList,
198
- ),
199
- );
200
- }
159
+ const removeFileFromList = (file: UploadedFile) => {
160
+ function filterOutFrom(listToFilterFrom: readonly UploadedFile[]) {
161
+ return listToFilterFrom.filter(
162
+ (fileInList) => file !== fileInList && file.id !== fileInList.id,
163
+ );
164
+ }
201
165
 
202
- function focusNextItem(
203
- focusId: number | null = null,
204
- isLastItem = false,
205
- timeout: number = FOCUS_TIMEOUT,
206
- ) {
207
- const nextFocusIdIndex = focusId
208
- ? uploadedFilesListReference.current.findIndex((item: UploadedFile) => item.id === focusId)
209
- : -1;
210
-
211
- requestAnimationFrame(() => {
212
- setTimeout(() => {
213
- if (isLastItem || nextFocusIdIndex === -1) {
214
- // If it's the last item or no next item, focus on the upload input reference
215
- uploadInputRef.current?.focus();
216
- } else {
217
- const nextFocusElement = itemRefs.current[nextFocusIdIndex];
218
- nextFocusElement?.focus();
219
- }
220
- }, timeout);
221
- });
222
- }
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
+ });
177
+
178
+ setUploadedFiles(updateListItem);
179
+ uploadedFilesListReference.current = updateListItem(uploadedFilesListReference.current);
180
+ };
181
+
182
+ const [fileToRemove, setFileToRemove] = useState<UploadedFile | null>(null);
223
183
 
224
184
  const removeFile = (file: UploadedFile) => {
225
185
  const { id, status } = file;
226
186
  const index = uploadedFiles.findIndex((f) => f.id === file.id);
227
- const isLastItem = index === uploadedFiles.length - 1;
228
- const nextIndexId = uploadedFiles[index + 1]?.id as number;
229
- const focusId = nextIndexId ?? null;
187
+ setFileToRemoveIndex(index);
230
188
 
231
189
  if (status === Status.FAILED) {
232
190
  removeFileFromList(file);
233
- setNextFocusItem({ focusId: focusId ?? null, isLastItem });
191
+ setFileToRemove(file);
234
192
  } else if (onDeleteFile && id) {
235
193
  modifyFileInList(file, { status: Status.PROCESSING, error: undefined });
236
- setNextFocusItem({ focusId, isLastItem });
237
194
 
238
195
  onDeleteFile(id)
239
196
  .then(() => {
@@ -241,7 +198,9 @@ const UploadInput = ({
241
198
  })
242
199
  .catch((error) => {
243
200
  modifyFileInList(file, { error: error as UploadError });
244
- setNextFocusItem({ focusId, isLastItem });
201
+ })
202
+ .finally(() => {
203
+ setFileToRemove(file);
245
204
  });
246
205
  }
247
206
  };
@@ -280,10 +239,12 @@ const UploadInput = ({
280
239
  return numberOfValidFiles >= maxFiles;
281
240
  }
282
241
 
242
+ // One or more files selected, create entries for them
283
243
  const addFiles = (selectedFiles: FileList) => {
284
244
  for (let i = 0; i < selectedFiles.length; i += 1) {
285
245
  const file = selectedFiles.item(i);
286
246
 
247
+ // Returning a FormData[] array instead of FileList so we can filter out incorrect files
287
248
  const formData = new FormData();
288
249
 
289
250
  if (file) {
@@ -292,11 +253,14 @@ const UploadInput = ({
292
253
 
293
254
  const allowedFileTypes = typeof fileTypes === 'string' ? fileTypes : fileTypes.join(',');
294
255
 
256
+ // Check if file type is valid
295
257
  if (!isTypeValid(file, allowedFileTypes)) {
296
258
  handleFileUploadFailure(file, formatMessage(MESSAGES.fileTypeNotSupported));
297
259
  continue;
298
260
  }
299
261
 
262
+ // Check if the filesize is valid
263
+ // Convert to rough bytes
300
264
  if (!isSizeValid(file, sizeLimit * 1000)) {
301
265
  const failureMessage = sizeLimitErrorMessage || formatMessage(MESSAGES.fileIsTooLarge);
302
266
  handleFileUploadFailure(file, failureMessage);
@@ -311,11 +275,14 @@ const UploadInput = ({
311
275
  continue;
312
276
  }
313
277
 
278
+ // Check if the file is already in the list
314
279
  const existingFile = uploadedFiles.find((f) => f.filename === file.name);
315
280
  if (existingFile) {
281
+ // Remove the file from the list before adding it again
316
282
  removeFileFromList(existingFile);
317
283
  }
318
284
 
285
+ // Add the file to the list
319
286
  formData.append(fileInputName, file);
320
287
  const pendingFile = {
321
288
  id: generateFileId(file),
@@ -325,8 +292,10 @@ const UploadInput = ({
325
292
 
326
293
  addFileToList(pendingFile);
327
294
 
295
+ // Start uploading the file
328
296
  onUploadFile(formData)
329
297
  .then(({ id, url, error }: UploadResponse) => {
298
+ // Replace the temporary id with the final one received from the API, and also set any errors
330
299
  modifyFileInList(pendingFile, { id, url, error, status: Status.SUCCEEDED });
331
300
  })
332
301
  .catch((error) => {
@@ -334,6 +303,7 @@ const UploadInput = ({
334
303
  });
335
304
 
336
305
  if (!multiple) {
306
+ // Only upload a single file
337
307
  break;
338
308
  }
339
309
  }
@@ -341,11 +311,20 @@ const UploadInput = ({
341
311
  };
342
312
 
343
313
  useLayoutEffect(() => {
344
- if (nextFocusItem) {
345
- focusNextItem(nextFocusItem.focusId, nextFocusItem.isLastItem);
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
346
326
  }
347
- setNextFocusItem(null);
348
- }, [nextFocusItem]);
327
+ }, [uploadedFiles, fileToRemove, fileToRemoveIndex, itemRefs, uploadInputRef]);
349
328
 
350
329
  useEffect(() => {
351
330
  setMounted(true);
@@ -4,8 +4,9 @@ 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 } from '../../common';
7
+ import { Size, Status, Typography, Sentiment } from '../../common';
8
8
  import ProcessIndicator from '../../processIndicator/ProcessIndicator';
9
+ import StatusIcon from '../../statusIcon/StatusIcon';
9
10
  import { UploadedFile, UploadError } from '../types';
10
11
 
11
12
  import MESSAGES from './UploadItem.messages';
@@ -37,8 +38,6 @@ interface UploadItemRef {
37
38
  export enum TEST_IDS {
38
39
  uploadItem = 'uploadItem',
39
40
  mediaBody = 'mediaBody',
40
- link = 'link',
41
- action = 'action',
42
41
  }
43
42
 
44
43
  const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
@@ -47,7 +46,6 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
47
46
  const { status, filename, error, errors, url } = file;
48
47
  const linkRef = useRef<HTMLAnchorElement>(null);
49
48
  const buttonRef = useRef<HTMLButtonElement>(null);
50
- const isSucceeded = [Status.SUCCEEDED, undefined].includes(status) && !!url;
51
49
 
52
50
  useImperativeHandle<UploadItemRef, UploadItemRef>(ref, () => ({
53
51
  focus: (): void => {
@@ -59,6 +57,8 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
59
57
  },
60
58
  }));
61
59
 
60
+ const isSucceeded = [Status.SUCCEEDED, undefined].includes(status) && !!url;
61
+
62
62
  /**
63
63
  * We're temporarily reverting to the regular icon components,
64
64
  * until the StatusIcon receives 24px sizing. Some misalignment
@@ -152,20 +152,16 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
152
152
  return (
153
153
  <div
154
154
  className={clsx('np-upload-input__item', { 'is-interactive': isSucceeded && url })}
155
- data-testid={`${file.id}-${TEST_IDS.uploadItem}`}
155
+ data-testid={TEST_IDS.uploadItem}
156
156
  >
157
157
  <UploadItemLink
158
158
  ref={linkRef}
159
159
  url={isSucceeded ? url : undefined}
160
160
  singleFileUpload={singleFileUpload}
161
- data-testid={`${file.id}-${TEST_IDS.link}`}
162
161
  onDownload={onDownloadFile}
163
162
  >
164
163
  <span className="np-upload-input__icon">{getIcon()}</span>
165
- <div
166
- className="np-upload-input__item-content"
167
- data-testid={`${file.id}-${TEST_IDS.mediaBody}`}
168
- >
164
+ <div className="np-upload-input__item-content" data-testid={TEST_IDS.mediaBody}>
169
165
  <Body type={Typography.BODY_LARGE} className="np-upload-input__title text-word-break">
170
166
  {getTitle()}
171
167
  </Body>
@@ -179,8 +175,6 @@ const UploadItem = forwardRef<UploadItemRef, UploadItemProps>(
179
175
  aria-label={formatMessage(MESSAGES.removeFile, { filename })}
180
176
  className="np-upload-input__item-button"
181
177
  type="button"
182
- tabIndex={0}
183
- data-testid={`${file.id}-${TEST_IDS.action}`}
184
178
  onClick={() => onDelete()}
185
179
  >
186
180
  <Bin size={16} />
@@ -10,14 +10,7 @@ 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 (
14
- <div
15
- ref={ref as React.RefObject<HTMLDivElement>}
16
- className={clsx('np-upload-input__item-container')}
17
- >
18
- {children}
19
- </div>
20
- );
13
+ return <div ref={ref as React.RefObject<HTMLDivElement>} className={clsx('np-upload-input__item-container')}>{children}</div>;
21
14
  }
22
15
 
23
16
  return (
@@ -30,7 +23,6 @@ export const UploadItemLink = forwardRef<HTMLAnchorElement | HTMLDivElement, Upl
30
23
  'np-upload-input__item-link',
31
24
  singleFileUpload ? 'np-upload-input__item-link--single-file' : '',
32
25
  )}
33
- tabIndex={0}
34
26
  onClick={onDownload}
35
27
  >
36
28
  {children}