@transferwise/components 0.0.0-experimental-4d90243 → 0.0.0-experimental-a308934

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 (115) hide show
  1. package/build/field/Field.js +5 -4
  2. package/build/field/Field.js.map +1 -1
  3. package/build/field/Field.mjs +5 -4
  4. package/build/field/Field.mjs.map +1 -1
  5. package/build/i18n/en.json +2 -0
  6. package/build/i18n/en.json.js +2 -0
  7. package/build/i18n/en.json.js.map +1 -1
  8. package/build/i18n/en.json.mjs +2 -0
  9. package/build/i18n/en.json.mjs.map +1 -1
  10. package/build/index.js +2 -0
  11. package/build/index.js.map +1 -1
  12. package/build/index.mjs +1 -0
  13. package/build/index.mjs.map +1 -1
  14. package/build/listItem/Prompt/ListItemPrompt.js +5 -4
  15. package/build/listItem/Prompt/ListItemPrompt.js.map +1 -1
  16. package/build/listItem/Prompt/ListItemPrompt.mjs +5 -4
  17. package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -1
  18. package/build/main.css +85 -29
  19. package/build/moneyInput/MoneyInput.js +12 -28
  20. package/build/moneyInput/MoneyInput.js.map +1 -1
  21. package/build/moneyInput/MoneyInput.mjs +14 -30
  22. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  23. package/build/moneyInput/currencyFormatting.js +2 -8
  24. package/build/moneyInput/currencyFormatting.js.map +1 -1
  25. package/build/moneyInput/currencyFormatting.mjs +4 -5
  26. package/build/moneyInput/currencyFormatting.mjs.map +1 -1
  27. package/build/prompt/ActionPrompt/ActionPrompt.js +8 -40
  28. package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
  29. package/build/prompt/ActionPrompt/ActionPrompt.mjs +8 -40
  30. package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -1
  31. package/build/prompt/CriticalBanner/CriticalBanner.js +145 -0
  32. package/build/prompt/CriticalBanner/CriticalBanner.js.map +1 -0
  33. package/build/prompt/CriticalBanner/CriticalBanner.mjs +143 -0
  34. package/build/prompt/CriticalBanner/CriticalBanner.mjs.map +1 -0
  35. package/build/prompt/CriticalBanner/helpers.js +29 -0
  36. package/build/prompt/CriticalBanner/helpers.js.map +1 -0
  37. package/build/prompt/CriticalBanner/helpers.mjs +26 -0
  38. package/build/prompt/CriticalBanner/helpers.mjs.map +1 -0
  39. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js +2 -0
  40. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
  41. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs +2 -0
  42. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
  43. package/build/prompt/common/Expander/Expander.js +47 -0
  44. package/build/prompt/common/Expander/Expander.js.map +1 -0
  45. package/build/prompt/common/Expander/Expander.messages.js +17 -0
  46. package/build/prompt/common/Expander/Expander.messages.js.map +1 -0
  47. package/build/prompt/common/Expander/Expander.messages.mjs +13 -0
  48. package/build/prompt/common/Expander/Expander.messages.mjs.map +1 -0
  49. package/build/prompt/common/Expander/Expander.mjs +44 -0
  50. package/build/prompt/common/Expander/Expander.mjs.map +1 -0
  51. package/build/prompt/helpers/promptMedia.js +52 -0
  52. package/build/prompt/helpers/promptMedia.js.map +1 -0
  53. package/build/prompt/helpers/promptMedia.mjs +50 -0
  54. package/build/prompt/helpers/promptMedia.mjs.map +1 -0
  55. package/build/styles/main.css +85 -29
  56. package/build/styles/prompt/CriticalBanner/CriticalBanner.css +39 -0
  57. package/build/styles/prompt/common/Expander/Expander.css +8 -0
  58. package/build/typeahead/Typeahead.js +5 -4
  59. package/build/typeahead/Typeahead.js.map +1 -1
  60. package/build/typeahead/Typeahead.mjs +5 -4
  61. package/build/typeahead/Typeahead.mjs.map +1 -1
  62. package/build/types/index.d.ts +2 -2
  63. package/build/types/index.d.ts.map +1 -1
  64. package/build/types/moneyInput/MoneyInput.d.ts +0 -6
  65. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  66. package/build/types/moneyInput/currencyFormatting.d.ts +3 -4
  67. package/build/types/moneyInput/currencyFormatting.d.ts.map +1 -1
  68. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +2 -11
  69. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
  70. package/build/types/prompt/CriticalBanner/CriticalBanner.d.ts +43 -0
  71. package/build/types/prompt/CriticalBanner/CriticalBanner.d.ts.map +1 -0
  72. package/build/types/prompt/CriticalBanner/helpers.d.ts +18 -0
  73. package/build/types/prompt/CriticalBanner/helpers.d.ts.map +1 -0
  74. package/build/types/prompt/CriticalBanner/index.d.ts +3 -0
  75. package/build/types/prompt/CriticalBanner/index.d.ts.map +1 -0
  76. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts +8 -3
  77. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
  78. package/build/types/prompt/common/Expander/Expander.d.ts +20 -0
  79. package/build/types/prompt/common/Expander/Expander.d.ts.map +1 -0
  80. package/build/types/prompt/common/Expander/Expander.messages.d.ts +14 -0
  81. package/build/types/prompt/common/Expander/Expander.messages.d.ts.map +1 -0
  82. package/build/types/prompt/helpers/promptMedia.d.ts +22 -0
  83. package/build/types/prompt/helpers/promptMedia.d.ts.map +1 -0
  84. package/build/types/prompt/index.d.ts +2 -0
  85. package/build/types/prompt/index.d.ts.map +1 -1
  86. package/build/types/test-utils/index.d.ts +4 -0
  87. package/build/types/test-utils/index.d.ts.map +1 -1
  88. package/package.json +3 -3
  89. package/src/i18n/en.json +2 -0
  90. package/src/index.ts +2 -2
  91. package/src/main.css +85 -29
  92. package/src/main.less +3 -1
  93. package/src/moneyInput/MoneyInput.story.tsx +1 -10
  94. package/src/moneyInput/MoneyInput.test.story.tsx +1 -141
  95. package/src/moneyInput/MoneyInput.test.tsx +0 -45
  96. package/src/moneyInput/MoneyInput.tsx +3 -27
  97. package/src/moneyInput/currencyFormatting.ts +5 -11
  98. package/src/prompt/ActionPrompt/ActionPrompt.tsx +9 -62
  99. package/src/prompt/CriticalBanner/CriticalBanner.accessibility.docs.mdx +131 -0
  100. package/src/prompt/CriticalBanner/CriticalBanner.css +39 -0
  101. package/src/prompt/CriticalBanner/CriticalBanner.less +45 -0
  102. package/src/prompt/CriticalBanner/CriticalBanner.story.tsx +476 -0
  103. package/src/prompt/CriticalBanner/CriticalBanner.test.story.tsx +67 -0
  104. package/src/prompt/CriticalBanner/CriticalBanner.tsx +188 -0
  105. package/src/prompt/CriticalBanner/helpers.ts +39 -0
  106. package/src/prompt/CriticalBanner/index.ts +2 -0
  107. package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +9 -2
  108. package/src/prompt/common/Expander/Expander.css +8 -0
  109. package/src/prompt/common/Expander/Expander.less +9 -0
  110. package/src/prompt/common/Expander/Expander.messages.ts +14 -0
  111. package/src/prompt/common/Expander/Expander.test.tsx +167 -0
  112. package/src/prompt/common/Expander/Expander.tsx +83 -0
  113. package/src/prompt/helpers/promptMedia.tsx +79 -0
  114. package/src/prompt/index.ts +4 -0
  115. package/src/sentimentSurface/SentimentSurface.story.tsx +43 -17
