@transferwise/components 46.63.0 → 46.64.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 (49) 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/locale/index.js.map +1 -1
  6. package/build/common/locale/index.mjs.map +1 -1
  7. package/build/dateLookup/tableLink/TableLink.js.map +1 -1
  8. package/build/dateLookup/tableLink/TableLink.mjs.map +1 -1
  9. package/build/instructionsList/InstructionsList.js.map +1 -1
  10. package/build/instructionsList/InstructionsList.mjs.map +1 -1
  11. package/build/types/card/Card.d.ts.map +1 -1
  12. package/build/types/circularButton/CircularButton.d.ts.map +1 -1
  13. package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
  14. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  15. package/build/types/uploadInput/uploadButton/UploadButton.d.ts +1 -1
  16. package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
  17. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +5 -1
  18. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  19. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +5 -5
  20. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
  21. package/build/uploadInput/UploadInput.js +42 -11
  22. package/build/uploadInput/UploadInput.js.map +1 -1
  23. package/build/uploadInput/UploadInput.mjs +43 -12
  24. package/build/uploadInput/UploadInput.mjs.map +1 -1
  25. package/build/uploadInput/uploadButton/UploadButton.js +14 -7
  26. package/build/uploadInput/uploadButton/UploadButton.js.map +1 -1
  27. package/build/uploadInput/uploadButton/UploadButton.mjs +15 -8
  28. package/build/uploadInput/uploadButton/UploadButton.mjs.map +1 -1
  29. package/build/uploadInput/uploadItem/UploadItem.js +18 -3
  30. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  31. package/build/uploadInput/uploadItem/UploadItem.mjs +18 -3
  32. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  33. package/build/uploadInput/uploadItem/UploadItemLink.js +6 -3
  34. package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
  35. package/build/uploadInput/uploadItem/UploadItemLink.mjs +6 -3
  36. package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
  37. package/package.json +1 -1
  38. package/src/card/Card.spec.tsx +4 -5
  39. package/src/card/Card.story.tsx +4 -6
  40. package/src/card/Card.tsx +3 -2
  41. package/src/circularButton/CircularButton.tsx +1 -1
  42. package/src/common/locale/index.ts +1 -1
  43. package/src/dateLookup/tableLink/TableLink.tsx +15 -15
  44. package/src/instructionsList/InstructionsList.tsx +1 -4
  45. package/src/uploadInput/UploadInput.tests.story.tsx +7 -3
  46. package/src/uploadInput/UploadInput.tsx +50 -8
  47. package/src/uploadInput/uploadButton/UploadButton.tsx +163 -141
  48. package/src/uploadInput/uploadItem/UploadItem.tsx +146 -124
  49. package/src/uploadInput/uploadItem/UploadItemLink.tsx +23 -25
@@ -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;
@@ -1,4 +1,4 @@
1
- import { PropsWithChildren, MouseEvent } from 'react';
1
+ import { PropsWithChildren, MouseEvent, forwardRef } from 'react';
2
2
  import { clsx } from 'clsx';
3
3
 
4
4
  type UploadItemLinkProps = PropsWithChildren<{
@@ -7,28 +7,26 @@ type UploadItemLinkProps = PropsWithChildren<{
7
7
  singleFileUpload: boolean;
8
8
  }>;
9
9
 
10
- export const UploadItemLink = ({
11
- children,
12
- url,
13
- onDownload,
14
- singleFileUpload,
15
- }: UploadItemLinkProps) => {
16
- if (!url) {
17
- return <div>{children}</div>;
18
- }
10
+ export const UploadItemLink = forwardRef<HTMLAnchorElement | HTMLDivElement, UploadItemLinkProps>(
11
+ ({ children, url, onDownload, singleFileUpload }, ref) => {
12
+ if (!url) {
13
+ return <div ref={ref as React.RefObject<HTMLDivElement>}>{children}</div>;
14
+ }
19
15
 
20
- return (
21
- <a
22
- href={url}
23
- target="_blank"
24
- rel="noopener noreferrer"
25
- className={clsx(
26
- 'np-upload-item__link',
27
- singleFileUpload ? 'np-upload-item--single-file' : '',
28
- )}
29
- onClick={onDownload}
30
- >
31
- {children}
32
- </a>
33
- );
34
- };
16
+ return (
17
+ <a
18
+ ref={ref as React.RefObject<HTMLAnchorElement>}
19
+ href={url}
20
+ target="_blank"
21
+ rel="noopener noreferrer"
22
+ className={clsx(
23
+ 'np-upload-item__link',
24
+ singleFileUpload ? 'np-upload-item--single-file' : '',
25
+ )}
26
+ onClick={onDownload}
27
+ >
28
+ {children}
29
+ </a>
30
+ );
31
+ },
32
+ );