@transferwise/components 0.0.0-experimental-1de8033 → 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.
Files changed (31) hide show
  1. package/build/main.css +3 -1
  2. package/build/prompt/ActionPrompt/ActionPrompt.js +4 -4
  3. package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
  4. package/build/prompt/ActionPrompt/ActionPrompt.mjs +4 -4
  5. package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -1
  6. package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
  7. package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
  8. package/build/styles/main.css +3 -1
  9. package/build/styles/prompt/InfoPrompt/InfoPrompt.css +3 -1
  10. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +7 -0
  11. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
  12. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +4 -2
  13. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
  14. package/package.json +1 -1
  15. package/src/iconButton/IconButton.story.tsx +173 -48
  16. package/src/iconButton/IconButton.test.story.tsx +194 -0
  17. package/src/listItem/_stories/ListItem.story.tsx +0 -1
  18. package/src/main.css +3 -1
  19. package/src/prompt/ActionPrompt/ActionPrompt.accessibility.docs.mdx +1 -17
  20. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +271 -107
  21. package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +86 -3
  22. package/src/prompt/ActionPrompt/ActionPrompt.tsx +11 -4
  23. package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +79 -0
  24. package/src/prompt/InfoPrompt/InfoPrompt.css +3 -1
  25. package/src/prompt/InfoPrompt/InfoPrompt.less +4 -4
  26. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +80 -86
  27. package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +54 -1
  28. package/src/prompt/InfoPrompt/InfoPrompt.tsx +4 -2
  29. package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +63 -0
  30. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +24 -29
  31. package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +21 -0
@@ -1,22 +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, Freeze, People } from '@transferwise/icons';
5
- import { ActionPrompt } from './ActionPrompt';
4
+ import { Bank, Star, Travel, Briefcase } from '@transferwise/icons';
5
+ import { ActionPrompt, type ActionPromptProps } from './ActionPrompt';
6
6
  import { lorem10 } from '../../test-utils';
7
- import Body from '../../body';
8
- import { action } from 'storybook/actions';
7
+ import Button from '../../button';
9
8
  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>;
20
9
 
21
10
  const withComponentGrid =
22
11
  ({ maxWidth = 'auto', gap = '1rem' } = {}) =>
@@ -37,151 +26,326 @@ const withComponentGrid =
37
26
  </div>
38
27
  );
39
28
 
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
- );
60
- },
29
+ const meta: Meta<typeof ActionPrompt> = {
30
+ title: 'Prompts/ActionPrompt',
31
+ component: ActionPrompt,
32
+ tags: ['new'],
61
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
+ },
62
+ };
63
+
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;
62
75
  };
63
76
 
64
- export const Mobile: Story = {
65
- ...AllSentiments,
66
- ...withVariantConfig(['mobile'], AllSentiments),
77
+ const previewArgGroup = {
78
+ category: 'Storybook Preview options',
79
+ type: {
80
+ summary: undefined,
81
+ },
82
+ };
83
+
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' } },
67
99
  };
68
100
 
69
- export const PrimaryActionOnly: Story = {
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
+ },
70
155
  args: {
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(),
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} />;
76
163
  },
77
164
  };
78
165
 
79
- export const WithoutDescription: Story = {
80
- render: () => {
81
- return (
82
- <>
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) => (
83
178
  <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
- />
90
- <ActionPrompt
91
- sentiment="success"
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() }}
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() }}
98
185
  />
99
- </>
100
- );
101
- },
102
- decorators: [withComponentGrid()],
186
+ ))}
187
+ </>
188
+ ),
103
189
  };
104
190
 
