@transferwise/components 46.63.0 → 46.65.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 (93) 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/bottomSheet/BottomSheet.js +8 -2
  6. package/build/common/bottomSheet/BottomSheet.js.map +1 -1
  7. package/build/common/bottomSheet/BottomSheet.mjs +8 -2
  8. package/build/common/bottomSheet/BottomSheet.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/drawer/Drawer.js +5 -3
  14. package/build/drawer/Drawer.js.map +1 -1
  15. package/build/drawer/Drawer.mjs +5 -3
  16. package/build/drawer/Drawer.mjs.map +1 -1
  17. package/build/flowNavigation/FlowNavigation.js +1 -1
  18. package/build/flowNavigation/FlowNavigation.js.map +1 -1
  19. package/build/flowNavigation/FlowNavigation.mjs +1 -1
  20. package/build/flowNavigation/FlowNavigation.mjs.map +1 -1
  21. package/build/flowNavigation/animatedLabel/AnimatedLabel.js +89 -15
  22. package/build/flowNavigation/animatedLabel/AnimatedLabel.js.map +1 -1
  23. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs +90 -16
  24. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs.map +1 -1
  25. package/build/instructionsList/InstructionsList.js.map +1 -1
  26. package/build/instructionsList/InstructionsList.mjs.map +1 -1
  27. package/build/main.css +10 -1
  28. package/build/styles/flowNavigation/animatedLabel/AnimatedLabel.css +10 -1
  29. package/build/styles/main.css +10 -1
  30. package/build/switch/Switch.js +3 -27
  31. package/build/switch/Switch.js.map +1 -1
  32. package/build/switch/Switch.mjs +3 -27
  33. package/build/switch/Switch.mjs.map +1 -1
  34. package/build/types/card/Card.d.ts.map +1 -1
  35. package/build/types/circularButton/CircularButton.d.ts.map +1 -1
  36. package/build/types/common/bottomSheet/BottomSheet.d.ts +3 -3
  37. package/build/types/common/bottomSheet/BottomSheet.d.ts.map +1 -1
  38. package/build/types/drawer/Drawer.d.ts +4 -3
  39. package/build/types/drawer/Drawer.d.ts.map +1 -1
  40. package/build/types/flowNavigation/FlowNavigation.d.ts.map +1 -1
  41. package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts +3 -3
  42. package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts.map +1 -1
  43. package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
  44. package/build/types/switch/Switch.d.ts.map +1 -1
  45. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  46. package/build/types/uploadInput/uploadButton/UploadButton.d.ts +1 -1
  47. package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
  48. package/build/types/uploadInput/uploadItem/UploadItem.d.ts +5 -1
  49. package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
  50. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +5 -5
  51. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
  52. package/build/uploadInput/UploadInput.js +42 -11
  53. package/build/uploadInput/UploadInput.js.map +1 -1
  54. package/build/uploadInput/UploadInput.mjs +43 -12
  55. package/build/uploadInput/UploadInput.mjs.map +1 -1
  56. package/build/uploadInput/uploadButton/UploadButton.js +14 -7
  57. package/build/uploadInput/uploadButton/UploadButton.js.map +1 -1
  58. package/build/uploadInput/uploadButton/UploadButton.mjs +15 -8
  59. package/build/uploadInput/uploadButton/UploadButton.mjs.map +1 -1
  60. package/build/uploadInput/uploadItem/UploadItem.js +18 -3
  61. package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
  62. package/build/uploadInput/uploadItem/UploadItem.mjs +18 -3
  63. package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
  64. package/build/uploadInput/uploadItem/UploadItemLink.js +6 -3
  65. package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
  66. package/build/uploadInput/uploadItem/UploadItemLink.mjs +6 -3
  67. package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
  68. package/package.json +3 -3
  69. package/src/card/Card.spec.tsx +4 -5
  70. package/src/card/Card.story.tsx +4 -6
  71. package/src/card/Card.tsx +3 -2
  72. package/src/circularButton/CircularButton.tsx +1 -1
  73. package/src/common/bottomSheet/BottomSheet.tsx +13 -4
  74. package/src/common/locale/index.ts +1 -1
  75. package/src/dateLookup/tableLink/TableLink.tsx +15 -15
  76. package/src/drawer/Drawer.tsx +7 -5
  77. package/src/flowNavigation/FlowNavigation.story.js +69 -17
  78. package/src/flowNavigation/FlowNavigation.tsx +1 -5
  79. package/src/flowNavigation/animatedLabel/AnimatedLabel.css +10 -1
  80. package/src/flowNavigation/animatedLabel/AnimatedLabel.less +10 -1
  81. package/src/flowNavigation/animatedLabel/AnimatedLabel.spec.js +64 -27
  82. package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +102 -20
  83. package/src/instructionsList/InstructionsList.tsx +1 -4
  84. package/src/main.css +10 -1
  85. package/src/switch/Switch.story.tsx +4 -7
  86. package/src/switch/Switch.tsx +1 -24
  87. package/src/switch/__snapshots__/Switch.spec.tsx.snap +2 -44
  88. package/src/uploadInput/UploadInput.tests.story.tsx +7 -3
  89. package/src/uploadInput/UploadInput.tsx +50 -8
  90. package/src/uploadInput/uploadButton/UploadButton.tsx +163 -141
  91. package/src/uploadInput/uploadItem/UploadItem.tsx +146 -124
  92. package/src/uploadInput/uploadItem/UploadItemLink.tsx +23 -25
  93. package/src/flowNavigation/animatedLabel/__snapshots__/AnimatedLabel.spec.js.snap +0 -25
