@transferwise/components 46.62.1 → 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 (63) hide show
  1. package/build/card/Card.js.map +1 -1
  2. package/build/card/Card.mjs.map +1 -1
  3. package/build/checkbox/Checkbox.js +1 -1
  4. package/build/checkbox/Checkbox.js.map +1 -1
  5. package/build/checkbox/Checkbox.mjs +1 -1
  6. package/build/checkbox/Checkbox.mjs.map +1 -1
  7. package/build/circularButton/CircularButton.js.map +1 -1
  8. package/build/circularButton/CircularButton.mjs.map +1 -1
  9. package/build/common/locale/index.js.map +1 -1
  10. package/build/common/locale/index.mjs.map +1 -1
  11. package/build/dateLookup/tableLink/TableLink.js.map +1 -1
  12. package/build/dateLookup/tableLink/TableLink.mjs.map +1 -1
  13. package/build/instructionsList/InstructionsList.js.map +1 -1
  14. package/build/instructionsList/InstructionsList.mjs.map +1 -1
  15. package/build/radio/Radio.js +2 -9
  16. package/build/radio/Radio.js.map +1 -1
  17. package/build/radio/Radio.mjs +2 -9
  18. package/build/radio/Radio.mjs.map +1 -1
  19. package/build/types/card/Card.d.ts.map +1 -1
  20. package/build/types/circularButton/CircularButton.d.ts.map +1 -1
  21. package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
  22. package/build/types/radio/Radio.d.ts.map +1 -1
  23. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  24. package/build/types/uploadInput/uploadButton/UploadButton.d.ts +1 -1
  25. package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
  26. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +5 -1
  27. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  28. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +5 -5
  29. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
  30. package/build/uploadInput/UploadInput.js +42 -11
  31. package/build/uploadInput/UploadInput.js.map +1 -1
  32. package/build/uploadInput/UploadInput.mjs +43 -12
  33. package/build/uploadInput/UploadInput.mjs.map +1 -1
  34. package/build/uploadInput/uploadButton/UploadButton.js +14 -7
  35. package/build/uploadInput/uploadButton/UploadButton.js.map +1 -1
  36. package/build/uploadInput/uploadButton/UploadButton.mjs +15 -8
  37. package/build/uploadInput/uploadButton/UploadButton.mjs.map +1 -1
  38. package/build/uploadInput/uploadItem/UploadItem.js +18 -3
  39. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  40. package/build/uploadInput/uploadItem/UploadItem.mjs +18 -3
  41. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  42. package/build/uploadInput/uploadItem/UploadItemLink.js +6 -3
  43. package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
  44. package/build/uploadInput/uploadItem/UploadItemLink.mjs +6 -3
  45. package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/card/Card.spec.tsx +4 -5
  48. package/src/card/Card.story.tsx +4 -6
  49. package/src/card/Card.tsx +3 -2
  50. package/src/checkbox/Checkbox.tsx +1 -1
  51. package/src/checkboxButton/CheckboxButton.story.tsx +18 -21
  52. package/src/circularButton/CircularButton.tsx +1 -1
  53. package/src/common/locale/index.ts +1 -1
  54. package/src/dateLookup/tableLink/TableLink.tsx +15 -15
  55. package/src/instructionsList/InstructionsList.tsx +1 -4
  56. package/src/radio/Radio.tsx +3 -9
  57. package/src/radio/__snapshots__/Radio.rtl.spec.tsx.snap +1 -1
  58. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +8 -3
  59. package/src/uploadInput/UploadInput.tests.story.tsx +7 -3
  60. package/src/uploadInput/UploadInput.tsx +50 -8
  61. package/src/uploadInput/uploadButton/UploadButton.tsx +163 -141
  62. package/src/uploadInput/uploadItem/UploadItem.tsx +146 -124
  63. package/src/uploadInput/uploadItem/UploadItemLink.tsx +23 -25
@@ -2,13 +2,14 @@ import { action } from '@storybook/addon-actions';
2
2
  import { Meta, StoryObj } from '@storybook/react';
3
3
  import { useState } from 'react';
4
4
 
5
- import CheckboxButton, { CheckboxButtonProps } from './CheckboxButton';
5
+ import CheckboxButton from './CheckboxButton';
6
6
 
