@transferwise/components 46.74.1 → 46.76.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 +8 -4
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +8 -4
- package/build/alert/Alert.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/main.css +3 -0
- 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/styles/alert/Alert.css +3 -0
- package/build/styles/main.css +3 -0
- package/build/types/alert/Alert.d.ts +17 -2
- package/build/types/alert/Alert.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/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/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/package.json +3 -3
- package/src/alert/Alert.css +3 -0
- package/src/alert/Alert.less +3 -0
- package/src/alert/Alert.spec.story.tsx +51 -5
- package/src/alert/Alert.spec.tsx +14 -0
- package/src/alert/Alert.story.tsx +109 -13
- package/src/alert/Alert.tsx +25 -5
- 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/main.css +3 -0
- package/src/main.less +1 -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/alert/Alert.tsx
CHANGED
|
@@ -50,6 +50,11 @@ export interface AlertProps {
|
|
|
50
50
|
className?: string;
|
|
51
51
|
/** An optional icon. If not provided, we will default the icon to something appropriate for the type */
|
|
52
52
|
icon?: React.ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Override for [StatusIcon's default, accessible name](/?path=/docs/other-statusicon-accessibility--docs)
|
|
55
|
+
* announced by the screen readers
|
|
56
|
+
* */
|
|
57
|
+
statusIconLabel?: string;
|
|
53
58
|
/** Title for the alert component */
|
|
54
59
|
title?: string;
|
|
55
60
|
/** The main body of the alert. Accepts plain text and bold words specified with **double stars */
|
|
@@ -59,8 +64,18 @@ export interface AlertProps {
|
|
|
59
64
|
/** The type dictates which icon and colour will be used */
|
|
60
65
|
type?: AlertType;
|
|
61
66
|
variant?: `${Variant}`;
|
|
62
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Controls rendering of the Alert component. <br />
|
|
69
|
+
* Toggle this prop instead using conditional rendering and logical AND (&&)
|
|
70
|
+
* operator, to make the component work with screen readers.
|
|
71
|
+
* @deprecated use `dynamicRender`
|
|
72
|
+
* */
|
|
63
73
|
active?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Toggle this prop when dealing with multiple, dynamic Alerts, to make them
|
|
76
|
+
* work with screen readers. This is especially helpful for the BFF use cases.
|
|
77
|
+
* */
|
|
78
|
+
dynamicRender?: boolean;
|
|
64
79
|
/** @deprecated Use `InlineAlert` instead. */
|
|
65
80
|
arrow?: `${AlertArrowPosition}`;
|
|
66
81
|
/** @deprecated Use `message` instead. Be aware `message` only accepts plain text or text with **bold** markdown. */
|
|
@@ -91,6 +106,7 @@ export default function Alert({
|
|
|
91
106
|
className,
|
|
92
107
|
dismissible,
|
|
93
108
|
icon,
|
|
109
|
+
statusIconLabel,
|
|
94
110
|
onDismiss,
|
|
95
111
|
message,
|
|
96
112
|
size,
|
|
@@ -98,6 +114,7 @@ export default function Alert({
|
|
|
98
114
|
type = 'neutral',
|
|
99
115
|
variant = 'desktop',
|
|
100
116
|
active = true,
|
|
117
|
+
dynamicRender = false,
|
|
101
118
|
}: AlertProps) {
|
|
102
119
|
useEffect(() => {
|
|
103
120
|
if (arrow !== undefined) {
|
|
@@ -142,17 +159,20 @@ export default function Alert({
|
|
|
142
159
|
|
|
143
160
|
const [shouldShow, setShouldShow] = useState<boolean>();
|
|
144
161
|
useEffect(() => {
|
|
145
|
-
if (shouldShow === undefined || !active) {
|
|
162
|
+
if ((shouldShow === undefined && !dynamicRender) || !active) {
|
|
146
163
|
setShouldShow(active);
|
|
147
164
|
} else {
|
|
148
165
|
setTimeout(() => setShouldShow(active), WDS_LIVE_REGION_DELAY_MS);
|
|
149
166
|
}
|
|
150
|
-
}, [active, shouldShow]);
|
|
167
|
+
}, [active, shouldShow, dynamicRender]);
|
|
151
168
|
|
|
152
169
|
const closeButtonReference = useRef<HTMLButtonElement>(null);
|
|
153
170
|
|
|
154
171
|
return (
|
|
155
|
-
<div
|
|
172
|
+
<div
|
|
173
|
+
role={resolvedType === Sentiment.NEGATIVE ? 'alert' : 'status'}
|
|
174
|
+
className="wds-alert__liveRegion"
|
|
175
|
+
>
|
|
156
176
|
{shouldShow && (
|
|
157
177
|
<div
|
|
158
178
|
className={clsx(
|
|
@@ -189,7 +209,7 @@ export default function Alert({
|
|
|
189
209
|
{icon ? (
|
|
190
210
|
<div className="alert__icon">{icon}</div>
|
|
191
211
|
) : (
|
|
192
|
-
<StatusIcon size={Size.LARGE} sentiment={resolvedType} />
|
|
212
|
+
<StatusIcon size={Size.LARGE} sentiment={resolvedType} iconLabel={statusIconLabel} />
|
|
193
213
|
)}
|
|
194
214
|
<div className="alert__message">
|
|
195
215
|
<div>
|
package/src/field/Field.spec.tsx
CHANGED
|
@@ -71,6 +71,25 @@ describe('Field', () => {
|
|
|
71
71
|
expect(screen.getByLabelText(/Phone number/)).toHaveAttribute('aria-describedby');
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it("should allow for InlineAlert's StatusIcon override via `messageIconLabel` prop", () => {
|
|
75
|
+
const customIconLabel = 'My custom icon label';
|
|
76
|
+
|
|
77
|
+
render(
|
|
78
|
+
<Field
|
|
79
|
+
label="Phone number"
|
|
80
|
+
description="This is help text"
|
|
81
|
+
sentiment="negative"
|
|
82
|
+
message="This is error text"
|
|
83
|
+
messageIconLabel={customIconLabel}
|
|
84
|
+
>
|
|
85
|
+
<Input />
|
|
86
|
+
</Field>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(screen.queryByRole('graphics-symbol', { name: 'Error.' })).not.toBeInTheDocument();
|
|
90
|
+
expect(screen.getByRole('graphics-symbol', { name: customIconLabel })).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
74
93
|
it('should show or hide (Optional) suffix depending on required prop', () => {
|
|
75
94
|
render(
|
|
76
95
|
<Field label="Phone number" required={false} description="This is help text">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { Input } from '../inputs/Input';
|
|
4
|
-
import { Field } from './Field';
|
|
4
|
+
import { Field, FieldProps } from './Field';
|
|
5
5
|
import { Sentiment } from '../common';
|
|
6
6
|
import DateInput from '../dateInput';
|
|
7
7
|
import { fn } from '@storybook/test';
|
|
@@ -14,9 +14,14 @@ export default {
|
|
|
14
14
|
component: Field,
|
|
15
15
|
title: 'Field',
|
|
16
16
|
tags: ['autodocs'],
|
|
17
|
+
argTypes: {
|
|
18
|
+
messageIconLabel: {
|
|
19
|
+
control: 'text',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
17
22
|
};
|
|
18
23
|
|
|
19
|
-
export const Basic = () => {
|
|
24
|
+
export const Basic = (args: FieldProps) => {
|
|
20
25
|
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
21
26
|
return (
|
|
22
27
|
<div className="row">
|
|
@@ -40,6 +45,7 @@ export const Basic = () => {
|
|
|
40
45
|
description="This a field Description"
|
|
41
46
|
sentiment={Sentiment.NEGATIVE}
|
|
42
47
|
message="Validation error, please take a look"
|
|
48
|
+
messageIconLabel={args.messageIconLabel}
|
|
43
49
|
>
|
|
44
50
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
45
51
|
</Field>
|
|
@@ -74,6 +80,7 @@ export const Basic = () => {
|
|
|
74
80
|
description="This a TextArea Description"
|
|
75
81
|
message={lorem10}
|
|
76
82
|
sentiment="negative"
|
|
83
|
+
messageIconLabel={args.messageIconLabel}
|
|
77
84
|
>
|
|
78
85
|
<TextArea />
|
|
79
86
|
</Field>
|
|
@@ -93,7 +100,7 @@ export const Basic = () => {
|
|
|
93
100
|
);
|
|
94
101
|
};
|
|
95
102
|
|
|
96
|
-
export const WithStatusMessages = () => {
|
|
103
|
+
export const WithStatusMessages = (args: FieldProps) => {
|
|
97
104
|
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
98
105
|
return (
|
|
99
106
|
<div>
|
|
@@ -101,6 +108,7 @@ export const WithStatusMessages = () => {
|
|
|
101
108
|
label="Text Input with Positive Message"
|
|
102
109
|
sentiment={Sentiment.POSITIVE}
|
|
103
110
|
message="Positive message"
|
|
111
|
+
messageIconLabel={args.messageIconLabel}
|
|
104
112
|
>
|
|
105
113
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
106
114
|
</Field>
|
|
@@ -109,6 +117,7 @@ export const WithStatusMessages = () => {
|
|
|
109
117
|
label="Text Input with Warning Message"
|
|
110
118
|
sentiment={Sentiment.WARNING}
|
|
111
119
|
message="Warning message"
|
|
120
|
+
messageIconLabel={args.messageIconLabel}
|
|
112
121
|
>
|
|
113
122
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
114
123
|
</Field>
|
|
@@ -117,6 +126,7 @@ export const WithStatusMessages = () => {
|
|
|
117
126
|
label="Text Input with Validation Error"
|
|
118
127
|
sentiment={Sentiment.NEGATIVE}
|
|
119
128
|
message="This is a required field"
|
|
129
|
+
messageIconLabel={args.messageIconLabel}
|
|
120
130
|
>
|
|
121
131
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
122
132
|
</Field>
|
|
@@ -127,6 +137,7 @@ export const WithStatusMessages = () => {
|
|
|
127
137
|
hint="This is a helpful message"
|
|
128
138
|
sentiment={Sentiment.NEGATIVE}
|
|
129
139
|
message="Validation error, please take a look"
|
|
140
|
+
messageIconLabel={args.messageIconLabel}
|
|
130
141
|
>
|
|
131
142
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
132
143
|
</Field>
|
|
@@ -136,12 +147,17 @@ export const WithStatusMessages = () => {
|
|
|
136
147
|
label="Text Input with deprecated `error` & `hint` props"
|
|
137
148
|
hint="This is a helpful message"
|
|
138
149
|
error="Validation error, please take a look"
|
|
150
|
+
messageIconLabel={args.messageIconLabel}
|
|
139
151
|
>
|
|
140
152
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
141
153
|
</Field>
|
|
142
154
|
|
|
143
155
|
{/* instance of info via `message` property, this is rare case (e.g MoneyInput) */}
|
|
144
|
-
<Field
|
|
156
|
+
<Field
|
|
157
|
+
label="Text Input with Info under the field"
|
|
158
|
+
message="This is a helpful message"
|
|
159
|
+
messageIconLabel={args.messageIconLabel}
|
|
160
|
+
>
|
|
145
161
|
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
146
162
|
</Field>
|
|
147
163
|
</div>
|
package/src/field/Field.tsx
CHANGED
|
@@ -20,6 +20,11 @@ export type FieldProps = {
|
|
|
20
20
|
/** @deprecated use `description` prop instead */
|
|
21
21
|
hint?: React.ReactNode;
|
|
22
22
|
message?: React.ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Override for the [InlineAlert icon's default, accessible name](/?path=/docs/other-statusicon-accessibility--docs)
|
|
25
|
+
* announced by the screen readers
|
|
26
|
+
* */
|
|
27
|
+
messageIconLabel?: string;
|
|
23
28
|
description?: React.ReactNode;
|
|
24
29
|
/** @deprecated use `message` and `type={Sentiment.NEGATIVE}` prop instead */
|
|
25
30
|
error?: React.ReactNode;
|
|
@@ -33,6 +38,7 @@ export const Field = ({
|
|
|
33
38
|
label,
|
|
34
39
|
required = true,
|
|
35
40
|
message: propMessage,
|
|
41
|
+
messageIconLabel,
|
|
36
42
|
hint,
|
|
37
43
|
description = hint,
|
|
38
44
|
sentiment: propType = Sentiment.NEUTRAL,
|
|
@@ -97,7 +103,7 @@ export const Field = ({
|
|
|
97
103
|
)}
|
|
98
104
|
|
|
99
105
|
{message && (
|
|
100
|
-
<InlineAlert type={sentiment} id={messageId}>
|
|
106
|
+
<InlineAlert type={sentiment} id={messageId} iconLabel={messageIconLabel}>
|
|
101
107
|
{message}
|
|
102
108
|
</InlineAlert>
|
|
103
109
|
)}
|
package/src/i18n/en.json
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"neptune.SelectInput.noResultsFound": "No results found",
|
|
28
28
|
"neptune.SelectOption.action.label": "Choose",
|
|
29
29
|
"neptune.SelectOption.selected.action.label": "Change chosen option",
|
|
30
|
+
"neptune.StatusIcon.iconLabel.error": "Error:",
|
|
31
|
+
"neptune.StatusIcon.iconLabel.information": "Information:",
|
|
32
|
+
"neptune.StatusIcon.iconLabel.pending": "Pending:",
|
|
33
|
+
"neptune.StatusIcon.iconLabel.success": "Success:",
|
|
34
|
+
"neptune.StatusIcon.iconLabel.warning": "Warning:",
|
|
30
35
|
"neptune.Summary.statusDone": "Item done",
|
|
31
36
|
"neptune.Summary.statusNotDone": "Item to do",
|
|
32
37
|
"neptune.Summary.statusPending": "Item pending",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
import { Sentiment } from '../common';
|
|
3
2
|
import { render, screen, mockMatchMedia } from '../test-utils';
|
|
4
3
|
|
|
@@ -81,4 +80,16 @@ describe('InlineAlert', () => {
|
|
|
81
80
|
expect(screen.getByTestId('status-icon')).toBeInTheDocument();
|
|
82
81
|
});
|
|
83
82
|
});
|
|
83
|
+
|
|
84
|
+
describe('other props', () => {
|
|
85
|
+
it('should respect `iconLabel` override', () => {
|
|
86
|
+
const customLabel = 'Custom Label';
|
|
87
|
+
render(
|
|
88
|
+
<InlineAlert type={Sentiment.WARNING} iconLabel={customLabel}>
|
|
89
|
+
{message}
|
|
90
|
+
</InlineAlert>,
|
|
91
|
+
);
|
|
92
|
+
expect(screen.getByLabelText(customLabel)).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
84
95
|
});
|
|
@@ -8,6 +8,7 @@ import Body from '../body';
|
|
|
8
8
|
export interface InlineAlertProps {
|
|
9
9
|
id?: string;
|
|
10
10
|
type?: `${Sentiment}`;
|
|
11
|
+
iconLabel?: string;
|
|
11
12
|
className?: string;
|
|
12
13
|
children: ReactNode;
|
|
13
14
|
}
|
|
@@ -32,6 +33,7 @@ const iconTypes = new Set<NonNullable<InlineAlertProps['type']>>([
|
|
|
32
33
|
export default function InlineAlert({
|
|
33
34
|
id,
|
|
34
35
|
type = 'neutral',
|
|
36
|
+
iconLabel,
|
|
35
37
|
className,
|
|
36
38
|
children,
|
|
37
39
|
}: InlineAlertProps) {
|
|
@@ -46,7 +48,9 @@ export default function InlineAlert({
|
|
|
46
48
|
className,
|
|
47
49
|
)}
|
|
48
50
|
>
|
|
49
|
-
{iconTypes.has(type) &&
|
|
51
|
+
{iconTypes.has(type) && (
|
|
52
|
+
<StatusIcon sentiment={type} size={Size.SMALL} iconLabel={iconLabel} />
|
|
53
|
+
)}
|
|
50
54
|
<div>{children}</div>
|
|
51
55
|
</Body>
|
|
52
56
|
);
|
package/src/main.css
CHANGED
package/src/main.less
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@import "./criticalBanner/CriticalCommsBanner.less";
|
|
2
2
|
@import "./accordion/Accordion.less";
|
|
3
3
|
@import "./actionButton/ActionButton.less";
|
|
4
|
+
@import "./alert/Alert.less";
|
|
4
5
|
@import "./avatar/Avatar.less";
|
|
5
6
|
@import "./badge/Badge.less";
|
|
6
7
|
@import "./button/Button.less";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Meta, Source } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Other/StatusIcon/Accessibility" />
|
|
4
|
+
|
|
5
|
+
# Accessibility
|
|
6
|
+
|
|
7
|
+
By default, the component offers accessible names for all the icons, to ensure their meaning is conveyed to sighted and non-sighted users alike. The built-in labels are derived from the provided `sentiment` prop (as listed below), but can be overridden via `iconLabel` prop:
|
|
8
|
+
|
|
9
|
+
<Source code={`
|
|
10
|
+
Sentiment.NEGATIVE -> 'Error:'
|
|
11
|
+
Sentiment.POSITIVE -> 'Success:'
|
|
12
|
+
Sentiment.WARNING -> 'Warning:'
|
|
13
|
+
Sentiment.PENDING -> 'Pending:'
|
|
14
|
+
Sentiment.NEUTRAL -> 'Information:'
|
|
15
|
+
|
|
16
|
+
// deprecated
|
|
17
|
+
Sentiment.ERROR -> 'Error:'
|
|
18
|
+
Sentiment.INFO -> 'Information:'
|
|
19
|
+
Sentiment.SUCCESS -> 'Success:'
|
|
20
|
+
`} dark />
|
|
21
|
+
|
|
22
|
+
The purpose of the colon (`:`) is to make the screen reader briefly pause and separate the icon label from the rest of the content.
|
|
23
|
+
|
|
24
|
+
## Presentational icons
|
|
25
|
+
|
|
26
|
+
In some very rare cases, where the sentiment/status is already clearly communicated for **all** users (e.g. a duplicated icon in the `Upload` component), it might be more user-friendly to treat the `StatusIcon` as purely presentational.
|
|
27
|
+
|
|
28
|
+
To achieve it, simply set the `iconLabel` to `null` and that will result in `role="none" aria-hidden` applied to the icon.
|
|
@@ -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(
|