@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.
- package/build/card/Card.js.map +1 -1
- package/build/card/Card.mjs.map +1 -1
- package/build/circularButton/CircularButton.js.map +1 -1
- package/build/circularButton/CircularButton.mjs.map +1 -1
- package/build/common/bottomSheet/BottomSheet.js +8 -2
- package/build/common/bottomSheet/BottomSheet.js.map +1 -1
- package/build/common/bottomSheet/BottomSheet.mjs +8 -2
- package/build/common/bottomSheet/BottomSheet.mjs.map +1 -1
- package/build/common/locale/index.js.map +1 -1
- package/build/common/locale/index.mjs.map +1 -1
- package/build/dateLookup/tableLink/TableLink.js.map +1 -1
- package/build/dateLookup/tableLink/TableLink.mjs.map +1 -1
- package/build/drawer/Drawer.js +5 -3
- package/build/drawer/Drawer.js.map +1 -1
- package/build/drawer/Drawer.mjs +5 -3
- package/build/drawer/Drawer.mjs.map +1 -1
- package/build/flowNavigation/FlowNavigation.js +1 -1
- package/build/flowNavigation/FlowNavigation.js.map +1 -1
- package/build/flowNavigation/FlowNavigation.mjs +1 -1
- package/build/flowNavigation/FlowNavigation.mjs.map +1 -1
- package/build/flowNavigation/animatedLabel/AnimatedLabel.js +89 -15
- package/build/flowNavigation/animatedLabel/AnimatedLabel.js.map +1 -1
- package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs +90 -16
- package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs.map +1 -1
- package/build/instructionsList/InstructionsList.js.map +1 -1
- package/build/instructionsList/InstructionsList.mjs.map +1 -1
- package/build/main.css +10 -1
- package/build/styles/flowNavigation/animatedLabel/AnimatedLabel.css +10 -1
- package/build/styles/main.css +10 -1
- package/build/switch/Switch.js +3 -27
- package/build/switch/Switch.js.map +1 -1
- package/build/switch/Switch.mjs +3 -27
- package/build/switch/Switch.mjs.map +1 -1
- package/build/types/card/Card.d.ts.map +1 -1
- package/build/types/circularButton/CircularButton.d.ts.map +1 -1
- package/build/types/common/bottomSheet/BottomSheet.d.ts +3 -3
- package/build/types/common/bottomSheet/BottomSheet.d.ts.map +1 -1
- package/build/types/drawer/Drawer.d.ts +4 -3
- package/build/types/drawer/Drawer.d.ts.map +1 -1
- package/build/types/flowNavigation/FlowNavigation.d.ts.map +1 -1
- package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts +3 -3
- package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts.map +1 -1
- package/build/types/instructionsList/InstructionsList.d.ts.map +1 -1
- package/build/types/switch/Switch.d.ts.map +1 -1
- package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
- package/build/types/uploadInput/uploadButton/UploadButton.d.ts +1 -1
- package/build/types/uploadInput/uploadButton/UploadButton.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts +5 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +5 -5
- package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
- package/build/uploadInput/UploadInput.js +42 -11
- package/build/uploadInput/UploadInput.js.map +1 -1
- package/build/uploadInput/UploadInput.mjs +43 -12
- package/build/uploadInput/UploadInput.mjs.map +1 -1
- package/build/uploadInput/uploadButton/UploadButton.js +14 -7
- package/build/uploadInput/uploadButton/UploadButton.js.map +1 -1
- package/build/uploadInput/uploadButton/UploadButton.mjs +15 -8
- package/build/uploadInput/uploadButton/UploadButton.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.js +18 -3
- package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.mjs +18 -3
- package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.js +6 -3
- package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.mjs +6 -3
- package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
- package/package.json +3 -3
- package/src/card/Card.spec.tsx +4 -5
- package/src/card/Card.story.tsx +4 -6
- package/src/card/Card.tsx +3 -2
- package/src/circularButton/CircularButton.tsx +1 -1
- package/src/common/bottomSheet/BottomSheet.tsx +13 -4
- package/src/common/locale/index.ts +1 -1
- package/src/dateLookup/tableLink/TableLink.tsx +15 -15
- package/src/drawer/Drawer.tsx +7 -5
- package/src/flowNavigation/FlowNavigation.story.js +69 -17
- package/src/flowNavigation/FlowNavigation.tsx +1 -5
- package/src/flowNavigation/animatedLabel/AnimatedLabel.css +10 -1
- package/src/flowNavigation/animatedLabel/AnimatedLabel.less +10 -1
- package/src/flowNavigation/animatedLabel/AnimatedLabel.spec.js +64 -27
- package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +102 -20
- package/src/instructionsList/InstructionsList.tsx +1 -4
- package/src/main.css +10 -1
- package/src/switch/Switch.story.tsx +4 -7
- package/src/switch/Switch.tsx +1 -24
- package/src/switch/__snapshots__/Switch.spec.tsx.snap +2 -44
- package/src/uploadInput/UploadInput.tests.story.tsx +7 -3
- package/src/uploadInput/UploadInput.tsx +50 -8
- package/src/uploadInput/uploadButton/UploadButton.tsx +163 -141
- package/src/uploadInput/uploadItem/UploadItem.tsx +146 -124
- package/src/uploadInput/uploadItem/UploadItemLink.tsx +23 -25
- 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
|
-
|
|
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.
|
|
19
|
-
expect(container.querySelectorAll('.np-animated-label--
|
|
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.
|
|
31
|
-
expect(screen.getByText(props.
|
|
32
|
-
expect(screen.getByText(props.
|
|
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.
|
|
37
|
-
expect(screen.getByText(props.
|
|
38
|
-
expect(screen.getByText(props.
|
|
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.
|
|
44
|
-
expect(screen.getByText(props.
|
|
45
|
-
expect(screen.getByText(props.
|
|
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.
|
|
50
|
-
expect(screen.getByText(props.
|
|
51
|
-
expect(screen.getByText(props.
|
|
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
|
|
2
|
+
import { useId, useState } from 'react';
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
|
|
14
|
+
steps: readonly Step[];
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
const AnimatedLabel = ({ activeLabel, className,
|
|
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
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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--
|
|
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(
|
|
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
|
/>
|
package/src/switch/Switch.tsx
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
33
|
+
id: 3,
|
|
30
34
|
filename: 'receipt failed.png',
|
|
31
35
|
status: Status.FAILED,
|
|
32
36
|
},
|
|
33
37
|
{
|
|
34
|
-
id:
|
|
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:
|
|
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(() =>
|
|
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}
|