@@ -1,53 +1,90 @@
1
- import { render, screen } from '../../test-utils';
1
+ import { render, screen, mockMatchMedia, userEvent, waitFor } from '../../test-utils';
2
2
 
3
3
  import AnimatedLabel from '.';
4
4
 
5
+ mockMatchMedia();
6
+
5
7
  const props = {
6
8
  activeLabel: 0,
7
- labels: ['label1', 'label2', 'label3'],
9
+ steps: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
8
10
  };
9
11
  jest.useFakeTimers();
10
12
  describe('AnimatedLabel', () => {
11
- it('renders all labels', () => {
12
- const { container } = render(<AnimatedLabel {...props} />);
13
- expect(container).toMatchSnapshot();
14
- });
15
-
16
13
  it('renders only one label with class in', () => {
17
14
  const { container } = render(<AnimatedLabel {...props} />);
18
- expect(screen.getByText(props.labels[0])).toHaveClass('np-animated-label--in');
19
- expect(container.querySelectorAll('.np-animated-label--in')).toHaveLength(1);
20
- });
21
-
22
- it('renders only one label with class out', () => {
23
- const { container } = render(<AnimatedLabel {...props} />);
24
- expect(screen.getByText(props.labels[1])).not.toHaveClass('np-animated-label--in');
25
- expect(container.querySelectorAll('.np-animated-label--in')).toHaveLength(1);
15
+ expect(screen.getByText(props.steps[0].label)).toHaveClass('np-animated-label--active');
16
+ expect(container.querySelectorAll('.np-animated-label--active')).toHaveLength(1);
26
17
  });
27
18
 
28
19
  it('when activeLabel increase it switches class accordingly', () => {
29
20
  const { rerender } = render(<AnimatedLabel {...props} />);
30
- expect(screen.getByText(props.labels[0])).toHaveClass('np-animated-label--in');
31
- expect(screen.getByText(props.labels[1])).not.toHaveClass('np-animated-label--in');
32
- expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--in');
21
+ expect(screen.getByText(props.steps[0].label)).toHaveClass('np-animated-label--active');
22
+ expect(screen.getByText(props.steps[1].label)).not.toHaveClass('np-animated-label--active');
23
+ expect(screen.getByText(props.steps[2].label)).not.toHaveClass('np-animated-label--active');
33
24
 
34
25
  rerender(<AnimatedLabel {...props} activeLabel={1} />);
35
26
 
36
- expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--in');
37
- expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--in');
38
- expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--in');
27
+ expect(screen.getByText(props.steps[0].label)).not.toHaveClass('np-animated-label--active');
28
+ expect(screen.getByText(props.steps[1].label)).toHaveClass('np-animated-label--active');
29
+ expect(screen.getByText(props.steps[2].label)).not.toHaveClass('np-animated-label--active');
39
30
  });
40
31
 
41
32
  it('when activeLabel decrease it switches class accordingly', () => {
42
33
  const { rerender } = render(<AnimatedLabel {...props} activeLabel={1} />);
43
- expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--in');
44
- expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--in');
45
- expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--in');
34
+ expect(screen.getByText(props.steps[0].label)).not.toHaveClass('np-animated-label--active');
35
+ expect(screen.getByText(props.steps[1].label)).toHaveClass('np-animated-label--active');
36
+ expect(screen.getByText(props.steps[2].label)).not.toHaveClass('np-animated-label--active');
46
37
 
47
38
  rerender(<AnimatedLabel {...props} activeLabel={0} />);
48
39
 
49
- expect(screen.getByText(props.labels[0])).toHaveClass('np-animated-label--in');
50
- expect(screen.getByText(props.labels[1])).not.toHaveClass('np-animated-label--in');
51
- expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--in');
40
+ expect(screen.getByText(props.steps[0].label)).toHaveClass('np-animated-label--active');
41
+ expect(screen.getByText(props.steps[1].label)).not.toHaveClass('np-animated-label--active');
42
+ expect(screen.getByText(props.steps[2].label)).not.toHaveClass('np-animated-label--active');
43
+ });
44
+
45
+ it('shows all steps in menu when click on the label', async () => {
46
+ render(<AnimatedLabel {...props} activeLabel={1} />);
47
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
48
+ userEvent.click(screen.getByRole('button', { name: /label2/i }));
49
+ await waitFor(() => {
50
+ expect(screen.getByRole('menu')).toBeInTheDocument();
51
+ });
52
+ expect(screen.getAllByRole('menuitem')).toHaveLength(props.steps.length);
53
+ });
54
+
55
+ it('switches label when click on one of steps as menu itme', async () => {
56
+ const handleClick = jest.fn();
57
+ const handleStepClick = (stepNumber) => {
58
+ return () => handleClick(stepNumber);
59
+ };
60
+ render(
61
+ <AnimatedLabel
62
+ {...{
63
+ activeLabel: 1,
64
+ steps: [
65
+ { label: 'label1', onClick: handleStepClick(0) },
66
+ { label: 'label2', onClick: handleStepClick(1) },
67
+ { label: 'label3', onClick: handleStepClick(2) },
68
+ ],
69
+ }}
70
+ />,
71
+ );
72
+ expect(screen.getByRole('button', { name: /label2/i })).toBeInTheDocument();
73
+ expect(screen.queryByRole('button', { name: /label1/i })).not.toBeInTheDocument();
74
+
75
+ userEvent.click(screen.getByRole('button', { name: /label2/i }));
76
+
77
+ await waitFor(() => {
78
+ expect(screen.getByRole('menu')).toBeInTheDocument();
79
+ });
80
+
81
+ userEvent.click(screen.getByRole('menuitem', { name: /label1/i }));
82
+
83
+ await waitFor(() => {
84
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
85
+ });
86
+
87
+ expect(handleClick).toHaveBeenCalledTimes(1);
88
+ expect(handleClick).toHaveBeenCalledWith(0);
52
89
  });
53
90
  });
