@transferwise/components 46.74.0 → 46.75.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/alert/Alert.js +3 -1
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +3 -1
- package/build/alert/Alert.mjs.map +1 -1
- package/build/dimmer/Dimmer.js +3 -1
- package/build/dimmer/Dimmer.js.map +1 -1
- package/build/dimmer/Dimmer.mjs +3 -1
- package/build/dimmer/Dimmer.mjs.map +1 -1
- package/build/drawer/Drawer.js +2 -0
- package/build/drawer/Drawer.js.map +1 -1
- package/build/drawer/Drawer.mjs +2 -0
- package/build/drawer/Drawer.mjs.map +1 -1
- package/build/field/Field.js +2 -0
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +2 -0
- package/build/field/Field.mjs.map +1 -1
- package/build/i18n/en.json +5 -0
- package/build/i18n/en.json.js +5 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +5 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/inlineAlert/InlineAlert.js +3 -1
- package/build/inlineAlert/InlineAlert.js.map +1 -1
- package/build/inlineAlert/InlineAlert.mjs +3 -1
- package/build/inlineAlert/InlineAlert.mjs.map +1 -1
- package/build/modal/Modal.js +3 -0
- package/build/modal/Modal.js.map +1 -1
- package/build/modal/Modal.mjs +3 -0
- package/build/modal/Modal.mjs.map +1 -1
- package/build/statusIcon/StatusIcon.js +50 -16
- package/build/statusIcon/StatusIcon.js.map +1 -1
- package/build/statusIcon/StatusIcon.messages.js +24 -0
- package/build/statusIcon/StatusIcon.messages.js.map +1 -0
- package/build/statusIcon/StatusIcon.messages.mjs +22 -0
- package/build/statusIcon/StatusIcon.messages.mjs.map +1 -0
- package/build/statusIcon/StatusIcon.mjs +48 -14
- package/build/statusIcon/StatusIcon.mjs.map +1 -1
- package/build/types/alert/Alert.d.ts +6 -1
- package/build/types/alert/Alert.d.ts.map +1 -1
- package/build/types/dimmer/Dimmer.d.ts +2 -1
- package/build/types/dimmer/Dimmer.d.ts.map +1 -1
- package/build/types/drawer/Drawer.d.ts +2 -1
- package/build/types/drawer/Drawer.d.ts.map +1 -1
- package/build/types/field/Field.d.ts +6 -1
- package/build/types/field/Field.d.ts.map +1 -1
- package/build/types/inlineAlert/InlineAlert.d.ts +2 -1
- package/build/types/inlineAlert/InlineAlert.d.ts.map +1 -1
- package/build/types/modal/Modal.d.ts +2 -1
- package/build/types/modal/Modal.d.ts.map +1 -1
- package/build/types/statusIcon/StatusIcon.d.ts +7 -1
- package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
- package/build/types/statusIcon/StatusIcon.messages.d.ts +29 -0
- package/build/types/statusIcon/StatusIcon.messages.d.ts.map +1 -0
- package/build/types/upload/Upload.d.ts +5 -0
- package/build/types/upload/Upload.d.ts.map +1 -1
- package/build/types/upload/steps/uploadImageStep/uploadImageStep.d.ts +1 -0
- package/build/types/upload/steps/uploadImageStep/uploadImageStep.d.ts.map +1 -1
- package/build/types/uploadInput/UploadInput.d.ts +9 -0
- package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts +16 -1
- package/build/types/uploadInput/uploadItem/UploadItem.d.ts.map +1 -1
- package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts.map +1 -1
- package/build/upload/Upload.js +4 -2
- package/build/upload/Upload.js.map +1 -1
- package/build/upload/Upload.mjs +4 -2
- package/build/upload/Upload.mjs.map +1 -1
- package/build/upload/steps/uploadImageStep/uploadImageStep.js +6 -3
- package/build/upload/steps/uploadImageStep/uploadImageStep.js.map +1 -1
- package/build/upload/steps/uploadImageStep/uploadImageStep.mjs +6 -3
- package/build/upload/steps/uploadImageStep/uploadImageStep.mjs.map +1 -1
- package/build/uploadInput/UploadInput.js +71 -66
- package/build/uploadInput/UploadInput.js.map +1 -1
- package/build/uploadInput/UploadInput.mjs +72 -67
- package/build/uploadInput/UploadInput.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.js +13 -4
- package/build/uploadInput/uploadItem/UploadItem.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItem.mjs +13 -4
- package/build/uploadInput/uploadItem/UploadItem.mjs.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.js +1 -0
- package/build/uploadInput/uploadItem/UploadItemLink.js.map +1 -1
- package/build/uploadInput/uploadItem/UploadItemLink.mjs +1 -0
- package/build/uploadInput/uploadItem/UploadItemLink.mjs.map +1 -1
- package/package.json +1 -1
- package/src/alert/Alert.spec.tsx +10 -0
- package/src/alert/Alert.tsx +7 -1
- package/src/dimmer/Dimmer.spec.js +8 -0
- package/src/dimmer/Dimmer.tsx +4 -0
- package/src/drawer/Drawer.spec.js +25 -6
- package/src/drawer/Drawer.tsx +3 -1
- package/src/field/Field.spec.tsx +19 -0
- package/src/field/Field.story.tsx +20 -4
- package/src/field/Field.tsx +7 -1
- package/src/i18n/en.json +5 -0
- package/src/inlineAlert/InlineAlert.spec.tsx +12 -1
- package/src/inlineAlert/InlineAlert.tsx +5 -1
- package/src/modal/Modal.spec.js +19 -1
- package/src/modal/Modal.tsx +4 -0
- package/src/statusIcon/StatusIcon.docs.mdx +28 -0
- package/src/statusIcon/StatusIcon.messages.ts +34 -0
- package/src/statusIcon/StatusIcon.spec.tsx +39 -4
- package/src/statusIcon/StatusIcon.story.tsx +15 -6
- package/src/statusIcon/StatusIcon.tsx +63 -14
- package/src/upload/Upload.spec.js +19 -0
- package/src/upload/Upload.tsx +7 -0
- package/src/upload/steps/uploadImageStep/uploadImageStep.spec.js +13 -0
- package/src/upload/steps/uploadImageStep/uploadImageStep.tsx +15 -4
- package/src/uploadInput/UploadInput.spec.tsx +121 -9
- package/src/uploadInput/UploadInput.tests.story.tsx +207 -140
- package/src/uploadInput/UploadInput.tsx +110 -77
- package/src/uploadInput/uploadItem/UploadItem.spec.tsx +1 -0
- package/src/uploadInput/uploadItem/UploadItem.tsx +30 -6
- package/src/uploadInput/uploadItem/UploadItemLink.tsx +9 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
|
|
3
|
+
export default defineMessages({
|
|
4
|
+
errorLabel: {
|
|
5
|
+
id: 'neptune.StatusIcon.iconLabel.error',
|
|
6
|
+
defaultMessage: 'Error:',
|
|
7
|
+
description:
|
|
8
|
+
'Visually hidden label read by screen readers, describing the Error icon – normally a prefix to remaining visible content, e.g. validation result.',
|
|
9
|
+
},
|
|
10
|
+
successLabel: {
|
|
11
|
+
id: 'neptune.StatusIcon.iconLabel.success',
|
|
12
|
+
defaultMessage: 'Success:',
|
|
13
|
+
description:
|
|
14
|
+
'Visually hidden label read by screen readers, describing the Success icon – normally a prefix to remaining visible content, e.g. validation result.',
|
|
15
|
+
},
|
|
16
|
+
warningLabel: {
|
|
17
|
+
id: 'neptune.StatusIcon.iconLabel.warning',
|
|
18
|
+
defaultMessage: 'Warning:',
|
|
19
|
+
description:
|
|
20
|
+
'Visually hidden label read by screen readers, describing the Warning icon – normally a prefix to remaining visible content, e.g. validation result.',
|
|
21
|
+
},
|
|
22
|
+
pendingLabel: {
|
|
23
|
+
id: 'neptune.StatusIcon.iconLabel.pending',
|
|
24
|
+
defaultMessage: 'Pending:',
|
|
25
|
+
description:
|
|
26
|
+
'Visually hidden label read by screen readers, describing the Pending icon – normally a prefix to remaining visible content, e.g. validation result.',
|
|
27
|
+
},
|
|
28
|
+
informationLabel: {
|
|
29
|
+
id: 'neptune.StatusIcon.iconLabel.information',
|
|
30
|
+
defaultMessage: 'Information:',
|
|
31
|
+
description:
|
|
32
|
+
'Visually hidden label read by screen readers, describing the Information icon – normally a prefix to remaining visible content, e.g. validation result.',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { Sentiment, Size
|
|
1
|
+
import { Sentiment, Size } from '../common';
|
|
2
2
|
import { render, cleanup, screen, mockMatchMedia } from '../test-utils';
|
|
3
3
|
|
|
4
|
-
import StatusIcon from '.';
|
|
4
|
+
import StatusIcon, { StatusIconProps } from '.';
|
|
5
5
|
|
|
6
6
|
mockMatchMedia();
|
|
7
7
|
|
|
8
8
|
describe('StatusIcon', () => {
|
|
9
9
|
const renderStatusIcon = (props?: {
|
|
10
|
-
sentiment?:
|
|
11
|
-
size?:
|
|
10
|
+
sentiment?: StatusIconProps['sentiment'];
|
|
11
|
+
size?: StatusIconProps['size'];
|
|
12
|
+
iconLabel?: StatusIconProps['iconLabel'];
|
|
12
13
|
}) => {
|
|
13
14
|
return render(
|
|
14
15
|
<StatusIcon
|
|
15
16
|
sentiment={props?.sentiment || Sentiment.NEUTRAL}
|
|
16
17
|
size={props?.size || Size.MEDIUM}
|
|
18
|
+
iconLabel={props?.iconLabel}
|
|
17
19
|
/>,
|
|
18
20
|
);
|
|
19
21
|
};
|
|
@@ -79,4 +81,37 @@ describe('StatusIcon', () => {
|
|
|
79
81
|
renderStatusIcon();
|
|
80
82
|
expect(screen.getByTestId('info-icon')).toHaveClass('light');
|
|
81
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('accessible name', () => {
|
|
86
|
+
it.each([
|
|
87
|
+
['Error', Sentiment.NEGATIVE],
|
|
88
|
+
['Success', Sentiment.POSITIVE],
|
|
89
|
+
['Warning', Sentiment.WARNING],
|
|
90
|
+
['Pending', Sentiment.PENDING],
|
|
91
|
+
['Information', Sentiment.NEUTRAL],
|
|
92
|
+
['Error', Sentiment.ERROR],
|
|
93
|
+
['Information', Sentiment.INFO],
|
|
94
|
+
['Success', Sentiment.SUCCESS],
|
|
95
|
+
])("should set '%s' as an accessible name for the '%s' sentiment", (label, sentiment) => {
|
|
96
|
+
renderStatusIcon({ sentiment });
|
|
97
|
+
|
|
98
|
+
expect(screen.getByLabelText(`${label}:`)).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should allow for `iconLabel` overrides', () => {
|
|
102
|
+
const iconLabel = 'Custom label';
|
|
103
|
+
renderStatusIcon({ sentiment: Sentiment.NEGATIVE, iconLabel });
|
|
104
|
+
|
|
105
|
+
expect(screen.getByLabelText(iconLabel)).toBeInTheDocument();
|
|
106
|
+
expect(screen.queryByLabelText(`Error:`)).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should allow for `iconLabel` to be `null` to keep the icon presentational', () => {
|
|
110
|
+
const iconLabel = null;
|
|
111
|
+
renderStatusIcon({ sentiment: Sentiment.NEGATIVE, iconLabel });
|
|
112
|
+
|
|
113
|
+
expect(screen.queryByRole(`graphics-symbol`)).not.toBeInTheDocument();
|
|
114
|
+
expect(screen.queryByLabelText(`Error:`)).not.toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
82
117
|
});
|
|
@@ -7,16 +7,25 @@ import StatusIcon from './StatusIcon';
|
|
|
7
7
|
export default {
|
|
8
8
|
component: StatusIcon,
|
|
9
9
|
title: 'Other/StatusIcon',
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
iconLabel: {
|
|
13
|
+
control: 'text',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
10
16
|
} satisfies Meta<typeof StatusIcon>;
|
|
11
17
|
|
|
12
18
|
type Story = StoryObj<typeof StatusIcon>;
|
|
13
19
|
|
|
14
|
-
export const Basic: Story = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
export const Basic: Story = {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ignored by the screen readers. Use with care.
|
|
24
|
+
*/
|
|
25
|
+
export const Presentational: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
iconLabel: null,
|
|
28
|
+
},
|
|
20
29
|
};
|
|
21
30
|
|
|
22
31
|
export const Variants: Story = {
|
|
@@ -1,34 +1,80 @@
|
|
|
1
1
|
import { Info, Alert, Cross, Check, ClockBorderless } from '@transferwise/icons';
|
|
2
2
|
import { clsx } from 'clsx';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
3
4
|
|
|
4
5
|
import { SizeSmall, SizeMedium, SizeLarge, Sentiment, Size, Breakpoint } from '../common';
|
|
5
6
|
import Circle, { CircleProps } from '../common/circle';
|
|
6
7
|
import { useMedia } from '../common/hooks/useMedia';
|
|
7
8
|
|
|
9
|
+
import messages from './StatusIcon.messages';
|
|
10
|
+
|
|
8
11
|
export type StatusIconProps = {
|
|
9
12
|
sentiment: `${Sentiment}`;
|
|
10
13
|
size: SizeSmall | SizeMedium | SizeLarge;
|
|
14
|
+
/**
|
|
15
|
+
* Override for the sentiment's-derived, default, accessible
|
|
16
|
+
* name announced by the screen readers. <br />
|
|
17
|
+
* Using `null` will render the icon purely presentational.
|
|
18
|
+
* */
|
|
19
|
+
iconLabel?: string | null;
|
|
11
20
|
};
|
|
12
21
|
|
|
13
|
-
const iconTypeMap = {
|
|
14
|
-
positive: Check,
|
|
15
|
-
neutral: Info,
|
|
16
|
-
warning: Alert,
|
|
17
|
-
negative: Cross,
|
|
18
|
-
pending: ClockBorderless,
|
|
19
|
-
info: Info,
|
|
20
|
-
error: Cross,
|
|
21
|
-
success: Check,
|
|
22
|
-
} satisfies Record<`${Sentiment}`, React.ElementType>;
|
|
23
|
-
|
|
24
22
|
const mapLegacySize = {
|
|
25
23
|
[String(Size.SMALL)]: 16,
|
|
26
24
|
[String(Size.MEDIUM)]: 40,
|
|
27
25
|
[String(Size.LARGE)]: 48,
|
|
28
26
|
} satisfies Record<string, CircleProps['size']>;
|
|
29
27
|
|
|
30
|
-
const StatusIcon = ({
|
|
31
|
-
|
|
28
|
+
const StatusIcon = ({
|
|
29
|
+
sentiment = 'neutral',
|
|
30
|
+
size: sizeProp = 'md',
|
|
31
|
+
iconLabel,
|
|
32
|
+
}: StatusIconProps) => {
|
|
33
|
+
const intl = useIntl();
|
|
34
|
+
|
|
35
|
+
const iconMetaBySentiment: Record<
|
|
36
|
+
`${Sentiment}`,
|
|
37
|
+
{
|
|
38
|
+
Icon: React.ElementType;
|
|
39
|
+
defaultIconLabel: string;
|
|
40
|
+
}
|
|
41
|
+
> = {
|
|
42
|
+
[Sentiment.NEGATIVE]: {
|
|
43
|
+
Icon: Cross,
|
|
44
|
+
defaultIconLabel: intl.formatMessage(messages.errorLabel),
|
|
45
|
+
},
|
|
46
|
+
[Sentiment.POSITIVE]: {
|
|
47
|
+
Icon: Check,
|
|
48
|
+
defaultIconLabel: intl.formatMessage(messages.successLabel),
|
|
49
|
+
},
|
|
50
|
+
[Sentiment.WARNING]: {
|
|
51
|
+
Icon: Alert,
|
|
52
|
+
defaultIconLabel: intl.formatMessage(messages.warningLabel),
|
|
53
|
+
},
|
|
54
|
+
[Sentiment.PENDING]: {
|
|
55
|
+
Icon: ClockBorderless,
|
|
56
|
+
defaultIconLabel: intl.formatMessage(messages.pendingLabel),
|
|
57
|
+
},
|
|
58
|
+
[Sentiment.NEUTRAL]: {
|
|
59
|
+
Icon: Info,
|
|
60
|
+
defaultIconLabel: intl.formatMessage(messages.informationLabel),
|
|
61
|
+
},
|
|
62
|
+
// deprecated
|
|
63
|
+
[Sentiment.ERROR]: {
|
|
64
|
+
Icon: Cross,
|
|
65
|
+
defaultIconLabel: intl.formatMessage(messages.errorLabel),
|
|
66
|
+
},
|
|
67
|
+
[Sentiment.INFO]: {
|
|
68
|
+
Icon: Info,
|
|
69
|
+
defaultIconLabel: intl.formatMessage(messages.informationLabel),
|
|
70
|
+
},
|
|
71
|
+
[Sentiment.SUCCESS]: {
|
|
72
|
+
Icon: Check,
|
|
73
|
+
defaultIconLabel: intl.formatMessage(messages.successLabel),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const { Icon, defaultIconLabel } = iconMetaBySentiment[sentiment];
|
|
77
|
+
|
|
32
78
|
const iconColor = sentiment === 'warning' || sentiment === 'pending' ? 'dark' : 'light';
|
|
33
79
|
const isTinyViewport = useMedia(`(max-width: ${Breakpoint.ZOOM_400}px)`);
|
|
34
80
|
const size = sizeProp === Size.SMALL && isTinyViewport ? 32 : mapLegacySize[sizeProp];
|
|
@@ -39,7 +85,10 @@ const StatusIcon = ({ sentiment = 'neutral', size: sizeProp = 'md' }: StatusIcon
|
|
|
39
85
|
data-testid="status-icon"
|
|
40
86
|
className={clsx('status-circle', `status-circle-${sizeProp}`, sentiment)}
|
|
41
87
|
>
|
|
42
|
-
<Icon
|
|
88
|
+
<Icon
|
|
89
|
+
className={clsx('status-icon', iconColor)}
|
|
90
|
+
title={iconLabel === null ? undefined : iconLabel || defaultIconLabel}
|
|
91
|
+
/>
|
|
43
92
|
</Circle>
|
|
44
93
|
);
|
|
45
94
|
};
|
|
@@ -7,6 +7,7 @@ import { ANIMATION_DURATION_IN_MS } from '../processIndicator';
|
|
|
7
7
|
import { CompleteStep, UploadImageStep, ProcessingStep } from './steps';
|
|
8
8
|
|
|
9
9
|
import Upload from '.';
|
|
10
|
+
import { Cross } from '@transferwise/icons';
|
|
10
11
|
|
|
11
12
|
mockMatchMedia();
|
|
12
13
|
|
|
@@ -252,6 +253,7 @@ describe('Upload', () => {
|
|
|
252
253
|
...UPLOADIMAGE_STEP_PROPS,
|
|
253
254
|
isComplete: false,
|
|
254
255
|
errorMessage: 'csFailureText',
|
|
256
|
+
errorIconLabel: undefined,
|
|
255
257
|
usButtonText: 'Try again',
|
|
256
258
|
usHelpImage: null,
|
|
257
259
|
});
|
|
@@ -282,5 +284,22 @@ describe('Upload', () => {
|
|
|
282
284
|
|
|
283
285
|
expect(props.onSuccess).toHaveBeenCalledTimes(1);
|
|
284
286
|
});
|
|
287
|
+
|
|
288
|
+
it('should respect `errorIconLabel` override', async () => {
|
|
289
|
+
const errorIconLabel = 'Custom error icon label';
|
|
290
|
+
component = mount(<Upload {...props} errorIconLabel={errorIconLabel} />);
|
|
291
|
+
const upload = component.children();
|
|
292
|
+
asyncFileRead.mockImplementation(async () => {
|
|
293
|
+
throw 'An error';
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
await upload.instance().fileDropped(TEST_FILE);
|
|
298
|
+
});
|
|
299
|
+
await waitForUpload();
|
|
300
|
+
component.update();
|
|
301
|
+
|
|
302
|
+
expect(component.find(Cross).at(1).prop('title')).toBe(errorIconLabel);
|
|
303
|
+
});
|
|
285
304
|
});
|
|
286
305
|
});
|
package/src/upload/Upload.tsx
CHANGED
|
@@ -56,6 +56,11 @@ export interface UploadProps extends WrappedComponentProps {
|
|
|
56
56
|
usHelpImage?: React.ReactNode;
|
|
57
57
|
usLabel?: string;
|
|
58
58
|
usPlaceholder?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Override for the [InlineAlert icon's default, accessible name](/?path=/docs/other-statusicon-accessibility--docs)
|
|
61
|
+
* announced by the screen readers
|
|
62
|
+
* */
|
|
63
|
+
errorIconLabel?: string;
|
|
59
64
|
/** @deprecated Only a single variant exists, please remove this prop. */
|
|
60
65
|
uploadStep?: `${UploadStep}`;
|
|
61
66
|
onCancel?: () => void;
|
|
@@ -338,6 +343,7 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
338
343
|
csSuccessText,
|
|
339
344
|
size,
|
|
340
345
|
intl,
|
|
346
|
+
errorIconLabel,
|
|
341
347
|
} = this.props;
|
|
342
348
|
|
|
343
349
|
const {
|
|
@@ -422,6 +428,7 @@ export class Upload extends Component<UploadProps, UploadState> {
|
|
|
422
428
|
? response.status
|
|
423
429
|
: undefined,
|
|
424
430
|
)}
|
|
431
|
+
errorIconLabel={errorIconLabel}
|
|
425
432
|
/>
|
|
426
433
|
)}
|
|
427
434
|
{isProcessing && (
|
|
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
|
|
3
3
|
import StatusIcon from '../../../statusIcon/StatusIcon';
|
|
4
4
|
|
|
5
5
|
import UploadImageStep from '.';
|
|
6
|
+
import { InlineAlert } from '../../../index';
|
|
6
7
|
|
|
7
8
|
describe('uploadImageStep', () => {
|
|
8
9
|
const UPLOADIMAGE_STEP_PROPS = {
|
|
@@ -66,6 +67,18 @@ describe('uploadImageStep', () => {
|
|
|
66
67
|
expect(component.find('.test-image')).toHaveLength(1);
|
|
67
68
|
});
|
|
68
69
|
|
|
70
|
+
it('should respect `errorIconLabel` override', () => {
|
|
71
|
+
const errorIconLabel = 'Error icon label';
|
|
72
|
+
component = shallow(
|
|
73
|
+
<UploadImageStep
|
|
74
|
+
{...UPLOADIMAGE_STEP_PROPS}
|
|
75
|
+
errorMessage="Maximum filesize is 2MB."
|
|
76
|
+
errorIconLabel={errorIconLabel}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
expect(component.find(InlineAlert).prop('iconLabel')).toBe(errorIconLabel);
|
|
80
|
+
});
|
|
81
|
+
|
|
69
82
|
describe('when errorMessage is not empty', () => {
|
|
70
83
|
it('renders errorMessage and icon when error is true', () => {
|
|
71
84
|
component = shallow(
|
|
@@ -14,6 +14,7 @@ export interface UploadImageStepProps {
|
|
|
14
14
|
usLabel: string;
|
|
15
15
|
usPlaceholder: string;
|
|
16
16
|
errorMessage?: string | string[];
|
|
17
|
+
errorIconLabel?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export default class UploadImageStep extends PureComponent<UploadImageStepProps> {
|
|
@@ -34,7 +35,7 @@ export default class UploadImageStep extends PureComponent<UploadImageStepProps>
|
|
|
34
35
|
if (errorMessage) {
|
|
35
36
|
return (
|
|
36
37
|
<div className="d-flex flex-column align-items-center">
|
|
37
|
-
<StatusIcon size={Size.LARGE} sentiment={Sentiment.NEGATIVE} />
|
|
38
|
+
<StatusIcon size={Size.LARGE} sentiment={Sentiment.NEGATIVE} iconLabel={null} />
|
|
38
39
|
</div>
|
|
39
40
|
);
|
|
40
41
|
}
|
|
@@ -55,8 +56,16 @@ export default class UploadImageStep extends PureComponent<UploadImageStepProps>
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
render() {
|
|
58
|
-
const {
|
|
59
|
-
|
|
59
|
+
const {
|
|
60
|
+
errorMessage,
|
|
61
|
+
isComplete,
|
|
62
|
+
errorIconLabel,
|
|
63
|
+
usAccept,
|
|
64
|
+
usButtonText,
|
|
65
|
+
usDisabled,
|
|
66
|
+
usLabel,
|
|
67
|
+
usPlaceholder,
|
|
68
|
+
} = this.props;
|
|
60
69
|
|
|
61
70
|
return (
|
|
62
71
|
<div className="droppable-default-card" aria-hidden={isComplete}>
|
|
@@ -88,7 +97,9 @@ export default class UploadImageStep extends PureComponent<UploadImageStepProps>
|
|
|
88
97
|
{errorMessage && (
|
|
89
98
|
<div className="upload-error-message">
|
|
90
99
|
<div className="m-t-3 has-error">
|
|
91
|
-
<InlineAlert type={Sentiment.NEGATIVE}
|
|
100
|
+
<InlineAlert type={Sentiment.NEGATIVE} iconLabel={errorIconLabel}>
|
|
101
|
+
{errorMessage}
|
|
102
|
+
</InlineAlert>
|
|
92
103
|
</div>
|
|
93
104
|
</div>
|
|
94
105
|
)}
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
import { within } from '@testing-library/react';
|
|
1
|
+
import { Matcher, within } from '@testing-library/react';
|
|
2
2
|
import { userEvent } from '@testing-library/user-event';
|
|
3
3
|
import { act } from 'react';
|
|
4
4
|
|
|
5
5
|
import { Status } from '../common';
|
|
6
6
|
import { Field } from '../field/Field';
|
|
7
|
-
import { mockMatchMedia, render, screen, waitFor
|
|
7
|
+
import { mockMatchMedia, render, screen, waitFor } from '../test-utils';
|
|
8
8
|
|
|
9
9
|
import UploadInput, { UploadInputProps } from './UploadInput';
|
|
10
10
|
import { TEST_IDS as UPLOAD_BUTTON_TEST_IDS } from './uploadButton/UploadButton';
|
|
11
|
-
import { TEST_IDS as UPLOAD_ITEM_TEST_IDS } from './uploadItem/UploadItem';
|
|
12
11
|
|
|
13
12
|
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });
|
|
14
13
|
|
|
14
|
+
const deleteFileAndWaitForFocus = async (fileToDeleteTestId: Matcher, nextFocusTestId: Matcher) => {
|
|
15
|
+
const fileToDelete = screen.getByTestId(fileToDeleteTestId);
|
|
16
|
+
|
|
17
|
+
await user.click(within(fileToDelete).getByLabelText('Remove file', { exact: false }));
|
|
18
|
+
|
|
19
|
+
const removeButton = screen.queryByText('Remove');
|
|
20
|
+
if (removeButton) {
|
|
21
|
+
await user.click(removeButton);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await waitFor(() => {
|
|
25
|
+
expect(screen.getByTestId(nextFocusTestId)).toHaveFocus();
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
15
29
|
mockMatchMedia();
|
|
16
30
|
|
|
17
31
|
describe('UploadInput', () => {
|
|
@@ -139,15 +153,21 @@ describe('UploadInput', () => {
|
|
|
139
153
|
onFilesChange,
|
|
140
154
|
});
|
|
141
155
|
|
|
142
|
-
const fileToDelete = screen.
|
|
143
|
-
within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
|
|
156
|
+
const fileToDelete = screen.getByTestId('1-uploadItem');
|
|
144
157
|
await act(async () => {
|
|
158
|
+
within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
|
|
145
159
|
await jest.runOnlyPendingTimersAsync();
|
|
146
160
|
});
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
await act(async () => {
|
|
163
|
+
screen.getByText('Remove').click();
|
|
164
|
+
await jest.runOnlyPendingTimersAsync();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await waitFor(() => {
|
|
168
|
+
expect(screen.queryByTestId('1-uploadItem')).not.toBeInTheDocument();
|
|
169
|
+
});
|
|
149
170
|
|
|
150
|
-
await waitForElementToBeRemoved(fileToDelete);
|
|
151
171
|
expect(props.onDeleteFile).toHaveBeenCalledWith(files[0].id);
|
|
152
172
|
|
|
153
173
|
expect(onFilesChange).toHaveBeenCalledTimes(2);
|
|
@@ -190,9 +210,9 @@ describe('UploadInput', () => {
|
|
|
190
210
|
onFilesChange,
|
|
191
211
|
});
|
|
192
212
|
|
|
193
|
-
const fileToDelete = screen.
|
|
194
|
-
within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
|
|
213
|
+
const fileToDelete = screen.getByTestId('1-uploadItem');
|
|
195
214
|
await act(async () => {
|
|
215
|
+
within(fileToDelete).getByLabelText('Remove file', { exact: false }).click();
|
|
196
216
|
await jest.runOnlyPendingTimersAsync();
|
|
197
217
|
});
|
|
198
218
|
|
|
@@ -212,6 +232,98 @@ describe('UploadInput', () => {
|
|
|
212
232
|
|
|
213
233
|
expect(screen.queryByLabelText('Remove file ', { exact: false })).not.toBeInTheDocument();
|
|
214
234
|
});
|
|
235
|
+
|
|
236
|
+
it('should focus the next item after a file is deleted', async () => {
|
|
237
|
+
const files = [
|
|
238
|
+
{ id: 1, filename: 'Sales-2024-invoice.pdf', status: Status.DONE },
|
|
239
|
+
{ id: 2, filename: 'CoWork-0317-invoice.pdf', status: Status.DONE },
|
|
240
|
+
{ id: 3, filename: 'purchase-receipt.pdf', status: Status.DONE },
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
renderComponent({ ...props, files, multiple: true, onFilesChange });
|
|
244
|
+
|
|
245
|
+
// Delete the first file and expect focus to move to the next one
|
|
246
|
+
await deleteFileAndWaitForFocus('1-uploadItem', '2-action');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should focus the previous item after the last file is deleted', async () => {
|
|
250
|
+
const files = [
|
|
251
|
+
{
|
|
252
|
+
id: 1,
|
|
253
|
+
filename: 'Sales-2024-invoice.pdf',
|
|
254
|
+
status: Status.DONE,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: 2,
|
|
258
|
+
filename: 'CoWork-0317-invoice.pdf',
|
|
259
|
+
status: Status.DONE,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 3,
|
|
263
|
+
filename: 'purchase-receipt.pdf',
|
|
264
|
+
status: Status.DONE,
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
renderComponent({
|
|
269
|
+
...props,
|
|
270
|
+
files,
|
|
271
|
+
multiple: true,
|
|
272
|
+
onFilesChange,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await deleteFileAndWaitForFocus('3-uploadItem', '2-action');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should focus the upload input after the only file is deleted', async () => {
|
|
279
|
+
const singleFile = [
|
|
280
|
+
{
|
|
281
|
+
id: 3,
|
|
282
|
+
filename: 'purchase-receipt.pdf',
|
|
283
|
+
status: Status.DONE,
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
renderComponent({
|
|
288
|
+
...props,
|
|
289
|
+
files: singleFile,
|
|
290
|
+
multiple: true,
|
|
291
|
+
onFilesChange,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await deleteFileAndWaitForFocus('3-uploadItem', 'uploadInput');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should focus on the next item or upload input after each file is deleted in sequence', async () => {
|
|
298
|
+
const filesWithFailed = [
|
|
299
|
+
{
|
|
300
|
+
id: 1,
|
|
301
|
+
filename: 'Sales-2024-invoice.pdf',
|
|
302
|
+
status: Status.DONE,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 2,
|
|
306
|
+
filename: 'CoWork-0317-invoice.pdf',
|
|
307
|
+
status: Status.FAILED,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: 3,
|
|
311
|
+
filename: 'purchase-receipt.pdf',
|
|
312
|
+
status: Status.DONE,
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
renderComponent({
|
|
317
|
+
...props,
|
|
318
|
+
files: filesWithFailed,
|
|
319
|
+
multiple: true,
|
|
320
|
+
onFilesChange,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await deleteFileAndWaitForFocus('3-uploadItem', '2-action');
|
|
324
|
+
await deleteFileAndWaitForFocus('1-uploadItem', '2-action');
|
|
325
|
+
await deleteFileAndWaitForFocus('2-uploadItem', 'uploadInput');
|
|
326
|
+
});
|
|
215
327
|
});
|
|
216
328
|
|
|
217
329
|
describe('Max File Upload limit', () => {
|