7
7
  export default {
8
8
  component: CheckboxButton,
9
9
  title: 'Actions/CheckboxButton',
10
10
  tags: ['autodocs'],
11
11
  args: {
12
+ disabled: false,
12
13
  onBlur: action('blur'),
13
14
  onClick: action('click'),
14
15
  onFocus: action('focus'),
@@ -20,7 +21,6 @@ type Story = StoryObj<typeof CheckboxButton>;
20
21
  export const Basic: Story = {
21
22
  args: {
22
23
  'aria-label': 'Toggle email updates',
23
- disabled: false,
24
24
  },
25
25
  render: (args) => {
26
26
  // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -30,25 +30,22 @@ export const Basic: Story = {
30
30
  },
31
31
  };
32
32
 
33
- const Template = (props: CheckboxButtonProps) => {
34
- const checked = true;
35
- const [indeterminate, setIndeterminate] = useState(true);
36
-
37
- return (
38
- <CheckboxButton
39
- {...props}
40
- checked={checked}
41
- indeterminate={indeterminate}
42
- onChange={() => setIndeterminate(!indeterminate)}
43
- />
44
- );
45
- };
46
-
47
- export const Indeterminate = () => {
48
- const args = {
33
+ export const Indeterminate: Story = {
34
+ args: {
49
35
  'aria-label': 'Group checkbox with indeterminate state',
50
- disabled: false,
51
36
  indeterminate: true,
52
- };
53
- return <Template {...args} />;
37
+ },
38
+ render: (args) => {
39
+ // eslint-disable-next-line react-hooks/rules-of-hooks
40
+ const [indeterminate, setIndeterminate] = useState(true);
41
+
42
+ return (
43
+ <CheckboxButton
44
+ {...args}
45
+ checked
46
+ indeterminate={indeterminate}
47
+ onChange={() => setIndeterminate(!indeterminate)}
48
+ />
49
+ );
50
+ },
54
51
  };
@@ -3,7 +3,7 @@ import { cloneElement } from 'react';
3
3
 
4
4
  import Body from '../body/Body';
5
5
  import { typeClassMap, priorityClassMap } from '../button/classMap';
6
- import { ControlType, Priority , Typography } from '../common';
6
+ import { ControlType, Priority, Typography } from '../common';
7
7
 
8
8
  export interface CircularButtonProps {
9
9
  className?: string;
@@ -79,7 +79,7 @@ export function getLangFromLocale(locale: string) {
79
79
  */
80
80
  export function getCountryFromLocale(locale: string) {
81
81
  const adjustedLocale = adjustLocale(locale);
82
- return adjustedLocale != null ? new Intl.Locale(adjustedLocale).region ?? null : null;
82
+ return adjustedLocale != null ? (new Intl.Locale(adjustedLocale).region ?? null) : null;
83
83
  }
84
84
 
85
85
  /**
@@ -56,21 +56,21 @@ const TableLink = ({
56
56
 
57
57
  return (
58
58
  <button
59
- ref={buttonRef}
60
- type="button"
61
- className={clsx(
62
- `tw-date-lookup-${type}-option np-text-body-default-bold`,
63
- { active: !!active },
64
- { today: !!today },
65
- )}
66
- disabled={disabled}
67
- tabIndex={autofocus ? 0 : -1}
68
- aria-label={calculateAriaLabel()}
69
- aria-pressed={active}
70
- onClick={onCalendarClick}
71
- >
72
- {title || item}
73
- </button>
59
+ ref={buttonRef}
60
+ type="button"
61
+ className={clsx(
62
+ `tw-date-lookup-${type}-option np-text-body-default-bold`,
63
+ { active: !!active },
64
+ { today: !!today },
65
+ )}
66
+ disabled={disabled}
67
+ tabIndex={autofocus ? 0 : -1}
68
+ aria-label={calculateAriaLabel()}
69
+ aria-pressed={active}
70
+ onClick={onCalendarClick}
71
+ >
72
+ {title || item}
73
+ </button>
74
74
  );
75
75
  };
76
76
 
@@ -57,10 +57,7 @@ function Instruction({ item, type }: { item: ReactNode | InstructionNode; type:
57
57
  const isInstructionNode =
58
58
  typeof item === 'object' && item !== null && 'content' in item && 'aria-label' in item;
59
59
  return (
60
- <li
61
- className="instruction"
62
- aria-label={isInstructionNode ? (item['aria-label']) : undefined}
63
- >
60
+ <li className="instruction" aria-label={isInstructionNode ? item['aria-label'] : undefined}>
64
61
  {type === 'do' ? (
65
62
  <DoIcon size={24} className={type} />
66
63
  ) : (
@@ -1,4 +1,3 @@
1
- import { useTheme } from '@wise/components-theming';
2
1
  import { clsx } from 'clsx';
3
2
 
4
3
  import Body from '../body/Body';
@@ -22,20 +21,19 @@ export default function Radio<T extends string | number = ''>({
22
21
  secondary,
23
22
  ...otherProps
24
23
  }: RadioProps<T>) {
25
- const { isModern } = useTheme();
26
24
  return (
27
25
  <div
28
26
  className={clsx(
29
27
  'radio np-radio',
30
28
  {
31
29
  'radio-lg': secondary,
32
- disabled: isModern && disabled,
30
+ 'radio-disabled': disabled,
33
31
  },
34
32
  className,
35
33
  )}
36
34
  >
37
35
  <label className={clsx({ disabled })} htmlFor={id}>
38
- <span className={clsx(isModern ? 'm-r-2' : 'p-r-2', 'np-radio-button')}>
36
+ <span className="m-r-2 np-radio-button">
39
37
  <RadioButton id={id} disabled={disabled} {...otherProps} />
40
38
  </span>
41
39
  <Body
@@ -44,11 +42,7 @@ export default function Radio<T extends string | number = ''>({
44
42
  className="np-radio__text"
45
43
  >
46
44
  {label}
47
- {secondary && (
48
- <Body as="span" className={clsx({ secondary: !isModern })}>
49
- {secondary}
50
- </Body>
51
- )}
45
+ {secondary && <Body as="span">{secondary}</Body>}
52
46
  </Body>
53
47
  {avatar && <span className="np-radio__avatar m-l-auto">{avatar}</span>}
54
48
  </label>
@@ -9,7 +9,7 @@ exports[`Radio shows the avatar when supplied 1`] = `
9
9
  class=""
10
10
  >
11
11
  <span
12
- class="p-r-2 np-radio-button"
12
+ class="m-r-2 np-radio-button"
13
13
  >
14
14
  <input
15
15
  class="sr-only"
@@ -2,6 +2,7 @@ import { StoryObj } from '@storybook/react';
2
2
  import { userEvent, within } from '@storybook/test';
3
3
 
4
4
  import TextareaWithDisplayFormat from './TextareaWithDisplayFormat';
5
+ import { Field } from '../field/Field';
5
6
 
6
7
  export default {
7
8
  component: TextareaWithDisplayFormat,
@@ -11,15 +12,19 @@ export default {
11
12
  type Story = StoryObj<typeof TextareaWithDisplayFormat>;
12
13
 
13
14
  export const Basic: Story = {
14
- render: (args) => {
15
+ render: () => {
16
+ const label = 'Textarea with display format';
17
+ const id = label.replaceAll(' ', '-').toLowerCase();
18
+
15
19
  return (
16
- <>
20
+ <Field id={id} label={label}>
17
21
  <TextareaWithDisplayFormat
22
+ id={id}
18
23
  value="0000"
19
24
  displayPattern="**** - **** - ****"
20
25
  onChange={console.log}
21
26
  />
22
- </>
27
+ </Field>
23
28
  );
24
29
  },
25
30
  // intentionally use interactive typing (over init value via `value` prop)
@@ -23,21 +23,25 @@ const files = [
23
23
  {
24
24
  id: 2,
25
25
  filename: 'purchase-receipt-1.pdf',
26
+ },
27
+ {
28
+ id: 6,
29
+ filename: 'purchase-receipt-1.pdf',
26
30
  url: 'https://wise.com/public-resources/assets/logos/wise/brand_logo_inverse.svg',
27
31
  },
28
32
  {
29
- id: 2,
33
+ id: 3,
30
34
  filename: 'receipt failed.png',
31
35
  status: Status.FAILED,
32
36
  },
33
37
  {
34
- id: 3,
38
+ id: 4,
35
39
  filename: 'receipt failed With error string.png',
36
40
  status: Status.FAILED,
37
41
  error: 'Something went wrong',
38
42
  },
39
43
  {
40
- id: 4,
44
+ id: 5,
41
45
  filename: 'receipt failed With error object.png',
42
46
  status: Status.FAILED,
43
47
  error: { message: 'Something went wrong' },
@@ -1,5 +1,5 @@
1
1
  import { clsx } from 'clsx';
2
- import { useEffect, 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';
@@ -101,6 +101,10 @@ export type UploadInputProps = {
101
101
  Pick<UploadItemProps, 'onDownload'> &
102
102
  CommonProps;
103
103
 
104
+ interface UploadItemRef {
105
+ focus: () => void;
106
+ }
107
+
104
108
  function generateFileId(file: File) {
105
109
  const { name, size } = file;
106
110
  const uploadTimeStamp = new Date().getTime();
@@ -131,8 +135,11 @@ const UploadInput = ({
131
135
  const inputAttributes = useInputAttributes({ nonLabelable: true });
132
136
 
133
137
  const [markedFileForDelete, setMarkedFileForDelete] = useState<UploadedFile | null>(null);
138
+ const [fileToRemoveIndex, setFileToRemoveIndex] = useState<number | null>(null);
134
139
  const [mounted, setMounted] = useState(false);
135
140
  const { formatMessage } = useIntl();
141
+ const itemRefs = useRef<(HTMLDivElement | UploadItemRef | null)[]>([]);
142
+ const uploadInputRef = useRef<HTMLInputElement | null>(null);
136
143
 
137
144
  const PROGRESS_STATUSES = new Set([Status.PENDING, Status.PROCESSING]);
138
145
 
@@ -174,21 +181,28 @@ const UploadInput = ({
174
181
  uploadedFilesListReference.current = updateListItem(uploadedFilesListReference.current);
175
182
  };
176
183
 
184
+ const [fileToRemove, setFileToRemove] = useState<UploadedFile | null>(null);
185
+
177
186
  const removeFile = (file: UploadedFile) => {
178
187
  const { id, status } = file;
188
+ const index = uploadedFiles.findIndex((f) => f.id === file.id);
189
+ setFileToRemoveIndex(index);
179
190
 
180
191
  if (status === Status.FAILED) {
181
- // If removing a failed upload, we're just updating the view
182
192
  removeFileFromList(file);
193
+ setFileToRemove(file);
183
194
  } else if (onDeleteFile && id) {
184
- // Set status to PROCESSING
185
195
  modifyFileInList(file, { status: Status.PROCESSING, error: undefined });
186
196
 
187
- // Notify host app about deletion
188
197
  onDeleteFile(id)
189
- .then(() => removeFileFromList(file))
198
+ .then(() => {
199
+ removeFileFromList(file);
200
+ })
190
201
  .catch((error) => {
191
202
  modifyFileInList(file, { error: error as UploadError });
203
+ })
204
+ .finally(() => {
205
+ setFileToRemove(file);
192
206
  });
193
207
  }
194
208
  };
@@ -263,10 +277,18 @@ const UploadInput = ({
263
277
  continue;
264
278
  }
265
279
 
280
+ // Check if the file is already in the list
281
+ const existingFile = uploadedFiles.find((f) => f.filename === file.name);
282
+ if (existingFile) {
283
+ // Remove the file from the list before adding it again
284
+ removeFileFromList(existingFile);
285
+ }
286
+
287
+ // Add the file to the list
266
288
  formData.append(fileInputName, file);
267
289
  const pendingFile = {
268
- id,
269
- filename: name,
290
+ id: generateFileId(file),
291
+ filename: file.name,
270
292
  status: Status.PENDING,
271
293
  };
272
294
 
@@ -290,6 +312,22 @@ const UploadInput = ({
290
312
  }
291
313
  };
292
314
 
315
+ useLayoutEffect(() => {
316
+ if (fileToRemove && fileToRemoveIndex !== null) {
317
+ requestAnimationFrame(() => {
318
+ const nextFocusIndex = Math.min(fileToRemoveIndex, uploadedFiles.length - 1);
319
+ if (itemRefs.current[nextFocusIndex]) {
320
+ itemRefs.current[nextFocusIndex].focus(); // Focus the next UploadItem
321
+ } else {
322
+ // If there's only one item left, focus the UploadButton
323
+ uploadInputRef.current?.focus();
324
+ }
325
+ });
326
+ setFileToRemove(null); // Reset the state
327
+ setFileToRemoveIndex(null); // Reset the index
328
+ }
329
+ }, [uploadedFiles, fileToRemove, fileToRemoveIndex, itemRefs, uploadInputRef]);
330
+
293
331
  useEffect(() => {
294
332
  setMounted(true);
295
333
  }, []);
@@ -307,9 +345,12 @@ const UploadInput = ({
307
345
  className={clsx('np-upload-input', className, { disabled })}
308
346
  {...inputAttributes}
309
347
  >
310
- {uploadedFiles.map((file) => (
348
+ {uploadedFiles.map((file, index) => (
311
349
  <UploadItem
312
350
  key={file.id}
351
+ ref={(el: UploadItemRef | null) => {
352
+ itemRefs.current[index] = el;
353
+ }}
313
354
  file={file}
314
355
  singleFileUpload={!multiple}
315
356
  canDelete={
@@ -326,6 +367,7 @@ const UploadInput = ({
326
367
  ))}
327
368
  {(multiple || (!multiple && !uploadedFiles.length)) && (
328
369
  <UploadButton
370
+ ref={uploadInputRef}
329
371
  id={id}
330
372
  uploadButtonTitle={uploadButtonTitle}
331
373
  disabled={areMaximumFilesUploadedAlready() || disabled}