@transferwise/components 0.0.0-experimental-276a15d → 0.0.0-experimental-dd4f4a5
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 +1 -3
- package/build/prompt/ActionPrompt/ActionPrompt.js +4 -4
- package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
- package/build/prompt/ActionPrompt/ActionPrompt.mjs +4 -4
- 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/styles/main.css +1 -3
- package/build/styles/prompt/InfoPrompt/InfoPrompt.css +1 -3
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +0 -7
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +2 -4
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/iconButton/IconButton.story.tsx +48 -173
- package/src/listItem/_stories/ListItem.story.tsx +1 -0
- package/src/main.css +1 -3
- package/src/prompt/ActionPrompt/ActionPrompt.accessibility.docs.mdx +17 -1
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +107 -299
- package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +3 -86
- package/src/prompt/ActionPrompt/ActionPrompt.tsx +4 -11
- package/src/prompt/InfoPrompt/InfoPrompt.css +1 -3
- package/src/prompt/InfoPrompt/InfoPrompt.less +4 -4
- package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +86 -93
- package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +1 -54
- package/src/prompt/InfoPrompt/InfoPrompt.tsx +2 -4
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +29 -24
- package/src/sentimentSurface/SentimentSurface.docs.mdx +1 -1
- package/src/sentimentSurface/SentimentSurface.story.tsx +1 -1
- package/src/sentimentSurface/SentimentSurface.test.story.tsx +1 -1
- package/src/iconButton/IconButton.test.story.tsx +0 -194
- package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +0 -79
- package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +0 -63
- package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +0 -21
package/src/main.css
CHANGED
|
@@ -5443,7 +5443,7 @@ html:not([dir="rtl"]) .np-navigation-option {
|
|
|
5443
5443
|
}
|
|
5444
5444
|
.wds-info-prompt {
|
|
5445
5445
|
--Prompt-gap: var(--size-8);
|
|
5446
|
-
--Prompt-padding:
|
|
5446
|
+
--Prompt-padding: 12px;
|
|
5447
5447
|
}
|
|
5448
5448
|
.wds-info-prompt__content {
|
|
5449
5449
|
display: flex;
|
|
@@ -5467,9 +5467,7 @@ html:not([dir="rtl"]) .np-navigation-option {
|
|
|
5467
5467
|
}
|
|
5468
5468
|
.wds-info-prompt__media svg {
|
|
5469
5469
|
width: 24px;
|
|
5470
|
-
width: var(--size-24);
|
|
5471
5470
|
height: 24px;
|
|
5472
|
-
height: var(--size-24);
|
|
5473
5471
|
}
|
|
5474
5472
|
.wds-info-prompt .wds-prompt__media-wrapper {
|
|
5475
5473
|
padding: 0;
|
|
@@ -31,7 +31,23 @@ If you want to provide a custom label for screen readers, you can use the `aria-
|
|
|
31
31
|
|
|
32
32
|
## Media
|
|
33
33
|
|
|
34
|
-
You can use `aria-
|
|
34
|
+
You can use the `aria-hidden` attribute on media assets to hide them from screen readers if they're not providing any additional information. This is useful when the media is purely decorative or when its content is already conveyed through other means (e.g., text).
|
|
35
|
+
|
|
36
|
+
<Source
|
|
37
|
+
dark
|
|
38
|
+
code={`
|
|
39
|
+
<ActionPrompt
|
|
40
|
+
media={{
|
|
41
|
+
'aria-hidden': true,
|
|
42
|
+
avatar: { asset: <People /> },
|
|
43
|
+
}}
|
|
44
|
+
title="Sync contacts for a faster experience"
|
|
45
|
+
...
|
|
46
|
+
/>
|
|
47
|
+
`}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
You can also use `aria-label` on media assets to provide a custom label for screen readers.
|
|
35
51
|
|
|
36
52
|
<Source
|
|
37
53
|
dark
|
|
@@ -1,11 +1,22 @@
|
|
|
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,
|
|
5
|
-
import { ActionPrompt
|
|
4
|
+
import { Bank, Freeze, People } from '@transferwise/icons';
|
|
5
|
+
import { ActionPrompt } from './ActionPrompt';
|
|
6
6
|
import { lorem10 } from '../../test-utils';
|
|
7
|
-
import
|
|
7
|
+
import Body from '../../body';
|
|
8
|
+
import { action } from 'storybook/actions';
|
|
8
9
|
import Title from '../../title';
|
|
10
|
+
import { withVariantConfig } from '../../../.storybook/helpers';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof ActionPrompt> = {
|
|
13
|
+
title: 'Prompts/ActionPrompt',
|
|
14
|
+
component: ActionPrompt,
|
|
15
|
+
tags: ['new'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof ActionPrompt>;
|
|
9
20
|
|
|
10
21
|
const withComponentGrid =
|
|
11
22
|
({ maxWidth = 'auto', gap = '1rem' } = {}) =>
|
|
@@ -26,354 +37,151 @@ const withComponentGrid =
|
|
|
26
37
|
</div>
|
|
27
38
|
);
|
|
28
39
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
description: {
|
|
51
|
-
control: 'text',
|
|
52
|
-
table: {
|
|
53
|
-
type: { summary: 'ReactNode' },
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
media: {
|
|
57
|
-
control: false,
|
|
58
|
-
table: {
|
|
59
|
-
type: {
|
|
60
|
-
summary: 'ActionPromptMedia',
|
|
61
|
-
detail:
|
|
62
|
-
'{ imgSrc?: string; avatar?: { imgSrc?: string; profileName?: string; profileType?: string; asset?: ReactNode; badge?: { flagCode: string } }; aria-label?: string }',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
action: {
|
|
67
|
-
control: false,
|
|
68
|
-
table: {
|
|
69
|
-
type: {
|
|
70
|
-
summary: 'ActionPromptAction',
|
|
71
|
-
detail: '{ label: ReactNode; onClick?: () => void; href?: string; target?: string }',
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
actionSecondary: {
|
|
76
|
-
control: false,
|
|
77
|
-
table: {
|
|
78
|
-
type: {
|
|
79
|
-
summary: 'ActionPromptActionSecondary',
|
|
80
|
-
detail: '{ label: ReactNode; onClick?: () => void; href?: string; target?: string }',
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
parameters: {
|
|
86
|
-
docs: {
|
|
87
|
-
toc: true,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export default meta;
|
|
93
|
-
type Story = StoryObj<typeof ActionPrompt>;
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Convenience controls for previewing rich markup,
|
|
97
|
-
* not otherwise possible via Storybook
|
|
98
|
-
*/
|
|
99
|
-
type PreviewStoryArgs = ActionPromptProps & {
|
|
100
|
-
previewMedia: ActionPromptProps['media'];
|
|
101
|
-
previewOnDismiss: boolean;
|
|
102
|
-
previewSecondaryAction: boolean;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const previewArgGroup = {
|
|
106
|
-
category: 'Storybook Preview options',
|
|
107
|
-
type: {
|
|
108
|
-
summary: undefined,
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const MEDIA_OPTIONS: Record<string, ActionPromptProps['media']> = {
|
|
113
|
-
undefined,
|
|
114
|
-
'Custom image (no badge)': { imgSrc: '../../wise-card.svg' },
|
|
115
|
-
'Avatar: Profile image': { avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } },
|
|
116
|
-
'Avatar: Business (GB badge)': {
|
|
117
|
-
avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } },
|
|
118
|
-
},
|
|
119
|
-
'Avatar: Personal (EU badge)': {
|
|
120
|
-
avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } },
|
|
40
|
+
export const AllSentiments: Story = {
|
|
41
|
+
render: () => {
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{(['negative', 'warning', 'neutral', 'success', 'proposition'] as const).map(
|
|
45
|
+
(sentiment) => (
|
|
46
|
+
<ActionPrompt
|
|
47
|
+
key={sentiment}
|
|
48
|
+
sentiment={sentiment}
|
|
49
|
+
title="Title"
|
|
50
|
+
description={lorem10}
|
|
51
|
+
action={{ label: 'Primary', onClick: fn() }}
|
|
52
|
+
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
53
|
+
className="m-b-2"
|
|
54
|
+
onDismiss={fn()}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
)}
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
121
60
|
},
|
|
122
|
-
|
|
123
|
-
'Avatar: Icon (Star)': { avatar: { asset: <Star /> } },
|
|
124
|
-
'Avatar: Icon (Travel)': { avatar: { asset: <Travel /> } },
|
|
125
|
-
'Avatar: Icon (Briefcase)': { avatar: { asset: <Briefcase /> } },
|
|
126
|
-
'Avatar: Initials': { avatar: { profileName: 'John Doe' } },
|
|
61
|
+
decorators: [withComponentGrid()],
|
|
127
62
|
};
|
|
128
63
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
control: 'select',
|
|
133
|
-
options: [
|
|
134
|
-
'undefined',
|
|
135
|
-
'Custom image (no badge)',
|
|
136
|
-
'Avatar: Profile image',
|
|
137
|
-
'Avatar: Business (GB badge)',
|
|
138
|
-
'Avatar: Personal (EU badge)',
|
|
139
|
-
'Avatar: Icon (Bank)',
|
|
140
|
-
'Avatar: Icon (Star)',
|
|
141
|
-
'Avatar: Icon (Travel)',
|
|
142
|
-
'Avatar: Icon (Briefcase)',
|
|
143
|
-
'Avatar: Initials',
|
|
144
|
-
],
|
|
145
|
-
mapping: MEDIA_OPTIONS,
|
|
146
|
-
table: previewArgGroup,
|
|
147
|
-
},
|
|
148
|
-
previewOnDismiss: {
|
|
149
|
-
name: 'Preview with `onDismiss`',
|
|
150
|
-
control: 'boolean',
|
|
151
|
-
table: previewArgGroup,
|
|
152
|
-
},
|
|
153
|
-
previewSecondaryAction: {
|
|
154
|
-
name: 'Preview with `actionSecondary`',
|
|
155
|
-
control: 'boolean',
|
|
156
|
-
table: previewArgGroup,
|
|
157
|
-
},
|
|
158
|
-
} as const;
|
|
159
|
-
|
|
160
|
-
const getPropsForPreview = (
|
|
161
|
-
args: PreviewStoryArgs,
|
|
162
|
-
): [ActionPromptProps, Partial<ActionPromptProps>] => {
|
|
163
|
-
const { previewMedia, previewOnDismiss, previewSecondaryAction, ...props } = args;
|
|
164
|
-
|
|
165
|
-
return [
|
|
166
|
-
props,
|
|
167
|
-
{
|
|
168
|
-
media: previewMedia,
|
|
169
|
-
onDismiss: previewOnDismiss ? fn() : undefined,
|
|
170
|
-
actionSecondary: previewSecondaryAction ? { label: 'Cancel', onClick: fn() } : undefined,
|
|
171
|
-
},
|
|
172
|
-
];
|
|
64
|
+
export const Mobile: Story = {
|
|
65
|
+
...AllSentiments,
|
|
66
|
+
...withVariantConfig(['mobile'], AllSentiments),
|
|
173
67
|
};
|
|
174
68
|
|
|
175
|
-
export const
|
|
176
|
-
argTypes: {
|
|
177
|
-
sentiment: {
|
|
178
|
-
control: 'radio',
|
|
179
|
-
options: ['success', 'negative', 'neutral', 'warning', 'proposition'],
|
|
180
|
-
},
|
|
181
|
-
...previewArgTypes,
|
|
182
|
-
},
|
|
69
|
+
export const PrimaryActionOnly: Story = {
|
|
183
70
|
args: {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const [props, previewProps] = getPropsForPreview(args);
|
|
190
|
-
return <ActionPrompt {...props} {...previewProps} />;
|
|
71
|
+
sentiment: 'proposition',
|
|
72
|
+
title: 'Payment successful',
|
|
73
|
+
description: 'Your money is on its way',
|
|
74
|
+
action: { label: 'View details', onClick: fn() },
|
|
75
|
+
onDismiss: fn(),
|
|
191
76
|
},
|
|
192
77
|
};
|
|
193
78
|
|
|
194
|
-
|
|
195
|
-
* ActionPrompt supports multiple sentiments to communicate different types of messages:
|
|
196
|
-
* - `neutral` (default): General prompts
|
|
197
|
-
* - `success`: Success confirmations
|
|
198
|
-
* - `negative`: Error or critical actions
|
|
199
|
-
* - `warning`: Warning messages
|
|
200
|
-
* - `proposition`: Promotional or feature prompts
|
|
201
|
-
*/
|
|
202
|
-
export const Sentiments: Story = {
|
|
203
|
-
render: (args) => (
|
|
204
|
-
<>
|
|
205
|
-
{(['success', 'warning', 'negative', 'neutral', 'proposition'] as const).map((sentiment) => (
|
|
206
|
-
<ActionPrompt
|
|
207
|
-
key={sentiment}
|
|
208
|
-
sentiment={sentiment}
|
|
209
|
-
title={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} Action`}
|
|
210
|
-
description={lorem10}
|
|
211
|
-
action={{ label: 'Primary', onClick: fn() }}
|
|
212
|
-
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
213
|
-
/>
|
|
214
|
-
))}
|
|
215
|
-
</>
|
|
216
|
-
),
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* ActionPrompt can have just a primary action, or both primary and secondary actions.
|
|
221
|
-
*/
|
|
222
|
-
export const Actions: Story = {
|
|
223
|
-
render: () => (
|
|
224
|
-
<>
|
|
225
|
-
<ActionPrompt
|
|
226
|
-
sentiment="proposition"
|
|
227
|
-
title="Primary action only"
|
|
228
|
-
description="This prompt has only one action button."
|
|
229
|
-
action={{ label: 'Get started', onClick: fn() }}
|
|
230
|
-
/>
|
|
231
|
-
<ActionPrompt
|
|
232
|
-
sentiment="warning"
|
|
233
|
-
title="Primary and secondary actions"
|
|
234
|
-
description="This prompt has both action buttons."
|
|
235
|
-
action={{ label: 'Confirm', onClick: fn() }}
|
|
236
|
-
actionSecondary={{ label: 'Cancel', onClick: fn() }}
|
|
237
|
-
/>
|
|
238
|
-
</>
|
|
239
|
-
),
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* ActionPrompt supports various media types to enhance visual communication.
|
|
244
|
-
*
|
|
245
|
-
* **Default**: Each sentiment has a default status icon that appears when no media is provided.
|
|
246
|
-
*
|
|
247
|
-
* **Icon Overrides**: Replace default icons with custom images, avatars with images, avatars with initials,
|
|
248
|
-
* avatars with custom icons, or avatars with profile types and badges.
|
|
249
|
-
*/
|
|
250
|
-
export const MediaTypes: Story = {
|
|
79
|
+
export const WithoutDescription: Story = {
|
|
251
80
|
render: () => {
|
|
252
81
|
return (
|
|
253
82
|
<>
|
|
254
|
-
<
|
|
83
|
+
<ActionPrompt
|
|
84
|
+
sentiment="warning"
|
|
85
|
+
title="Session expiring soon"
|
|
86
|
+
action={{ label: 'Stay logged in', onClick: fn() }}
|
|
87
|
+
actionSecondary={{ label: 'Log out', onClick: fn() }}
|
|
88
|
+
onDismiss={fn()}
|
|
89
|
+
/>
|
|
255
90
|
<ActionPrompt
|
|
256
91
|
sentiment="success"
|
|
257
|
-
title="
|
|
258
|
-
|
|
259
|
-
|
|
92
|
+
title="Session expiring soon"
|
|
93
|
+
action={{ label: 'Stay logged in', onClick: fn() }}
|
|
94
|
+
/>
|
|
95
|
+
<ActionPrompt
|
|
96
|
+
title="Session expiring soon"
|
|
97
|
+
action={{ label: 'Stay logged in', onClick: fn() }}
|
|
260
98
|
/>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
decorators: [withComponentGrid()],
|
|
103
|
+
};
|
|
261
104
|
|
|
262
|
-
|
|
105
|
+
export const Media: Story = {
|
|
106
|
+
render: () => {
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
263
109
|
<ActionPrompt
|
|
264
110
|
sentiment="neutral"
|
|
265
111
|
title="Custom image"
|
|
266
112
|
description={lorem10}
|
|
267
113
|
media={{ imgSrc: '../../wise-card.svg' }}
|
|
268
|
-
action={{ label: '
|
|
114
|
+
action={{ label: 'Invite now', onClick: fn() }}
|
|
269
115
|
/>
|
|
270
116
|
<ActionPrompt
|
|
271
117
|
sentiment="neutral"
|
|
272
|
-
title="
|
|
118
|
+
title="Profile Image"
|
|
273
119
|
description={lorem10}
|
|
274
120
|
media={{ avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } }}
|
|
275
|
-
action={{ label: '
|
|
121
|
+
action={{ label: 'Invite now', onClick: fn() }}
|
|
276
122
|
/>
|
|
277
123
|
<ActionPrompt
|
|
278
|
-
sentiment="
|
|
279
|
-
title="
|
|
124
|
+
sentiment="proposition"
|
|
125
|
+
title="Business Profile"
|
|
280
126
|
description={lorem10}
|
|
281
|
-
media={{ avatar: {
|
|
282
|
-
action={{ label: '
|
|
127
|
+
media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
|
|
128
|
+
action={{ label: 'Invite now', onClick: fn() }}
|
|
283
129
|
/>
|
|
284
130
|
<ActionPrompt
|
|
285
|
-
sentiment="
|
|
286
|
-
title="
|
|
131
|
+
sentiment="negative"
|
|
132
|
+
title="Personal Profile"
|
|
287
133
|
description={lorem10}
|
|
288
|
-
media={{ avatar: {
|
|
289
|
-
action={{ label: '
|
|
134
|
+
media={{ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } } }}
|
|
135
|
+
action={{ label: 'Invite now', onClick: fn() }}
|
|
290
136
|
/>
|
|
291
137
|
<ActionPrompt
|
|
292
|
-
sentiment="
|
|
293
|
-
title="
|
|
138
|
+
sentiment="warning"
|
|
139
|
+
title="Custom Icon"
|
|
294
140
|
description={lorem10}
|
|
295
|
-
media={{ avatar: {
|
|
296
|
-
action={{ label: '
|
|
141
|
+
media={{ avatar: { asset: <Bank /> } }}
|
|
142
|
+
action={{ label: 'Invite now', onClick: fn() }}
|
|
297
143
|
/>
|
|
298
144
|
<ActionPrompt
|
|
299
|
-
sentiment="
|
|
300
|
-
title="
|
|
145
|
+
sentiment="success"
|
|
146
|
+
title="Initials"
|
|
301
147
|
description={lorem10}
|
|
302
|
-
media={{ avatar: {
|
|
303
|
-
action={{ label: '
|
|
148
|
+
media={{ avatar: { profileName: 'John Doe' } }}
|
|
149
|
+
action={{ label: 'Invite now', onClick: fn() }}
|
|
150
|
+
actionSecondary={{ label: 'Maybe later', onClick: fn() }}
|
|
304
151
|
/>
|
|
305
152
|
</>
|
|
306
153
|
);
|
|
307
154
|
},
|
|
155
|
+
decorators: [withComponentGrid()],
|
|
308
156
|
};
|
|
309
157
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
export const Dismissible: Story = {
|
|
315
|
-
render: (args) => {
|
|
316
|
-
const [showPrompt, setShowPrompt] = useState(true);
|
|
158
|
+
export const OnDismiss: Story = {
|
|
159
|
+
render: () => {
|
|
160
|
+
const [showNeutralPrompt, setShowNeutralPrompt] = useState(true);
|
|
161
|
+
const [showSuccessPrompt, setShowSuccessPrompt] = useState(true);
|
|
317
162
|
|
|
318
163
|
return (
|
|
319
164
|
<>
|
|
320
|
-
{
|
|
165
|
+
{showNeutralPrompt && (
|
|
321
166
|
<ActionPrompt
|
|
322
167
|
sentiment="proposition"
|
|
323
|
-
title="
|
|
324
|
-
description="
|
|
325
|
-
action={{ label: '
|
|
326
|
-
|
|
168
|
+
title="Payment successful"
|
|
169
|
+
description="Your money is on its way"
|
|
170
|
+
action={{ label: 'View details', onClick: fn() }}
|
|
171
|
+
className="m-b-2"
|
|
172
|
+
onDismiss={() => setShowNeutralPrompt(false)}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
{showSuccessPrompt && (
|
|
176
|
+
<ActionPrompt
|
|
177
|
+
sentiment="success"
|
|
178
|
+
title="Payment successful"
|
|
179
|
+
description="Your money is on its way"
|
|
180
|
+
action={{ label: 'View details', onClick: fn() }}
|
|
181
|
+
onDismiss={() => setShowSuccessPrompt(false)}
|
|
327
182
|
/>
|
|
328
|
-
) : (
|
|
329
|
-
<Button v2 onClick={() => setShowPrompt(true)}>
|
|
330
|
-
Show prompt again
|
|
331
|
-
</Button>
|
|
332
183
|
)}
|
|
333
184
|
</>
|
|
334
185
|
);
|
|
335
186
|
},
|
|
336
187
|
};
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* ActionPrompt adapts its layout based on available width. At narrow widths (mobile),
|
|
340
|
-
* the buttons stack vertically. At wider widths (desktop), they appear side by side.
|
|
341
|
-
*/
|
|
342
|
-
export const Responsiveness: Story = {
|
|
343
|
-
render: (args) => (
|
|
344
|
-
<div
|
|
345
|
-
style={{
|
|
346
|
-
display: 'grid',
|
|
347
|
-
gridTemplateColumns: '320px 480px',
|
|
348
|
-
gap: '1rem',
|
|
349
|
-
alignItems: 'start',
|
|
350
|
-
}}
|
|
351
|
-
>
|
|
352
|
-
<ActionPrompt
|
|
353
|
-
sentiment="proposition"
|
|
354
|
-
title="Stacked (320px)"
|
|
355
|
-
description={lorem10}
|
|
356
|
-
media={{ avatar: { asset: <Travel /> } }}
|
|
357
|
-
action={{ label: 'Primary action', onClick: fn() }}
|
|
358
|
-
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
359
|
-
onDismiss={fn()}
|
|
360
|
-
/>
|
|
361
|
-
<ActionPrompt
|
|
362
|
-
sentiment="proposition"
|
|
363
|
-
title="Side-by-side (480px)"
|
|
364
|
-
description={lorem10}
|
|
365
|
-
media={{ avatar: { asset: <Travel /> } }}
|
|
366
|
-
action={{ label: 'Primary action', onClick: fn() }}
|
|
367
|
-
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
368
|
-
onDismiss={fn()}
|
|
369
|
-
/>
|
|
370
|
-
</div>
|
|
371
|
-
),
|
|
372
|
-
parameters: {
|
|
373
|
-
docs: {
|
|
374
|
-
canvas: {
|
|
375
|
-
sourceState: 'hidden',
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
};
|
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
import { ReactElement } from 'react';
|
|
2
1
|
import { Freeze, People } from '@transferwise/icons';
|
|
3
2
|
import { action } from 'storybook/actions';
|
|
4
|
-
import { fn } from 'storybook/test';
|
|
5
3
|
import ActionPrompt from './ActionPrompt';
|
|
6
4
|
import { Body, Title } from '../..';
|
|
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
|
-
);
|
|
5
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
18
6
|
|
|
19
7
|
export default {
|
|
20
8
|
title: 'Prompts/ActionPrompt/Tests',
|
|
21
9
|
component: ActionPrompt,
|
|
22
|
-
tags: ['!manifest', '!autodocs'
|
|
10
|
+
tags: ['!manifest', '!autodocs'],
|
|
23
11
|
};
|
|
24
12
|
|
|
25
13
|
type Story = StoryObj<typeof ActionPrompt>;
|
|
@@ -142,7 +130,7 @@ export const VariousA11yFeatures: Story = {
|
|
|
142
130
|
sentiment="negative"
|
|
143
131
|
media={{ avatar: { asset: <People /> } }}
|
|
144
132
|
title="Sync contacts for a faster experience"
|
|
145
|
-
description="Find contacts on Wise — it
|
|
133
|
+
description="Find contacts on Wise — it’s simple, secure and you pick who you add."
|
|
146
134
|
action={{
|
|
147
135
|
label: 'Sync contacts',
|
|
148
136
|
onClick: () => {
|
|
@@ -157,74 +145,3 @@ export const VariousA11yFeatures: Story = {
|
|
|
157
145
|
);
|
|
158
146
|
},
|
|
159
147
|
};
|
|
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,13 +34,6 @@ 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
|
-
*/
|
|
44
37
|
export const ActionPrompt = ({
|
|
45
38
|
sentiment = 'neutral',
|
|
46
39
|
title,
|
|
@@ -62,7 +55,7 @@ export const ActionPrompt = ({
|
|
|
62
55
|
|
|
63
56
|
const ariaLabelledByIds = [
|
|
64
57
|
media['aria-hidden'] ? undefined : mediaId,
|
|
65
|
-
ariaLabel ? undefined : titleId,
|
|
58
|
+
Boolean(ariaLabel) ? undefined : titleId,
|
|
66
59
|
]
|
|
67
60
|
.filter(Boolean)
|
|
68
61
|
.join(' ');
|
|
@@ -111,7 +104,7 @@ export const ActionPrompt = ({
|
|
|
111
104
|
id={mediaId}
|
|
112
105
|
size={48}
|
|
113
106
|
sentiment={sentiment}
|
|
114
|
-
iconLabel={media['aria-hidden'] ? null : media['aria-label']}
|
|
107
|
+
iconLabel={Boolean(media['aria-hidden']) ? null : media['aria-label']}
|
|
115
108
|
/>
|
|
116
109
|
);
|
|
117
110
|
};
|
|
@@ -155,9 +148,9 @@ export const ActionPrompt = ({
|
|
|
155
148
|
</Button>
|
|
156
149
|
</>
|
|
157
150
|
}
|
|
158
|
-
role="region"
|
|
159
151
|
onDismiss={onDismiss}
|
|
160
|
-
|
|
152
|
+
role="region"
|
|
153
|
+
{...(Boolean(ariaLabel)
|
|
161
154
|
? { 'aria-label': ariaLabel }
|
|
162
155
|
: {
|
|
163
156
|
'aria-labelledby': ariaLabelledByIds,
|