@@ -1,32 +1,114 @@
1
1
  import { clsx } from 'clsx';
2
- import React from 'react';
2
+ import { useId, useState } from 'react';
3
3
 
4
- import Body from '../../body';
5
- import { Typography } from '../../common';
4
+ import type { Step } from '../../stepper/Stepper';
5
+ import BottomSheet from '../../common/bottomSheet';
6
+ import Option from '../../common/Option';
7
+ import { Check, ChevronDown } from '@transferwise/icons';
8
+ import { OverlayIdContext, OverlayIdProvider } from '../../provider/overlay/OverlayIdProvider';
9
+ import { List } from '../../listItem';
6
10
 
7
11
  export interface AnimatedLabelProps {
8
12
  activeLabel: number;
9
13
  className?: string;
10
- labels: readonly React.ReactNode[];
14
+ steps: readonly Step[];
11
15
  }
12
16
 
13
- const AnimatedLabel = ({ activeLabel, className, labels }: AnimatedLabelProps) => {
17
+ const AnimatedLabel = ({ activeLabel, className, steps }: AnimatedLabelProps) => {
18
+ const labelId = useId();
19
+ const [showSteps, setShowSteps] = useState(false);
20
+
21
+ function handleStepAction(onClick: Step['onClick']) {
22
+ return () => {
23
+ setShowSteps(false);
24
+ onClick?.();
25
+ };
26
+ }
27
+
14
28
  return (
15
- <Body type={Typography.BODY_LARGE_BOLD} className={clsx('np-animated-label', className)}>
16
- {labels.map((label, index) => {
17
- const nextLabel = index - 1;
18
- return (
19
- <div
20
- key={nextLabel}
21
- className={clsx('text-xs-center', {
22
- 'np-animated-label--in text-ellipsis': index === activeLabel,
23
- })}
24
- >
25
- {label}
26
- </div>
27
- );
28
- })}
29
- </Body>
29
+ <OverlayIdProvider open={showSteps}>
30
+ <OverlayIdContext.Consumer>
31
+ {(overlayId) => {
32
+ return (
33
+ <>
34
+ <button
35
+ type="button"
36
+ id={labelId}
37
+ aria-haspopup="menu"
38
+ aria-controls={overlayId}
39
+ aria-expanded={showSteps}
40
+ className={clsx(
41
+ 'np-animated-label',
42
+ 'btn-unstyled',
43
+ 'np-text-body-large-bold',
44
+ className,
45
+ )}
46
+ onClick={() => setShowSteps(true)}
47
+ >
48
+ {steps.map(({ label }, index) => {
49
+ const isCurrentStep = activeLabel === index;
50
+ const previousIndex = index - 1;
51
+ return (
52
+ <div
53
+ key={previousIndex}
54
+ aria-hidden={!isCurrentStep}
55
+ className={clsx('text-xs-center', 'd-inline-flex', {
56
+ 'np-animated-label--active text-ellipsis': isCurrentStep,
57
+ })}
58
+ >
59
+ {label} <ChevronDown className="m-l-1" size={24} />
60
+ </div>
61
+ );
62
+ })}
63
+ </button>
64
+ <BottomSheet
65
+ role="menu"
66
+ aria-labelledby={labelId}
67
+ open={showSteps}
68
+ onClose={() => setShowSteps(false)}
69
+ >
70
+ <List className="m-b-0 p-a-1">
71
+ {steps.map((step, index) => {
72
+ const isCurrentStep = activeLabel === index;
73
+ const isDisabled = activeLabel < index;
74
+ const itemId = `step-${index}`;
75
+ return (
76
+ <Option
77
+ key={itemId}
78
+ id={itemId}
79
+ as="li"
80
+ role="menuitem"
81
+ decision={false}
82
+ className={clsx('np-animated-label-option', 'p-x-3', 'p-y-1', 'm-y-1', {
83
+ clickable: !isDisabled,
84
+ })}
85
+ title={step.label}
86
+ content={step.hoverLabel}
87
+ button={isCurrentStep ? <Check size={24} /> : null}
88
+ aria-current={isCurrentStep ? 'step' : false}
89
+ aria-disabled={isDisabled}
90
+ disabled={isDisabled}
91
+ isContainerAligned
92
+ {...(!isDisabled && {
93
+ tabIndex: 0,
94
+ onClick: handleStepAction(step.onClick),
95
+ onKeyDown: (event) => {
96
+ event.preventDefault();
97
+ if (event.code === 'Enter' || event.code === 'Space') {
98
+ handleStepAction(step.onClick)();
99
+ }
100
+ },
101
+ })}
102
+ />
103
+ );
104
+ })}
105
+ </List>
106
+ </BottomSheet>
107
+ </>
108
+ );
109
+ }}
110
+ </OverlayIdContext.Consumer>
111
+ </OverlayIdProvider>
30
112
  );
31
113
  };
32
114
 
@@ -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
  ) : (
package/src/main.css CHANGED
@@ -2098,7 +2098,7 @@ button.np-option {
2098
2098
  transform: translateX(-8px);
2099
2099
  transition: all 0.15s ease-in;
2100
2100
  }
2101
- .np-animated-label--in {
2101
+ .np-animated-label--active {
2102
2102
  height: auto;
2103
2103
  opacity: 1;
2104
2104
  position: relative;
@@ -2106,6 +2106,15 @@ button.np-option {
2106
2106
  transform: translateX(0);
2107
2107
  transition: all 0.15s ease-in 0.15s;
2108
2108
  }
2109
+ .np-animated-label-option {
2110
+ border-radius: 10px;
2111
+ border-radius: var(--radius-small);
2112
+ }
2113
+ .np-animated-label-option:not(.disabled):hover,
2114
+ .np-animated-label-option:not(.disabled):focus-visible {
2115
+ outline: var(--ring-outline-color) solid var(--ring-outline-width);
2116
+ outline-offset: var(--ring-outline-offset);
2117
+ }
2109
2118
  .np-back-button {
2110
2119
  color: #00a2dd;
2111
2120
  color: var(--color-interactive-accent);
@@ -10,7 +10,7 @@ export default {
10
10
 
11
11
  export const Basic = () => {
12
12
  const [checked1, setCheck1] = useState(false);
13
- const [checked2, setCheck2] = useState(false);
13
+ const [checked2, setCheck2] = useState(true);
14
14
 
15
15
  return (
16
16
  <div className="d-flex flex-column">
@@ -22,11 +22,10 @@ export const Basic = () => {
22
22
  onClick={() => setCheck1(!checked1)}
23
23
  />
24
24
  </Field>
25
-
26
25
  <Switch
27
- checked={checked2}
28
- className="a-class-name m-t-4"
29
26
  aria-label="I'm a switch without label"
27
+ checked={checked2}
28
+ className="a-class-name"
30
29
  onClick={() => setCheck2(!checked2)}
31
30
  />
32
31
  </div>
@@ -43,17 +42,15 @@ export const Disabled = () => {
43
42
  checked={checked}
44
43
  disabled
45
44
  className="a-class-name"
46
- aria-labelledby="labelID"
47
45
  id="switchId"
48
46
  onClick={() => setCheck(!checked)}
49
47
  />
50
48
  </Field>
51
-
52
49
  <Switch
50
+ aria-label="I'm a switch without label"
53
51
  checked={!checked}
54
52
  disabled
55
53
  className="a-class-name"
56
- aria-labelledby="labelID"
57
54
  id="switchId1"
58
55
  onClick={() => setCheck(!checked)}
59
56
  />
@@ -1,5 +1,3 @@
1
- import { CheckCircleFill, CrossCircleFill } from '@transferwise/icons';
2
- import { useTheme } from '@wise/components-theming';
3
1
  import { clsx } from 'clsx';
4
2
  import type { KeyboardEventHandler, MouseEvent } from 'react';
5
3
 
@@ -26,7 +24,6 @@ export type SwitchProps = CommonProps & {
26
24
  const Switch = (props: SwitchProps) => {
27
25
  const inputAttributes = useInputAttributes({ nonLabelable: true });
28
26
 
29
- const { isModern } = useTheme();
30
27
  const {
31
28
  checked,
32
29
  className,
@@ -44,25 +41,6 @@ const Switch = (props: SwitchProps) => {
44
41
  }
45
42
  };
46
43
 
47
- const returnIcon = () => {
48
- if (isModern) {
49
- return <span className="np-switch--thumb" />;
50
- }
51
-
52
- if (checked) {
53
- return (
54
- <span className="np-switch--thumb">
55
- <CheckCircleFill size={24} />
56
- </span>
57
- );
58
- }
59
- return (
60
- <span className="np-switch--thumb">
61
- <CrossCircleFill size={24} />
62
- </span>
63
- );
64
- };
65
-
66
44
  const ariaLabelledby =
67
45
  (ariaLabel ? undefined : ariaLabelledbyProp) ?? inputAttributes['aria-labelledby'];
68
46
 
@@ -70,7 +48,6 @@ const Switch = (props: SwitchProps) => {
70
48
  <span
71
49
  className={clsx(
72
50
  'np-switch',
73
-
74
51
  {
75
52
  'np-switch--unchecked': !checked,
76
53
  'np-switch--checked': checked,
@@ -89,7 +66,7 @@ const Switch = (props: SwitchProps) => {
89
66
  onClick={!disabled ? onClick : undefined}
90
67
  onKeyDown={!disabled ? handleKeyDown : undefined}
91
68
  >
92
- {returnIcon()}
69
+ <span className="np-switch--thumb" />
93
70
  <input type="checkbox" checked={checked} readOnly />
94
71
  </span>
95
72
  );
@@ -12,28 +12,7 @@ exports[`Switch renders component when checked 1`] = `
12
12
  >
13
13
  <span
14
14
  class="np-switch--thumb"
15
- >
16
- <span
17
- aria-hidden="true"
18
- class="tw-icon tw-icon-check-circle-fill "
19
- data-testid="check-circle-fill-icon"
20
- role="presentation"
21
- >
22
- <svg
23
- fill="currentColor"
24
- focusable="false"
25
- height="24"
26
- viewBox="0 0 24 24"
27
- width="24"
28
- >
29
- <path
30
- clip-rule="evenodd"
31
- d="M12 22.286c5.68 0 10.286-4.605 10.286-10.286C22.286 6.32 17.68 1.714 12 1.714 6.32 1.714 1.714 6.32 1.714 12c0 5.68 4.605 10.286 10.286 10.286Zm-1.32-5.397 7.712-7.711-1.212-1.213-7.109 7.109-3.465-3.466-1.212 1.212 4.068 4.069a.861.861 0 0 0 1.219 0Z"
32
- fill-rule="evenodd"
33
- />
34
- </svg>
35
- </span>
36
- </span>
15
+ />
37
16
  <input
38
17
  checked=""
39
18
  readonly=""
@@ -55,28 +34,7 @@ exports[`Switch renders component when unchecked 1`] = `
55
34
  >
56
35
  <span
57
36
  class="np-switch--thumb"
58
- >
59
- <span
60
- aria-hidden="true"
61
- class="tw-icon tw-icon-cross-circle-fill "
62
- data-testid="cross-circle-fill-icon"
63
- role="presentation"
64
- >
65
- <svg
66
- fill="currentColor"
67
- focusable="false"
68
- height="24"
69
- viewBox="0 0 24 24"
70
- width="24"
71
- >
72
- <path
73
- clip-rule="evenodd"
74
- d="M1.714 12C1.714 6.344 6.343 1.716 12 1.716S22.286 6.343 22.286 12c0 5.657-4.629 10.285-10.286 10.285S1.714 17.658 1.714 12.001ZM12 10.8l4.243-4.244 1.2 1.2L13.2 12l4.243 4.243-1.2 1.2L12 13.2l-4.243 4.243-1.2-1.2L10.8 12 6.557 7.756l1.2-1.2L12 10.8Z"
75
- fill-rule="evenodd"
76
- />
77
- </svg>
78
- </span>
79
- </span>
37
+ />
80
38
  <input
81
39
  readonly=""
82
40
  type="checkbox"
@@ -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}