@transferwise/components 0.0.0-experimental-4b8556a → 0.0.0-experimental-3064bdb
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/alert/Alert.js +52 -49
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +53 -50
- package/build/alert/Alert.mjs.map +1 -1
- package/build/i18n/en.json +2 -0
- package/build/i18n/en.json.js +2 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +2 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/main.css +214 -0
- package/build/styles/main.css +214 -0
- package/build/styles/table/Table.css +214 -0
- package/build/types/alert/Alert.d.ts +3 -1
- package/build/types/alert/Alert.d.ts.map +1 -1
- package/build/types/table/Table.d.ts +23 -0
- package/build/types/table/Table.d.ts.map +1 -0
- package/build/types/table/Table.messages.d.ts +12 -0
- package/build/types/table/Table.messages.d.ts.map +1 -0
- package/build/types/table/TableCell.d.ts +37 -0
- package/build/types/table/TableCell.d.ts.map +1 -0
- package/build/types/table/TableHeader.d.ts +12 -0
- package/build/types/table/TableHeader.d.ts.map +1 -0
- package/build/types/table/TableRow.d.ts +17 -0
- package/build/types/table/TableRow.d.ts.map +1 -0
- package/build/types/table/TableStatusText.d.ts +9 -0
- package/build/types/table/TableStatusText.d.ts.map +1 -0
- package/build/types/table/index.d.ts +6 -0
- package/build/types/table/index.d.ts.map +1 -0
- 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 +3 -3
- package/src/alert/Alert.spec.tsx +20 -0
- package/src/alert/Alert.story.tsx +50 -2
- package/src/alert/Alert.tsx +57 -50
- package/src/i18n/en.json +2 -0
- package/src/main.css +214 -0
- package/src/main.less +1 -0
- package/src/table/Table.css +214 -0
- package/src/table/Table.less +253 -0
- package/src/table/Table.messages.ts +12 -0
- package/src/table/Table.spec.tsx +87 -0
- package/src/table/Table.story.tsx +352 -0
- package/src/table/Table.tsx +121 -0
- package/src/table/TableCell.spec.tsx +298 -0
- package/src/table/TableCell.tsx +153 -0
- package/src/table/TableHeader.spec.tsx +58 -0
- package/src/table/TableHeader.tsx +50 -0
- package/src/table/TableRow.spec.tsx +104 -0
- package/src/table/TableRow.tsx +62 -0
- package/src/table/TableStatusText.spec.tsx +53 -0
- package/src/table/TableStatusText.tsx +35 -0
- package/src/table/index.ts +11 -0
- package/src/uploadInput/UploadInput.spec.tsx +6 -150
- package/src/uploadInput/UploadInput.tsx +60 -83
- package/src/uploadInput/uploadItem/UploadItem.tsx +6 -12
- package/src/uploadInput/uploadItem/UploadItemLink.tsx +1 -9
|
@@ -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; // Set to 400ms to allow animations and Modal to unmount
|
|
172
143
|
|
|
173
144
|
const [uploadedFiles, setUploadedFiles] = useState<readonly UploadedFile[]>(
|
|
174
145
|
multiple || files.length === 0 ? files : [files[0]],
|
|
@@ -176,65 +147,49 @@ 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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const nextFocusElement = itemRefs.current[nextFocusIdIndex];
|
|
220
|
-
nextFocusElement?.focus();
|
|
221
|
-
}
|
|
222
|
-
}, timeout);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
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);
|
|
225
183
|
|
|
226
184
|
const removeFile = (file: UploadedFile) => {
|
|
227
185
|
const { id, status } = file;
|
|
228
186
|
const index = uploadedFiles.findIndex((f) => f.id === file.id);
|
|
229
|
-
|
|
230
|
-
const nextIndexId = uploadedFiles[index + 1]?.id as number;
|
|
231
|
-
const focusId = nextIndexId ?? null;
|
|
187
|
+
setFileToRemoveIndex(index);
|
|
232
188
|
|
|
233
189
|
if (status === Status.FAILED) {
|
|
234
|
-
setNextFocusItem({ focusId: focusId ?? null, isLastItem });
|
|
235
190
|
removeFileFromList(file);
|
|
191
|
+
setFileToRemove(file);
|
|
236
192
|
} else if (onDeleteFile && id) {
|
|
237
|
-
setNextFocusItem({ focusId, isLastItem });
|
|
238
193
|
modifyFileInList(file, { status: Status.PROCESSING, error: undefined });
|
|
239
194
|
|
|
240
195
|
onDeleteFile(id)
|
|
@@ -242,8 +197,10 @@ const UploadInput = ({
|
|
|
242
197
|
removeFileFromList(file);
|
|
243
198
|
})
|
|
244
199
|
.catch((error) => {
|
|
245
|
-
setNextFocusItem({ focusId, isLastItem });
|
|
246
200
|
modifyFileInList(file, { error: error as UploadError });
|
|
201
|
+
})
|
|
202
|
+
.finally(() => {
|
|
203
|
+
setFileToRemove(file);
|
|
247
204
|
});
|
|
248
205
|
}
|
|
249
206
|
};
|
|
@@ -282,10 +239,12 @@ const UploadInput = ({
|
|
|
282
239
|
return numberOfValidFiles >= maxFiles;
|
|
283
240
|
}
|
|
284
241
|
|
|
242
|
+
// One or more files selected, create entries for them
|
|
285
243
|
const addFiles = (selectedFiles: FileList) => {
|
|
286
244
|
for (let i = 0; i < selectedFiles.length; i += 1) {
|
|
287
245
|
const file = selectedFiles.item(i);
|
|
288
246
|
|
|
247
|
+
// Returning a FormData[] array instead of FileList so we can filter out incorrect files
|
|
289
248
|
const formData = new FormData();
|
|
290
249
|
|
|
291
250
|
if (file) {
|
|
@@ -294,11 +253,14 @@ const UploadInput = ({
|
|
|
294
253
|
|
|
295
254
|
const allowedFileTypes = typeof fileTypes === 'string' ? fileTypes : fileTypes.join(',');
|
|
296
255
|
|
|
256
|
+
// Check if file type is valid
|
|
297
257
|
if (!isTypeValid(file, allowedFileTypes)) {
|
|
298
258
|
handleFileUploadFailure(file, formatMessage(MESSAGES.fileTypeNotSupported));
|
|
299
259
|
continue;
|
|
300
260
|
}
|
|
301
261
|
|
|
262
|
+
// Check if the filesize is valid
|
|
263
|
+
// Convert to rough bytes
|
|
302
264
|
if (!isSizeValid(file, sizeLimit * 1000)) {
|
|
303
265
|
const failureMessage = sizeLimitErrorMessage || formatMessage(MESSAGES.fileIsTooLarge);
|
|
304
266
|
handleFileUploadFailure(file, failureMessage);
|
|
@@ -313,11 +275,14 @@ const UploadInput = ({
|
|
|
313
275
|
continue;
|
|
314
276
|
}
|
|
315
277
|
|
|
278
|
+
// Check if the file is already in the list
|
|
316
279
|
const existingFile = uploadedFiles.find((f) => f.filename === file.name);
|
|
317
280
|
if (existingFile) {
|
|
281
|
+
// Remove the file from the list before adding it again
|
|
318
282
|
removeFileFromList(existingFile);
|
|
319
283
|
}
|
|
320
284
|
|
|
285
|
+
// Add the file to the list
|
|
321
286
|
formData.append(fileInputName, file);
|
|
322
287
|
const pendingFile = {
|
|
323
288
|
id: generateFileId(file),
|
|
@@ -327,8 +292,10 @@ const UploadInput = ({
|
|
|
327
292
|
|
|
328
293
|
addFileToList(pendingFile);
|
|
329
294
|
|
|
295
|
+
// Start uploading the file
|
|
330
296
|
onUploadFile(formData)
|
|
331
297
|
.then(({ id, url, error }: UploadResponse) => {
|
|
298
|
+
// Replace the temporary id with the final one received from the API, and also set any errors
|
|
332
299
|
modifyFileInList(pendingFile, { id, url, error, status: Status.SUCCEEDED });
|
|
333
300
|
})
|
|
334
301
|
.catch((error) => {
|
|
@@ -336,6 +303,7 @@ const UploadInput = ({
|
|
|
336
303
|
});
|
|
337
304
|
|
|
338
305
|
if (!multiple) {
|
|
306
|
+
// Only upload a single file
|
|
339
307
|
break;
|
|
340
308
|
}
|
|
341
309
|
}
|
|
@@ -343,11 +311,20 @@ const UploadInput = ({
|
|
|
343
311
|
};
|
|
344
312
|
|
|
345
313
|
useLayoutEffect(() => {
|
|
346
|
-
if (
|
|
347
|
-
|
|
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
|
|
348
326
|
}
|
|
349
|
-
|
|
350
|
-
}, [nextFocusItem]);
|
|
327
|
+
}, [uploadedFiles, fileToRemove, fileToRemoveIndex, itemRefs, uploadInputRef]);
|
|
351
328
|
|
|
352
329
|
useEffect(() => {
|
|
353
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}
|