@@ -0,0 +1,476 @@
1
+ import { ReactElement, useState } from 'react';
2
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
3
+ import { fn } from 'storybook/test';
4
+ import { Bank, Star, Travel, Briefcase } from '@transferwise/icons';
5
+ import { lorem10, lorem20 } from '../../test-utils';
6
+ import Button from '../../button';
7
+ import Title from '../../title';
8
+ import { CriticalBanner, type CriticalBannerProps } from './CriticalBanner';
9
+
10
+ const withComponentGrid =
11
+ ({ maxWidth = 'auto', gap = '1rem' } = {}) =>
12
+ (Story: () => ReactElement) => (
13
+ <div
14
+ style={{
15
+ width: '100%',
16
+ display: 'flex',
17
+ flexDirection: 'column',
18
+ justifyContent: 'flex-start',
19
+ alignItems: 'flex-start',
20
+ alignContent: 'flex-start',
21
+ gap,
22
+ maxWidth,
23
+ }}
24
+ >
25
+ <Story />
26
+ </div>
27
+ );
28
+
29
+ const meta: Meta<typeof CriticalBanner> = {
30
+ title: 'Prompts/CriticalBanner',
31
+ component: CriticalBanner,
32
+ tags: ['new'],
33
+ decorators: [withComponentGrid()],
34
+ args: {
35
+ title: 'Critical Action Required',
36
+ description: 'Please complete the following action to continue.',
37
+ action: { label: 'Take Action', onClick: fn() },
38
+ expanded: true,
39
+ onToggle: fn(),
40
+ },
41
+ argTypes: {
42
+ sentiment: {
43
+ control: 'select',
44
+ options: ['success', 'negative', 'neutral', 'warning'],
45
+ },
46
+ title: {
47
+ control: 'text',
48
+ table: {
49
+ type: { summary: 'ReactNode' },
50
+ },
51
+ },
52
+ description: {
53
+ control: 'text',
54
+ table: {
55
+ type: { summary: 'ReactNode' },
56
+ },
57
+ },
58
+ expanded: {
59
+ control: 'boolean',
60
+ },
61
+ media: {
62
+ control: false,
63
+ table: {
64
+ type: {
65
+ summary: 'CriticalBannerMedia',
66
+ detail:
67
+ '{ imgSrc?: string; avatar?: { imgSrc?: string; profileName?: string; profileType?: string; asset?: ReactNode; badge?: { flagCode: string } }; aria-label?: string }',
68
+ },
69
+ },
70
+ },
71
+ action: {
72
+ control: false,
73
+ table: {
74
+ type: {
75
+ summary: 'CriticalBannerAction',
76
+ detail: '{ label: ReactNode; onClick?: () => void; href?: string; target?: string }',
77
+ },
78
+ },
79
+ },
80
+ actionSecondary: {
81
+ control: false,
82
+ table: {
83
+ type: {
84
+ summary: 'CriticalBannerActionSecondary',
85
+ detail: '{ label: ReactNode; onClick?: () => void; href?: string; target?: string }',
86
+ },
87
+ },
88
+ },
89
+ },
90
+ };
91
+
92
+ export default meta;
93
+ type Story = StoryObj<typeof CriticalBanner>;
94
+
95
+ /**
96
+ * Convenience controls for previewing rich markup,
97
+ * not otherwise possible via Storybook
98
+ */
99
+ type PreviewStoryArgs = CriticalBannerProps & {
100
+ previewMedia: CriticalBannerProps['media'];
101
+ previewSecondaryAction: boolean;
102
+ };
103
+
104
+ const previewArgGroup = {
105
+ category: 'Storybook Preview options',
106
+ type: {
107
+ summary: undefined,
108
+ },
109
+ };
110
+
111
+ const MEDIA_OPTIONS: Record<string, CriticalBannerProps['media']> = {
112
+ undefined,
113
+ 'Custom image (no badge)': { imgSrc: '../../wise-card.svg' },
114
+ 'Avatar: Profile image': { avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } },
115
+ 'Avatar: Business (GB badge)': {
116
+ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } },
117
+ },
118
+ 'Avatar: Personal (EU badge)': {
119
+ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } },
120
+ },
121
+ 'Avatar: Icon (Bank)': { avatar: { asset: <Bank /> } },
122
+ 'Avatar: Icon (Star)': { avatar: { asset: <Star /> } },
123
+ 'Avatar: Icon (Travel)': { avatar: { asset: <Travel /> } },
124
+ 'Avatar: Icon (Briefcase)': { avatar: { asset: <Briefcase /> } },
125
+ 'Avatar: Initials': { avatar: { profileName: 'John Doe' } },
126
+ };
127
+
128
+ const previewArgTypes = {
129
+ previewMedia: {
130
+ name: 'Preview with `media`',
131
+ control: 'select',
132
+ options: [
133
+ 'undefined',
134
+ 'Custom image (no badge)',
135
+ 'Avatar: Profile image',
136
+ 'Avatar: Business (GB badge)',
137
+ 'Avatar: Personal (EU badge)',
138
+ 'Avatar: Icon (Bank)',
139
+ 'Avatar: Icon (Star)',
140
+ 'Avatar: Icon (Travel)',
141
+ 'Avatar: Icon (Briefcase)',
142
+ 'Avatar: Initials',
143
+ ],
144
+ mapping: MEDIA_OPTIONS,
145
+ table: previewArgGroup,
146
+ },
147
+ previewSecondaryAction: {
148
+ name: 'Preview with `actionSecondary`',
149
+ control: 'boolean',
150
+ table: previewArgGroup,
151
+ },
152
+ } as const;
153
+
154
+ const getPropsForPreview = (
155
+ args: PreviewStoryArgs,
156
+ ): [CriticalBannerProps, Partial<CriticalBannerProps>] => {
157
+ const { previewMedia, previewSecondaryAction, ...props } = args;
158
+
159
+ return [
160
+ props,
161
+ {
162
+ media: previewMedia,
163
+ actionSecondary: previewSecondaryAction ? { label: 'Cancel', onClick: fn() } : undefined,
164
+ },
165
+ ];
166
+ };
167
+
168
+ /**
169
+ * The Playground story demonstrates the CriticalBanner with interactive controls.
170
+ * The story manages the `expanded` state internally to enable the toggle behavior.
171
+ */
172
+ export const Playground: StoryObj<PreviewStoryArgs> = {
173
+ argTypes: {
174
+ sentiment: {
175
+ control: 'radio',
176
+ options: ['success', 'negative', 'neutral', 'warning'],
177
+ },
178
+ ...previewArgTypes,
179
+ },
180
+ args: {
181
+ previewMedia: { avatar: undefined },
182
+ previewSecondaryAction: true,
183
+ sentiment: 'negative',
184
+ },
185
+ render: function PlaygroundRender(args: PreviewStoryArgs) {
186
+ const [expanded, setExpanded] = useState(args.expanded);
187
+ const [props, previewProps] = getPropsForPreview(args);
188
+ return (
189
+ <CriticalBanner
190
+ {...props}
191
+ {...previewProps}
192
+ expanded={expanded}
193
+ onToggle={() => setExpanded(!expanded)}
194
+ />
195
+ );
196
+ },
197
+ };
198
+
199
+ /**
200
+ * CriticalBanner supports multiple sentiments to communicate different types of critical messages:
201
+ * - `negative` (default): Critical errors or issues
202
+ * - `warning`: Warning messages requiring attention
203
+ * - `neutral`: General important notices
204
+ * - `success`: Success confirmations
205
+ */
206
+ export const Sentiments: Story = {
207
+ render: () => {
208
+ const [expandedStates, setExpandedStates] = useState<Record<string, boolean>>({
209
+ success: true,
210
+ warning: true,
211
+ negative: true,
212
+ neutral: true,
213
+ });
214
+
215
+ const handleToggle = (sentiment: string) => {
216
+ setExpandedStates((prev) => ({ ...prev, [sentiment]: !prev[sentiment] }));
217
+ };
218
+
219
+ return (
220
+ <>
221
+ {(['negative', 'warning', 'neutral', 'success'] as const).map((sentiment) => (
222
+ <CriticalBanner
223
+ key={sentiment}
224
+ sentiment={sentiment}
225
+ title={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} Banner`}
226
+ description={lorem10}
227
+ action={{ label: 'Primary', onClick: fn() }}
228
+ actionSecondary={{ label: 'Secondary', onClick: fn() }}
229
+ expanded={expandedStates[sentiment]}
230
+ onToggle={() => handleToggle(sentiment)}
231
+ />
232
+ ))}
233
+ </>
234
+ );
235
+ },
236
+ };
237
+
238
+ /**
239
+ * CriticalBanner can have just a primary action, both primary and secondary actions, or no actions at all.
240
+ */
241
+ export const Actions: Story = {
242
+ render: () => {
243
+ const [expandedStates, setExpandedStates] = useState({
244
+ noActions: true,
245
+ primaryOnly: true,
246
+ both: true,
247
+ });
248
+
249
+ return (
250
+ <>
251
+ <CriticalBanner
252
+ sentiment="negative"
253
+ title="No actions"
254
+ description="This banner has no action buttons."
255
+ expanded={expandedStates.noActions}
256
+ onToggle={() => setExpandedStates((prev) => ({ ...prev, noActions: !prev.noActions }))}
257
+ />
258
+ <CriticalBanner
259
+ sentiment="warning"
260
+ title="Primary action only"
261
+ description="This banner has only one action button."
262
+ action={{ label: 'Take Action', onClick: fn() }}
263
+ expanded={expandedStates.primaryOnly}
264
+ onToggle={() =>
265
+ setExpandedStates((prev) => ({ ...prev, primaryOnly: !prev.primaryOnly }))
266
+ }
267
+ />
268
+ <CriticalBanner
269
+ sentiment="negative"
270
+ title="Primary and secondary actions"
271
+ description="This banner has both action buttons."
272
+ action={{ label: 'Confirm', onClick: fn() }}
273
+ actionSecondary={{ label: 'Cancel', onClick: fn() }}
274
+ expanded={expandedStates.both}
275
+ onToggle={() => setExpandedStates((prev) => ({ ...prev, both: !prev.both }))}
276
+ />
277
+ </>
278
+ );
279
+ },
280
+ };
281
+
282
+ /**
283
+ * CriticalBanner supports various media types to enhance visual communication.
284
+ *
285
+ * **Default**: Each sentiment has a default status icon that appears when no media is provided.
286
+ *
287
+ * **Icon Overrides**: Replace default icons with custom images, avatars with images, avatars with initials,
288
+ * avatars with custom icons, or avatars with profile types and badges.
289
+ */
290
+ export const MediaTypes: Story = {
291
+ render: () => {
292
+ const [expandedStates, setExpandedStates] = useState<Record<string, boolean>>({
293
+ default: true,
294
+ customImage: true,
295
+ avatarImage: true,
296
+ avatarInitials: true,
297
+ avatarIcon: true,
298
+ businessBadge: true,
299
+ personalBadge: true,
300
+ });
301
+
302
+ const handleToggle = (key: string) => {
303
+ setExpandedStates((prev) => ({ ...prev, [key]: !prev[key] }));
304
+ };
305
+
306
+ return (
307
+ <>
308
+ <Title type="title-body">Default</Title>
309
+ <CriticalBanner
310
+ sentiment="negative"
311
+ title="Default status icon"
312
+ description="When no media is provided, the sentiment's default status icon is displayed."
313
+ action={{ label: 'Continue', onClick: fn() }}
314
+ expanded={expandedStates.default}
315
+ onToggle={() => handleToggle('default')}
316
+ />
317
+
318
+ <Title type="title-body">Icon Overrides</Title>
319
+ <CriticalBanner
320
+ sentiment="negative"
321
+ title="Custom image"
322
+ description={lorem10}
323
+ media={{ imgSrc: '../../wise-card.svg' }}
324
+ action={{ label: 'View card', onClick: fn() }}
325
+ expanded={expandedStates.customImage}
326
+ onToggle={() => handleToggle('customImage')}
327
+ />
328
+ <CriticalBanner
329
+ sentiment="negative"
330
+ title="Avatar with image"
331
+ description={lorem10}
332
+ media={{ avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } }}
333
+ action={{ label: 'View profile', onClick: fn() }}
334
+ expanded={expandedStates.avatarImage}
335
+ onToggle={() => handleToggle('avatarImage')}
336
+ />
337
+ <CriticalBanner
338
+ sentiment="negative"
339
+ title="Avatar with initials"
340
+ description={lorem10}
341
+ media={{ avatar: { profileName: 'John Doe' } }}
342
+ action={{ label: 'Send invite', onClick: fn() }}
343
+ expanded={expandedStates.avatarInitials}
344
+ onToggle={() => handleToggle('avatarInitials')}
345
+ />
346
+ <CriticalBanner
347
+ sentiment="warning"
348
+ title="Avatar with custom icon"
349
+ description={lorem10}
350
+ media={{ avatar: { asset: <Bank /> } }}
351
+ action={{ label: 'Connect bank', onClick: fn() }}
352
+ expanded={expandedStates.avatarIcon}
353
+ onToggle={() => handleToggle('avatarIcon')}
354
+ />
355
+ <CriticalBanner
356
+ sentiment="neutral"
357
+ title="Business profile with badge"
358
+ description={lorem10}
359
+ media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
360
+ action={{ label: 'Open account', onClick: fn() }}
361
+ expanded={expandedStates.businessBadge}
362
+ onToggle={() => handleToggle('businessBadge')}
363
+ />
364
+ <CriticalBanner
365
+ sentiment="negative"
366
+ title="Personal profile with badge"
367
+ description={lorem10}
368
+ media={{ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } } }}
369
+ action={{ label: 'Verify', onClick: fn() }}
370
+ expanded={expandedStates.personalBadge}
371
+ onToggle={() => handleToggle('personalBadge')}
372
+ />
373
+ </>
374
+ );
375
+ },
376
+ };
377
+
378
+ /**
379
+ * The CriticalBanner is fully controlled - the parent component manages the `expanded` state.
380
+ * Clicking the chevron calls `onToggle`, allowing the parent to update the state.
381
+ */
382
+ export const ExpandCollapse: Story = {
383
+ render: () => {
384
+ const [expanded, setExpanded] = useState(true);
385
+
386
+ return (
387
+ <>
388
+ <CriticalBanner
389
+ sentiment="negative"
390
+ title="Your account requires verification"
391
+ description="Please verify your identity to continue using all features. This process typically takes 2-3 minutes."
392
+ action={{ label: 'Verify now', onClick: fn() }}
393
+ actionSecondary={{ label: 'Learn more', onClick: fn() }}
394
+ expanded={expanded}
395
+ onToggle={() => setExpanded(!expanded)}
396
+ />
397
+ <div style={{ marginTop: '1rem' }}>
398
+ <Button v2 onClick={() => setExpanded(!expanded)}>
399
+ {expanded ? 'Collapse externally' : 'Expand externally'}
400
+ </Button>
401
+ </div>
402
+ </>
403
+ );
404
+ },
405
+ };
406
+
407
+ /**
408
+ * While the component itself will stretch to the full available width, the text container will be
409
+ * capped at `480px` to ensure optimal readability.
410
+ */
411
+ export const ParagraphWidth: Story = {
412
+ parameters: {
413
+ docs: {
414
+ canvas: {
415
+ sourceState: 'hidden',
416
+ },
417
+ },
418
+ },
419
+ render: () => {
420
+ const [expanded, setExpanded] = useState(true);
421
+ return (
422
+ <CriticalBanner
423
+ title={lorem10}
424
+ description={lorem20}
425
+ sentiment="negative"
426
+ action={{ label: 'View details', onClick: fn() }}
427
+ expanded={expanded}
428
+ onToggle={() => setExpanded(!expanded)}
429
+ />
430
+ );
431
+ },
432
+ };
433
+
434
+ /**
435
+ * Demonstrates the accessibility features of CriticalBanner.
436
+ * The component uses aria-live="assertive" to immediately announce changes to screen readers.
437
+ *
438
+ * To test with a screen reader:
439
+ * 1. Enable your screen reader (VoiceOver on Mac: Cmd+F5, NVDA on Windows)
440
+ * 2. Click "Show banner" - the banner content should be announced immediately
441
+ * 3. Click the chevron to expand/collapse - changes should be announced
442
+ * 4. Click "Hide banner" and then "Show banner" again - it should announce again
443
+ *
444
+ * Announcement behavior:
445
+ * - Collapsed with title: Announces title only
446
+ * - Collapsed without title: Announces description only
447
+ * - Expanded: Announces title, description, and action button labels
448
+ */
449
+ export const LiveRegionAnnouncements: Story = {
450
+ render: () => {
451
+ const [showBanner, setShowBanner] = useState(false);
452
+ const [expanded, setExpanded] = useState(false);
453
+
454
+ return (
455
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
456
+ <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
457
+ <Button v2 onClick={() => setShowBanner(!showBanner)}>
458
+ {showBanner ? 'Hide banner' : 'Show banner'}
459
+ </Button>
460
+ </div>
461
+
462
+ {showBanner && (
463
+ <CriticalBanner
464
+ sentiment="negative"
465
+ title="Your account requires verification"
466
+ description="Please verify your identity to continue using all features"
467
+ action={{ label: 'Verify now', onClick: fn() }}
468
+ actionSecondary={{ label: 'Learn more', onClick: fn() }}
469
+ expanded={expanded}
470
+ onToggle={() => setExpanded(!expanded)}
471
+ />
472
+ )}
473
+ </div>
474
+ );
475
+ },
476
+ };
@@ -0,0 +1,67 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { fn } from 'storybook/test';
3
+ import { CriticalBanner } from './CriticalBanner';
4
+
5
+ const meta: Meta<typeof CriticalBanner> = {
6
+ title: 'Prompts/CriticalBanner/Tests',
7
+ component: CriticalBanner,
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof CriticalBanner>;
12
+
13
+ const longDescription =
14
+ 'DescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescriptionDescription';
15
+
16
+ /**
17
+ * Shows all four collapsed/expanded × with-title/without-title combinations.
18
+ * Used for visual regression testing of the collapsed state behaviour:
19
+ * - With title: description is hidden when collapsed
20
+ * - Without title: description is visible (up to 2 lines) when collapsed
21
+ */
22
+ export const CollapsedVariants: Story = {
23
+ render: () => (
24
+ <div
25
+ style={{
26
+ display: 'grid',
27
+ gridTemplateColumns: '1fr 1fr',
28
+ gap: '1rem',
29
+ width: '100%',
30
+ }}
31
+ >
32
+ {/* Row 1: Expanded */}
33
+ <CriticalBanner
34
+ sentiment="negative"
35
+ title="Title (optional)"
36
+ description="Description"
37
+ action={{ label: 'Primary', onClick: fn() }}
38
+ expanded
39
+ onToggle={fn()}
40
+ />
41
+ <CriticalBanner
42
+ sentiment="negative"
43
+ description={longDescription}
44
+ action={{ label: 'Primary', onClick: fn() }}
45
+ expanded
46
+ onToggle={fn()}
47
+ />
48
+
49
+ {/* Row 2: Collapsed */}
50
+ <CriticalBanner
51
+ sentiment="negative"
52
+ title="Title (optional)"
53
+ description="Description"
54
+ action={{ label: 'Primary', onClick: fn() }}
55
+ expanded={false}
56
+ onToggle={fn()}
57
+ />
58
+ <CriticalBanner
59
+ sentiment="negative"
60
+ description={longDescription}
61
+ action={{ label: 'Primary', onClick: fn() }}
62
+ expanded={false}
63
+ onToggle={fn()}
64
+ />
65
+ </div>
66
+ ),
67
+ };