@transferwise/components 0.0.0-experimental-cdb8834 → 0.0.0-experimental-2b6c540
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/main.css +5 -11
- package/build/prompt/ActionPrompt/ActionPrompt.js +0 -2
- package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
- package/build/prompt/ActionPrompt/ActionPrompt.mjs +0 -2
- package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -1
- package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
- package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.js +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.js.map +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs +1 -1
- package/build/prompt/InlinePrompt/InlinePrompt.mjs.map +1 -1
- package/build/styles/main.css +5 -11
- package/build/styles/prompt/ActionPrompt/ActionPrompt.css +0 -4
- package/build/styles/prompt/InfoPrompt/InfoPrompt.css +3 -3
- package/build/styles/prompt/InlinePrompt/InlinePrompt.css +2 -3
- package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +0 -1
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +7 -0
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +4 -2
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/iconButton/IconButton.story.tsx +173 -48
- package/src/iconButton/IconButton.test.story.tsx +194 -0
- package/src/listItem/_stories/ListItem.story.tsx +0 -1
- package/src/main.css +5 -11
- package/src/prompt/ActionPrompt/ActionPrompt.accessibility.docs.mdx +1 -17
- package/src/prompt/ActionPrompt/ActionPrompt.css +0 -4
- package/src/prompt/ActionPrompt/ActionPrompt.less +1 -5
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +262 -119
- package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +86 -3
- package/src/prompt/ActionPrompt/ActionPrompt.tsx +9 -6
- package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +79 -0
- package/src/prompt/InfoPrompt/InfoPrompt.css +3 -3
- package/src/prompt/InfoPrompt/InfoPrompt.less +3 -4
- package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +80 -109
- package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +54 -1
- package/src/prompt/InfoPrompt/InfoPrompt.tsx +4 -2
- package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +63 -0
- package/src/prompt/InlinePrompt/InlinePrompt.css +2 -3
- package/src/prompt/InlinePrompt/InlinePrompt.less +2 -2
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +25 -30
- package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +21 -0
- package/src/prompt/InlinePrompt/InlinePrompt.test.tsx +3 -10
- package/src/prompt/InlinePrompt/InlinePrompt.tsx +1 -1
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +0 -1
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +1 -2
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import { ReactElement, useState } from 'react';
|
|
2
2
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
3
|
import { fn } from 'storybook/test';
|
|
4
|
-
import { Bank } from '@transferwise/icons';
|
|
5
|
-
import { ActionPrompt } from './ActionPrompt';
|
|
6
|
-
import { lorem10
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
const meta: Meta<typeof ActionPrompt> = {
|
|
11
|
-
title: 'Prompts/ActionPrompt',
|
|
12
|
-
component: ActionPrompt,
|
|
13
|
-
tags: ['new'],
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export default meta;
|
|
17
|
-
type Story = StoryObj<typeof ActionPrompt>;
|
|
4
|
+
import { Bank, Star, Travel, Briefcase } from '@transferwise/icons';
|
|
5
|
+
import { ActionPrompt, type ActionPromptProps } from './ActionPrompt';
|
|
6
|
+
import { lorem10 } from '../../test-utils';
|
|
7
|
+
import Button from '../../button';
|
|
8
|
+
import Title from '../../title';
|
|
18
9
|
|
|
19
10
|
const withComponentGrid =
|
|
20
11
|
({ maxWidth = 'auto', gap = '1rem' } = {}) =>
|
|
@@ -35,149 +26,281 @@ const withComponentGrid =
|
|
|
35
26
|
</div>
|
|
36
27
|
);
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{(['negative', 'warning', 'neutral', 'success', 'proposition'] as const).map(
|
|
43
|
-
(sentiment) => (
|
|
44
|
-
<ActionPrompt
|
|
45
|
-
key={sentiment}
|
|
46
|
-
sentiment={sentiment}
|
|
47
|
-
title="Title"
|
|
48
|
-
description={lorem10}
|
|
49
|
-
action={{ label: 'Primary', onClick: fn() }}
|
|
50
|
-
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
51
|
-
className="m-b-2"
|
|
52
|
-
onDismiss={fn()}
|
|
53
|
-
/>
|
|
54
|
-
),
|
|
55
|
-
)}
|
|
56
|
-
</>
|
|
57
|
-
);
|
|
58
|
-
},
|
|
29
|
+
const meta: Meta<typeof ActionPrompt> = {
|
|
30
|
+
title: 'Prompts/ActionPrompt',
|
|
31
|
+
component: ActionPrompt,
|
|
32
|
+
tags: ['new'],
|
|
59
33
|
decorators: [withComponentGrid()],
|
|
34
|
+
args: {
|
|
35
|
+
title: 'Action Required',
|
|
36
|
+
description: 'Please complete the following action to continue.',
|
|
37
|
+
action: { label: 'Continue', onClick: fn() },
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
sentiment: {
|
|
41
|
+
control: 'select',
|
|
42
|
+
options: ['success', 'negative', 'neutral', 'warning', 'proposition'],
|
|
43
|
+
},
|
|
44
|
+
title: {
|
|
45
|
+
control: 'text',
|
|
46
|
+
table: {
|
|
47
|
+
type: { summary: 'ReactNode' },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
description: {
|
|
51
|
+
control: 'text',
|
|
52
|
+
table: {
|
|
53
|
+
type: { summary: 'ReactNode' },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
parameters: {
|
|
58
|
+
docs: {
|
|
59
|
+
toc: true,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
export
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
export default meta;
|
|
65
|
+
type Story = StoryObj<typeof ActionPrompt>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convenience controls for previewing rich markup,
|
|
69
|
+
* not otherwise possible via Storybook
|
|
70
|
+
*/
|
|
71
|
+
type PreviewStoryArgs = ActionPromptProps & {
|
|
72
|
+
previewMedia: ActionPromptProps['media'];
|
|
73
|
+
previewOnDismiss: boolean;
|
|
74
|
+
previewSecondaryAction: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const previewArgGroup = {
|
|
78
|
+
category: 'Storybook Preview options',
|
|
79
|
+
type: {
|
|
80
|
+
summary: undefined,
|
|
81
|
+
},
|
|
65
82
|
};
|
|
66
83
|
|
|
67
|
-
|
|
84
|
+
const MEDIA_OPTIONS: Record<string, ActionPromptProps['media']> = {
|
|
85
|
+
undefined,
|
|
86
|
+
'Custom image (no badge)': { imgSrc: '../../wise-card.svg' },
|
|
87
|
+
'Avatar: Profile image': { avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } },
|
|
88
|
+
'Avatar: Business (GB badge)': {
|
|
89
|
+
avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } },
|
|
90
|
+
},
|
|
91
|
+
'Avatar: Personal (EU badge)': {
|
|
92
|
+
avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } },
|
|
93
|
+
},
|
|
94
|
+
'Avatar: Icon (Bank)': { avatar: { asset: <Bank /> } },
|
|
95
|
+
'Avatar: Icon (Star)': { avatar: { asset: <Star /> } },
|
|
96
|
+
'Avatar: Icon (Travel)': { avatar: { asset: <Travel /> } },
|
|
97
|
+
'Avatar: Icon (Briefcase)': { avatar: { asset: <Briefcase /> } },
|
|
98
|
+
'Avatar: Initials': { avatar: { profileName: 'John Doe' } },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const previewArgTypes = {
|
|
102
|
+
previewMedia: {
|
|
103
|
+
name: 'Preview with `media`',
|
|
104
|
+
control: 'select',
|
|
105
|
+
options: [
|
|
106
|
+
'undefined',
|
|
107
|
+
'Custom image (no badge)',
|
|
108
|
+
'Avatar: Profile image',
|
|
109
|
+
'Avatar: Business (GB badge)',
|
|
110
|
+
'Avatar: Personal (EU badge)',
|
|
111
|
+
'Avatar: Icon (Bank)',
|
|
112
|
+
'Avatar: Icon (Star)',
|
|
113
|
+
'Avatar: Icon (Travel)',
|
|
114
|
+
'Avatar: Icon (Briefcase)',
|
|
115
|
+
'Avatar: Initials',
|
|
116
|
+
],
|
|
117
|
+
mapping: MEDIA_OPTIONS,
|
|
118
|
+
table: previewArgGroup,
|
|
119
|
+
},
|
|
120
|
+
previewOnDismiss: {
|
|
121
|
+
name: 'Preview with `onDismiss`',
|
|
122
|
+
control: 'boolean',
|
|
123
|
+
table: previewArgGroup,
|
|
124
|
+
},
|
|
125
|
+
previewSecondaryAction: {
|
|
126
|
+
name: 'Preview with `actionSecondary`',
|
|
127
|
+
control: 'boolean',
|
|
128
|
+
table: previewArgGroup,
|
|
129
|
+
},
|
|
130
|
+
} as const;
|
|
131
|
+
|
|
132
|
+
const getPropsForPreview = (
|
|
133
|
+
args: PreviewStoryArgs,
|
|
134
|
+
): [ActionPromptProps, Partial<ActionPromptProps>] => {
|
|
135
|
+
const { previewMedia, previewOnDismiss, previewSecondaryAction, ...props } = args;
|
|
136
|
+
|
|
137
|
+
return [
|
|
138
|
+
props,
|
|
139
|
+
{
|
|
140
|
+
media: previewMedia,
|
|
141
|
+
onDismiss: previewOnDismiss ? fn() : undefined,
|
|
142
|
+
actionSecondary: previewSecondaryAction ? { label: 'Cancel', onClick: fn() } : undefined,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const Playground: StoryObj<PreviewStoryArgs> = {
|
|
148
|
+
argTypes: {
|
|
149
|
+
sentiment: {
|
|
150
|
+
control: 'radio',
|
|
151
|
+
options: ['success', 'negative', 'neutral', 'warning', 'proposition'],
|
|
152
|
+
},
|
|
153
|
+
...previewArgTypes,
|
|
154
|
+
},
|
|
68
155
|
args: {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
156
|
+
previewMedia: { avatar: undefined },
|
|
157
|
+
previewOnDismiss: true,
|
|
158
|
+
previewSecondaryAction: true,
|
|
159
|
+
},
|
|
160
|
+
render: (args: PreviewStoryArgs) => {
|
|
161
|
+
const [props, previewProps] = getPropsForPreview(args);
|
|
162
|
+
return <ActionPrompt {...props} {...previewProps} />;
|
|
74
163
|
},
|
|
75
164
|
};
|
|
76
165
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
sentiment="success"
|
|
90
|
-
title="Session expiring soon"
|
|
91
|
-
action={{ label: 'Stay logged in', onClick: fn() }}
|
|
92
|
-
/>
|
|
166
|
+
/**
|
|
167
|
+
* ActionPrompt supports multiple sentiments to communicate different types of messages:
|
|
168
|
+
* - `neutral` (default): General prompts
|
|
169
|
+
* - `success`: Success confirmations
|
|
170
|
+
* - `negative`: Error or critical actions
|
|
171
|
+
* - `warning`: Warning messages
|
|
172
|
+
* - `proposition`: Promotional or feature prompts
|
|
173
|
+
*/
|
|
174
|
+
export const Sentiments: Story = {
|
|
175
|
+
render: (args) => (
|
|
176
|
+
<>
|
|
177
|
+
{(['success', 'warning', 'negative', 'neutral', 'proposition'] as const).map((sentiment) => (
|
|
93
178
|
<ActionPrompt
|
|
94
|
-
|
|
95
|
-
|
|
179
|
+
key={sentiment}
|
|
180
|
+
sentiment={sentiment}
|
|
181
|
+
title={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} Action`}
|
|
182
|
+
description={lorem10}
|
|
183
|
+
action={{ label: 'Primary', onClick: fn() }}
|
|
184
|
+
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
96
185
|
/>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
decorators: [withComponentGrid()],
|
|
186
|
+
))}
|
|
187
|
+
</>
|
|
188
|
+
),
|
|
101
189
|
};
|
|
102
190
|
|
|
103
|
-
|
|
191
|
+
/**
|
|
192
|
+
* ActionPrompt can have just a primary action, or both primary and secondary actions.
|
|
193
|
+
*/
|
|
194
|
+
export const Actions: Story = {
|
|
195
|
+
render: () => (
|
|
196
|
+
<>
|
|
197
|
+
<ActionPrompt
|
|
198
|
+
sentiment="proposition"
|
|
199
|
+
title="Primary action only"
|
|
200
|
+
description="This prompt has only one action button."
|
|
201
|
+
action={{ label: 'Get started', onClick: fn() }}
|
|
202
|
+
/>
|
|
203
|
+
<ActionPrompt
|
|
204
|
+
sentiment="warning"
|
|
205
|
+
title="Primary and secondary actions"
|
|
206
|
+
description="This prompt has both action buttons."
|
|
207
|
+
action={{ label: 'Confirm', onClick: fn() }}
|
|
208
|
+
actionSecondary={{ label: 'Cancel', onClick: fn() }}
|
|
209
|
+
/>
|
|
210
|
+
</>
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* ActionPrompt supports various media types to enhance visual communication.
|
|
216
|
+
*
|
|
217
|
+
* **Default**: Each sentiment has a default status icon that appears when no media is provided.
|
|
218
|
+
*
|
|
219
|
+
* **Icon Overrides**: Replace default icons with custom images, avatars with images, avatars with initials,
|
|
220
|
+
* avatars with custom icons, or avatars with profile types and badges.
|
|
221
|
+
*/
|
|
222
|
+
export const MediaTypes: Story = {
|
|
104
223
|
render: () => {
|
|
105
224
|
return (
|
|
106
225
|
<>
|
|
226
|
+
<Title type="title-body">Default</Title>
|
|
227
|
+
<ActionPrompt
|
|
228
|
+
sentiment="success"
|
|
229
|
+
title="Default status icon"
|
|
230
|
+
description="When no media is provided, the sentiment's default status icon is displayed."
|
|
231
|
+
action={{ label: 'Continue', onClick: fn() }}
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
<Title type="title-body">Icon Overrides</Title>
|
|
107
235
|
<ActionPrompt
|
|
108
236
|
sentiment="neutral"
|
|
109
237
|
title="Custom image"
|
|
110
238
|
description={lorem10}
|
|
111
239
|
media={{ imgSrc: '../../wise-card.svg' }}
|
|
112
|
-
action={{ label: '
|
|
240
|
+
action={{ label: 'View card', onClick: fn() }}
|
|
113
241
|
/>
|
|
114
242
|
<ActionPrompt
|
|
115
243
|
sentiment="neutral"
|
|
116
|
-
title="
|
|
244
|
+
title="Avatar with image"
|
|
117
245
|
description={lorem10}
|
|
118
246
|
media={{ avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } }}
|
|
119
|
-
action={{ label: '
|
|
247
|
+
action={{ label: 'View profile', onClick: fn() }}
|
|
120
248
|
/>
|
|
121
249
|
<ActionPrompt
|
|
122
|
-
sentiment="
|
|
123
|
-
title="
|
|
250
|
+
sentiment="success"
|
|
251
|
+
title="Avatar with initials"
|
|
124
252
|
description={lorem10}
|
|
125
|
-
media={{ avatar: {
|
|
126
|
-
action={{ label: '
|
|
253
|
+
media={{ avatar: { profileName: 'John Doe' } }}
|
|
254
|
+
action={{ label: 'Send invite', onClick: fn() }}
|
|
127
255
|
/>
|
|
128
256
|
<ActionPrompt
|
|
129
|
-
sentiment="
|
|
130
|
-
title="
|
|
257
|
+
sentiment="warning"
|
|
258
|
+
title="Avatar with custom icon"
|
|
131
259
|
description={lorem10}
|
|
132
|
-
media={{ avatar: {
|
|
133
|
-
action={{ label: '
|
|
260
|
+
media={{ avatar: { asset: <Bank /> } }}
|
|
261
|
+
action={{ label: 'Connect bank', onClick: fn() }}
|
|
134
262
|
/>
|
|
135
263
|
<ActionPrompt
|
|
136
|
-
sentiment="
|
|
137
|
-
title="
|
|
264
|
+
sentiment="proposition"
|
|
265
|
+
title="Business profile with badge"
|
|
138
266
|
description={lorem10}
|
|
139
|
-
media={{ avatar: {
|
|
140
|
-
action={{ label: '
|
|
267
|
+
media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
|
|
268
|
+
action={{ label: 'Open account', onClick: fn() }}
|
|
141
269
|
/>
|
|
142
270
|
<ActionPrompt
|
|
143
|
-
sentiment="
|
|
144
|
-
title="
|
|
271
|
+
sentiment="negative"
|
|
272
|
+
title="Personal profile with badge"
|
|
145
273
|
description={lorem10}
|
|
146
|
-
media={{ avatar: {
|
|
147
|
-
action={{ label: '
|
|
148
|
-
actionSecondary={{ label: 'Maybe later', onClick: fn() }}
|
|
274
|
+
media={{ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } } }}
|
|
275
|
+
action={{ label: 'Verify', onClick: fn() }}
|
|
149
276
|
/>
|
|
150
277
|
</>
|
|
151
278
|
);
|
|
152
279
|
},
|
|
153
|
-
decorators: [withComponentGrid()],
|
|
154
280
|
};
|
|
155
281
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
282
|
+
/**
|
|
283
|
+
* When `onDismiss` is provided, a close button appears allowing users to dismiss the prompt.
|
|
284
|
+
* Note: The component itself is not automatically removed - you must handle state management.
|
|
285
|
+
*/
|
|
286
|
+
export const Dismissible: Story = {
|
|
287
|
+
render: (args) => {
|
|
288
|
+
const [showPrompt, setShowPrompt] = useState(true);
|
|
160
289
|
|
|
161
290
|
return (
|
|
162
291
|
<>
|
|
163
|
-
{
|
|
292
|
+
{showPrompt ? (
|
|
164
293
|
<ActionPrompt
|
|
165
294
|
sentiment="proposition"
|
|
166
|
-
title="
|
|
167
|
-
description="
|
|
168
|
-
action={{ label: '
|
|
169
|
-
|
|
170
|
-
onDismiss={() => setShowNeutralPrompt(false)}
|
|
171
|
-
/>
|
|
172
|
-
)}
|
|
173
|
-
{showSuccessPrompt && (
|
|
174
|
-
<ActionPrompt
|
|
175
|
-
sentiment="success"
|
|
176
|
-
title="Payment successful"
|
|
177
|
-
description="Your money is on its way"
|
|
178
|
-
action={{ label: 'View details', onClick: fn() }}
|
|
179
|
-
onDismiss={() => setShowSuccessPrompt(false)}
|
|
295
|
+
title="Special offer"
|
|
296
|
+
description="Get 50% off your next transfer!"
|
|
297
|
+
action={{ label: 'Claim now', onClick: fn() }}
|
|
298
|
+
onDismiss={() => setShowPrompt(false)}
|
|
180
299
|
/>
|
|
300
|
+
) : (
|
|
301
|
+
<Button v2 onClick={() => setShowPrompt(true)}>
|
|
302
|
+
Show prompt again
|
|
303
|
+
</Button>
|
|
181
304
|
)}
|
|
182
305
|
</>
|
|
183
306
|
);
|
|
@@ -185,12 +308,39 @@ export const OnDismiss: Story = {
|
|
|
185
308
|
};
|
|
186
309
|
|
|
187
310
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* [Visit wise.design](https://wise.design/components/info-prompt#writing-an-info-prompt) for guidance on writing effective prompt messages that are concise and easy to understand.
|
|
311
|
+
* ActionPrompt adapts its layout based on available width. At narrow widths (mobile),
|
|
312
|
+
* the buttons stack vertically. At wider widths (desktop), they appear side by side.
|
|
192
313
|
*/
|
|
193
|
-
export const
|
|
314
|
+
export const Responsiveness: Story = {
|
|
315
|
+
render: (args) => (
|
|
316
|
+
<div
|
|
317
|
+
style={{
|
|
318
|
+
display: 'grid',
|
|
319
|
+
gridTemplateColumns: '320px 480px',
|
|
320
|
+
gap: '1rem',
|
|
321
|
+
alignItems: 'start',
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
<ActionPrompt
|
|
325
|
+
sentiment="proposition"
|
|
326
|
+
title="Stacked (320px)"
|
|
327
|
+
description={lorem10}
|
|
328
|
+
media={{ avatar: { asset: <Travel /> } }}
|
|
329
|
+
action={{ label: 'Primary action', onClick: fn() }}
|
|
330
|
+
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
331
|
+
onDismiss={fn()}
|
|
332
|
+
/>
|
|
333
|
+
<ActionPrompt
|
|
334
|
+
sentiment="proposition"
|
|
335
|
+
title="Side-by-side (480px)"
|
|
336
|
+
description={lorem10}
|
|
337
|
+
media={{ avatar: { asset: <Travel /> } }}
|
|
338
|
+
action={{ label: 'Primary action', onClick: fn() }}
|
|
339
|
+
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
340
|
+
onDismiss={fn()}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
),
|
|
194
344
|
parameters: {
|
|
195
345
|
docs: {
|
|
196
346
|
canvas: {
|
|
@@ -198,11 +348,4 @@ export const ParagraphWidth: Story = {
|
|
|
198
348
|
},
|
|
199
349
|
},
|
|
200
350
|
},
|
|
201
|
-
args: {
|
|
202
|
-
title: lorem10,
|
|
203
|
-
description: lorem20,
|
|
204
|
-
sentiment: 'success',
|
|
205
|
-
action: { label: 'View details', onClick: fn() },
|
|
206
|
-
onDismiss: () => {},
|
|
207
|
-
},
|
|
208
351
|
};
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
1
2
|
import { Freeze, People } from '@transferwise/icons';
|
|
2
3
|
import { action } from 'storybook/actions';
|
|
4
|
+
import { fn } from 'storybook/test';
|
|
3
5
|
import ActionPrompt from './ActionPrompt';
|
|
4
6
|
import { Body, Title } from '../..';
|
|
5
|
-
import {
|
|
7
|
+
import { StoryObj } from '@storybook/react-webpack5';
|
|
8
|
+
import { allModes } from '../../../.storybook/modes';
|
|
9
|
+
import { withVariantConfig } from '../../../.storybook/helpers';
|
|
10
|
+
|
|
11
|
+
const withComponentGrid =
|
|
12
|
+
({ gap = '1rem' } = {}) =>
|
|
13
|
+
(Story: () => ReactElement) => (
|
|
14
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap }}>
|
|
15
|
+
<Story />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
6
18
|
|
|
7
19
|
export default {
|
|
8
20
|
title: 'Prompts/ActionPrompt/Tests',
|
|
9
21
|
component: ActionPrompt,
|
|
10
|
-
tags: ['!manifest', '!autodocs'],
|
|
22
|
+
tags: ['!manifest', '!autodocs', 'new'],
|
|
11
23
|
};
|
|
12
24
|
|
|
13
25
|
type Story = StoryObj<typeof ActionPrompt>;
|
|
@@ -130,7 +142,7 @@ export const VariousA11yFeatures: Story = {
|
|
|
130
142
|
sentiment="negative"
|
|
131
143
|
media={{ avatar: { asset: <People /> } }}
|
|
132
144
|
title="Sync contacts for a faster experience"
|
|
133
|
-
description="Find contacts on Wise — it
|
|
145
|
+
description="Find contacts on Wise — it's simple, secure and you pick who you add."
|
|
134
146
|
action={{
|
|
135
147
|
label: 'Sync contacts',
|
|
136
148
|
onClick: () => {
|
|
@@ -145,3 +157,74 @@ export const VariousA11yFeatures: Story = {
|
|
|
145
157
|
);
|
|
146
158
|
},
|
|
147
159
|
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ActionPrompt can be shown with or without a description.
|
|
163
|
+
*/
|
|
164
|
+
export const WithoutDescription: Story = {
|
|
165
|
+
render: () => {
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<ActionPrompt
|
|
169
|
+
sentiment="warning"
|
|
170
|
+
title="Session expiring soon"
|
|
171
|
+
action={{ label: 'Stay logged in', onClick: fn() }}
|
|
172
|
+
actionSecondary={{ label: 'Log out', onClick: fn() }}
|
|
173
|
+
/>
|
|
174
|
+
<ActionPrompt
|
|
175
|
+
sentiment="success"
|
|
176
|
+
title="Payment successful"
|
|
177
|
+
action={{ label: 'View details', onClick: fn() }}
|
|
178
|
+
/>
|
|
179
|
+
<ActionPrompt title="Quick action" action={{ label: 'Continue', onClick: fn() }} />
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
decorators: [withComponentGrid()],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const AllThemesAndSentiments: Story = {
|
|
187
|
+
render: () => {
|
|
188
|
+
return (
|
|
189
|
+
<>
|
|
190
|
+
{(['negative', 'warning', 'neutral', 'success', 'proposition'] as const).map(
|
|
191
|
+
(sentiment) => (
|
|
192
|
+
<ActionPrompt
|
|
193
|
+
key={sentiment}
|
|
194
|
+
sentiment={sentiment}
|
|
195
|
+
title={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} action`}
|
|
196
|
+
description="This demonstrates the prompt appearance in different themes."
|
|
197
|
+
action={{ label: 'Primary', onClick: fn() }}
|
|
198
|
+
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
199
|
+
onDismiss={fn()}
|
|
200
|
+
/>
|
|
201
|
+
),
|
|
202
|
+
)}
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
decorators: [withComponentGrid({ gap: '1.5rem' })],
|
|
207
|
+
parameters: {
|
|
208
|
+
padding: '16px',
|
|
209
|
+
variants: ['default', 'dark', 'bright-green', 'forest-green'],
|
|
210
|
+
chromatic: {
|
|
211
|
+
dark: allModes.dark,
|
|
212
|
+
brightGreen: allModes.brightGreen,
|
|
213
|
+
forestGreen: allModes.forestGreen,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const TinyScreen: Story = {
|
|
219
|
+
render: () => (
|
|
220
|
+
<ActionPrompt
|
|
221
|
+
sentiment="warning"
|
|
222
|
+
title="Complete your profile"
|
|
223
|
+
description="Add your business details to unlock all features and start sending money internationally."
|
|
224
|
+
action={{ label: 'Complete profile', onClick: fn() }}
|
|
225
|
+
actionSecondary={{ label: 'Remind me later', onClick: fn() }}
|
|
226
|
+
onDismiss={fn()}
|
|
227
|
+
/>
|
|
228
|
+
),
|
|
229
|
+
...withVariantConfig(['400%']),
|
|
230
|
+
};
|
|
@@ -34,6 +34,13 @@ export type ActionPromptProps = {
|
|
|
34
34
|
'aria-label'?: AriaAttributes['aria-label'];
|
|
35
35
|
} & Pick<PrimitivePromptProps, 'id' | 'className' | 'data-testid' | 'sentiment' | 'onDismiss'>;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Use an action prompt for optional feedback that doesn't require immediate action, such as feature upsells, warnings, or suggestions. These prompts are typically used outside of core product flows (e.g., Launchpad, Recipient, or Transaction screens) and can be addressed at the user's convenience.
|
|
39
|
+
*
|
|
40
|
+
* If your message is about immediate user feedback (e.g., form submission errors, download failures, missing data warnings), use an [info prompt](https://storybook.wise.design/?path=/docs/prompts-infoprompt--docs) instead.
|
|
41
|
+
*
|
|
42
|
+
* Guidance can be found in the [design system](https://wise.design/components/action-prompt).
|
|
43
|
+
*/
|
|
37
44
|
export const ActionPrompt = ({
|
|
38
45
|
sentiment = 'neutral',
|
|
39
46
|
title,
|
|
@@ -158,14 +165,10 @@ export const ActionPrompt = ({
|
|
|
158
165
|
})}
|
|
159
166
|
>
|
|
160
167
|
<div className={clsx('d-flex', 'flex-column', 'justify-content-center')}>
|
|
161
|
-
<Body id={titleId} type={Typography.BODY_LARGE_BOLD}
|
|
168
|
+
<Body id={titleId} type={Typography.BODY_LARGE_BOLD}>
|
|
162
169
|
{title}
|
|
163
170
|
</Body>
|
|
164
|
-
{description &&
|
|
165
|
-
<Body id={descId} className="wds-action-prompt__content">
|
|
166
|
-
{description}
|
|
167
|
-
</Body>
|
|
168
|
-
)}
|
|
171
|
+
{description && <Body id={descId}>{description}</Body>}
|
|
169
172
|
</div>
|
|
170
173
|
</PrimitivePrompt>
|
|
171
174
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Meta, Source } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Prompts/InfoPrompt/Accessibility" />
|
|
4
|
+
|
|
5
|
+
# Accessibility
|
|
6
|
+
|
|
7
|
+
Under the hood, `InfoPrompt` is marked as `role="region"` for a section of content that users might want to navigate to directly.
|
|
8
|
+
|
|
9
|
+
By default, it's labelled by the `title` and described by the `description`.
|
|
10
|
+
|
|
11
|
+
## Announcement Behaviour
|
|
12
|
+
|
|
13
|
+
`InfoPrompt` is designed for contextual messages that appear within a screen. Unlike `ActionPrompt`, it can be configured to announce to screen readers using ARIA roles:
|
|
14
|
+
|
|
15
|
+
- **`role="status"`**: Politely announces updates when the screen reader is idle. Use for non-critical success messages or informational updates (e.g., "Your profile has been saved").
|
|
16
|
+
- **`role="alert"`**: Immediately interrupts to announce urgent messages. Use for time-sensitive information that requires immediate attention (e.g., errors, warnings, deadlines).
|
|
17
|
+
|
|
18
|
+
**Default behavior:** Without a role, the prompt won't be announced automatically. Users will encounter it when navigating the page.
|
|
19
|
+
|
|
20
|
+
<Source
|
|
21
|
+
dark
|
|
22
|
+
code={`
|
|
23
|
+
// Polite announcement for non-critical updates
|
|
24
|
+
<InfoPrompt
|
|
25
|
+
sentiment="success"
|
|
26
|
+
description="Your profile has been saved"
|
|
27
|
+
role="status"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
// Immediate announcement for urgent messages
|
|
31
|
+
|
|
32
|
+
<InfoPrompt
|
|
33
|
+
sentiment="negative"
|
|
34
|
+
title="Session expired"
|
|
35
|
+
description="Your session has expired. Please log in again."
|
|
36
|
+
role="alert"
|
|
37
|
+
/>
|
|
38
|
+
`}
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
## Custom Labels
|
|
42
|
+
|
|
43
|
+
If you want to provide a custom label for screen readers, you can use the `aria-label` prop. Make sure to include all necessary information in it, because when provided, the default labelling will be removed.
|
|
44
|
+
|
|
45
|
+
<Source
|
|
46
|
+
dark
|
|
47
|
+
code={`
|
|
48
|
+
<InfoPrompt
|
|
49
|
+
sentiment="negative"
|
|
50
|
+
title="Payment failed"
|
|
51
|
+
description="Unable to process payment."
|
|
52
|
+
aria-label="Critical error: Your payment could not be processed. Please update your payment method and try again within 24 hours."
|
|
53
|
+
/>
|
|
54
|
+
`}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
## Media
|
|
58
|
+
|
|
59
|
+
Custom media icons should include their own accessibility attributes. Use the `title` prop on icons to provide accessible names for screen readers:
|
|
60
|
+
|
|
61
|
+
<Source
|
|
62
|
+
dark
|
|
63
|
+
code={`
|
|
64
|
+
<InfoPrompt
|
|
65
|
+
sentiment="success"
|
|
66
|
+
description="Your travel account is ready!"
|
|
67
|
+
media={{ asset: <Travel title="Travel feature" /> }}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<InfoPrompt
|
|
71
|
+
sentiment="warning"
|
|
72
|
+
title="Business travel"
|
|
73
|
+
description="Book your business trip with Wise."
|
|
74
|
+
media={{ asset: <Briefcase title="Business" /> }}
|
|
75
|
+
/>
|
|
76
|
+
`}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
**Note:** Unlike `ActionPrompt` (which uses `media['aria-label']`) and `InlinePrompt` (which uses `mediaLabel`), `InfoPrompt` requires you to add accessibility attributes directly to the icon component you pass.
|