@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.
- package/build/button/Button.mjs +1 -1
- package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.js +5 -4
- package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.js.map +1 -1
- package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.mjs +5 -4
- package/build/common/polymorphicWithOverrides/PolymorphicWithOverrides.mjs.map +1 -1
- package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts.map +1 -1
- package/build/types/uploadInput/UploadInput.d.ts +0 -9
- package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts +1 -3
- 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 +51 -57
- package/build/uploadInput/UploadInput.js.map +1 -1
- package/build/uploadInput/UploadInput.mjs +51 -57
- package/build/uploadInput/UploadInput.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.js +3 -8
- package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.mjs +3 -8
- package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.js +0 -1
- package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.mjs +0 -1
- package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
- package/package.json +6 -6
- package/src/common/polymorphicWithOverrides/PolymorphicWithOverrides.tsx +10 -6
- package/src/uploadInput/UploadInput.spec.tsx +2 -90
- package/src/uploadInput/UploadInput.tsx +60 -81
- package/src/uploadInput/uploadItem/UploadItem.tsx +6 -12
- 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
|
|
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-
|
|
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.
|
|
80
|
-
"@types/react-dom": "^18.3.
|
|
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
|
-
"@
|
|
96
|
-
"@
|
|
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": "
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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 [
|
|
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
|
-
|
|
186
|
-
|
|
151
|
+
function addToList(listToAddTo: readonly UploadedFile[]) {
|
|
152
|
+
return [...listToAddTo, recentUploadedFile];
|
|
153
|
+
}
|
|
187
154
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
345
|
-
|
|
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
|
-
|
|
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={
|
|
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}
|