105
- export const Media: Story = {
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 = {
106
223
  render: () => {
107
224
  return (
108
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>
109
235
  <ActionPrompt
110
236
  sentiment="neutral"
111
237
  title="Custom image"
112
238
  description={lorem10}
113
239
  media={{ imgSrc: '../../wise-card.svg' }}
114
- action={{ label: 'Invite now', onClick: fn() }}
240
+ action={{ label: 'View card', onClick: fn() }}
115
241
  />
116
242
  <ActionPrompt
117
243
  sentiment="neutral"
118
- title="Profile Image"
244
+ title="Avatar with image"
119
245
  description={lorem10}
120
246
  media={{ avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } }}
121
- action={{ label: 'Invite now', onClick: fn() }}
247
+ action={{ label: 'View profile', onClick: fn() }}
122
248
  />
123
249
  <ActionPrompt
124
- sentiment="proposition"
125
- title="Business Profile"
250
+ sentiment="success"
251
+ title="Avatar with initials"
126
252
  description={lorem10}
127
- media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
128
- action={{ label: 'Invite now', onClick: fn() }}
253
+ media={{ avatar: { profileName: 'John Doe' } }}
254
+ action={{ label: 'Send invite', onClick: fn() }}
129
255
  />
130
256
  <ActionPrompt
131
- sentiment="negative"
132
- title="Personal Profile"
257
+ sentiment="warning"
258
+ title="Avatar with custom icon"
133
259
  description={lorem10}
134
- media={{ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } } }}
135
- action={{ label: 'Invite now', onClick: fn() }}
260
+ media={{ avatar: { asset: <Bank /> } }}
261
+ action={{ label: 'Connect bank', onClick: fn() }}
136
262
  />
137
263
  <ActionPrompt
138
- sentiment="warning"
139
- title="Custom Icon"
264
+ sentiment="proposition"
265
+ title="Business profile with badge"
140
266
  description={lorem10}
141
- media={{ avatar: { asset: <Bank /> } }}
142
- action={{ label: 'Invite now', onClick: fn() }}
267
+ media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
268
+ action={{ label: 'Open account', onClick: fn() }}
143
269
  />
144
270
  <ActionPrompt
145
- sentiment="success"
146
- title="Initials"
271
+ sentiment="negative"
272
+ title="Personal profile with badge"
147
273
  description={lorem10}
148
- media={{ avatar: { profileName: 'John Doe' } }}
149
- action={{ label: 'Invite now', onClick: fn() }}
150
- actionSecondary={{ label: 'Maybe later', onClick: fn() }}
274
+ media={{ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } } }}
275
+ action={{ label: 'Verify', onClick: fn() }}
151
276
  />
152
277
  </>
153
278
  );
154
279
  },
155
- decorators: [withComponentGrid()],
156
280
  };
157
281
 
158
- export const OnDismiss: Story = {
159
- render: () => {
160
- const [showNeutralPrompt, setShowNeutralPrompt] = useState(true);
161
- const [showSuccessPrompt, setShowSuccessPrompt] = useState(true);
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);
162
289
 
163
290
  return (
164
291
  <>
165
- {showNeutralPrompt && (
292
+ {showPrompt ? (
166
293
  <ActionPrompt
167
294
  sentiment="proposition"
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)}
295
+ title="Special offer"
296
+ description="Get 50% off your next transfer!"
297
+ action={{ label: 'Claim now', onClick: fn() }}
298
+ onDismiss={() => setShowPrompt(false)}
182
299
  />
300
+ ) : (
301
+ <Button v2 onClick={() => setShowPrompt(true)}>
302
+ Show prompt again
303
+ </Button>
183
304
  )}
184
305
  </>
185
306
  );
186
307
  },
187
308
  };
309
+
310
+ /**
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.
313
+ */
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
+ ),
344
+ parameters: {
345
+ docs: {
346
+ canvas: {
347
+ sourceState: 'hidden',
348
+ },
349
+ },
350
+ },
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 { Meta, StoryObj } from '@storybook/react-webpack5';
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’s simple, secure and you pick who you add."
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,
@@ -55,7 +62,7 @@ export const ActionPrompt = ({
55
62
 
56
63
  const ariaLabelledByIds = [
57
64
  media['aria-hidden'] ? undefined : mediaId,
58
- Boolean(ariaLabel) ? undefined : titleId,
65
+ ariaLabel ? undefined : titleId,
59
66
  ]
60
67
  .filter(Boolean)
61
68
  .join(' ');
@@ -104,7 +111,7 @@ export const ActionPrompt = ({
104
111
  id={mediaId}
105
112
  size={48}
106
113
  sentiment={sentiment}
107
- iconLabel={Boolean(media['aria-hidden']) ? null : media['aria-label']}
114
+ iconLabel={media['aria-hidden'] ? null : media['aria-label']}
108
115
  />
109
116
  );
110
117
  };
@@ -148,9 +155,9 @@ export const ActionPrompt = ({
148
155
  </Button>
149
156
  </>
150
157
  }
151
- onDismiss={onDismiss}
152
158
  role="region"
153
- {...(Boolean(ariaLabel)
159
+ onDismiss={onDismiss}
160
+ {...(ariaLabel
154
161
  ? { 'aria-label': ariaLabel }
155
162
  : {
156
163
  'aria-labelledby': ariaLabelledByIds,
@@ -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.