@transferwise/components 46.147.0 → 46.148.1

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 (124) hide show
  1. package/build/container/Container.js.map +1 -1
  2. package/build/container/Container.mjs.map +1 -1
  3. package/build/main.css +63 -6
  4. package/build/prompt/CriticalBanner/CriticalBanner.js +81 -68
  5. package/build/prompt/CriticalBanner/CriticalBanner.js.map +1 -1
  6. package/build/prompt/CriticalBanner/CriticalBanner.mjs +82 -69
  7. package/build/prompt/CriticalBanner/CriticalBanner.mjs.map +1 -1
  8. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
  9. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
  10. package/build/styles/main.css +63 -6
  11. package/build/styles/prompt/ActionPrompt/ActionPrompt.css +2 -1
  12. package/build/styles/prompt/CriticalBanner/CriticalBanner.css +50 -1
  13. package/build/styles/prompt/InfoPrompt/InfoPrompt.css +2 -1
  14. package/build/styles/prompt/InlinePrompt/InlinePrompt.css +2 -1
  15. package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +2 -2
  16. package/build/types/container/Container.d.ts +2 -2
  17. package/build/types/container/Container.d.ts.map +1 -1
  18. package/build/types/iconButton/IconButton.d.ts +1 -1
  19. package/build/types/prompt/CriticalBanner/CriticalBanner.d.ts.map +1 -1
  20. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts +1 -1
  21. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/accordion/Accordion.story.tsx +25 -0
  24. package/src/avatarLayout/AvatarLayout.story.tsx +10 -0
  25. package/src/avatarView/AvatarView.story.tsx +8 -0
  26. package/src/body/Body.story.tsx +12 -0
  27. package/src/button/_stories/Button.story.tsx +7 -1
  28. package/src/calendar/Calendar.story.tsx +19 -7
  29. package/src/carousel/Carousel.story.tsx +35 -0
  30. package/src/checkbox/Checkbox.story.tsx +20 -0
  31. package/src/checkboxButton/CheckboxButton.story.tsx +16 -0
  32. package/src/chevron/Chevron.story.tsx +6 -0
  33. package/src/chips/Chips.story.tsx +23 -0
  34. package/src/circularButton/CircularButton.story.tsx +13 -0
  35. package/src/common/baseCard/BaseCard.story.tsx +12 -0
  36. package/src/common/bottomSheet/BottomSheet.story.tsx +21 -0
  37. package/src/common/circle/Circle.story.tsx +11 -0
  38. package/src/container/Container.story.tsx +12 -0
  39. package/src/container/Container.tsx +2 -2
  40. package/src/dateInput/DateInput.story.tsx +20 -0
  41. package/src/dateLookup/DateLookup.story.tsx +23 -0
  42. package/src/decision/Decision.story.tsx +36 -0
  43. package/src/definitionList/DefinitionList.story.tsx +16 -0
  44. package/src/dimmer/Dimmer.story.tsx +24 -0
  45. package/src/display/Display.story.tsx +11 -0
  46. package/src/divider/Divider.story.tsx +6 -0
  47. package/src/drawer/Drawer.story.tsx +25 -0
  48. package/src/dropFade/DropFade.story.tsx +27 -0
  49. package/src/emphasis/Emphasis.story.tsx +10 -0
  50. package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +37 -0
  51. package/src/field/Field.story.tsx +16 -0
  52. package/src/flowNavigation/FlowNavigation.story.tsx +25 -0
  53. package/src/header/Header.story.tsx +17 -0
  54. package/src/iconButton/IconButton.story.tsx +14 -0
  55. package/src/image/Image.story.tsx +11 -0
  56. package/src/info/Info.story.tsx +10 -0
  57. package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.tsx +23 -0
  58. package/src/inputs/InputGroup.story.tsx +37 -0
  59. package/src/inputs/SearchInput.story.tsx +22 -0
  60. package/src/inputs/SelectInput/_stories/SelectInput.story.tsx +42 -0
  61. package/src/inputs/TextArea.story.tsx +22 -0
  62. package/src/instructionsList/InstructionsList.story.tsx +19 -0
  63. package/src/label/Label.story.tsx +17 -0
  64. package/src/link/Link.story.tsx +11 -0
  65. package/src/list/List.story.tsx +19 -0
  66. package/src/listItem/_stories/ListItem.story.tsx +20 -0
  67. package/src/loader/Loader.story.tsx +6 -0
  68. package/src/logo/Logo.story.tsx +6 -0
  69. package/src/main.css +63 -6
  70. package/src/markdown/Markdown.story.tsx +17 -0
  71. package/src/modal/Modal.story.tsx +23 -0
  72. package/src/money/Money.story.tsx +7 -0
  73. package/src/moneyInput/MoneyInput.story.tsx +34 -0
  74. package/src/nudge/Nudge.story.tsx +17 -0
  75. package/src/overlayHeader/OverlayHeader.story.tsx +10 -0
  76. package/src/phoneNumberInput/PhoneNumberInput.story.tsx +23 -0
  77. package/src/popover/Popover.story.tsx +12 -0
  78. package/src/primitives/PrimitiveAnchor/stories/PrimitiveAnchor.story.tsx +11 -0
  79. package/src/primitives/PrimitiveButton/stories/PrimitiveButton.story.tsx +11 -0
  80. package/src/processIndicator/ProcessIndicator.story.tsx +10 -0
  81. package/src/progress/Progress.story.tsx +6 -0
  82. package/src/progressBar/ProgressBar.story.tsx +12 -0
  83. package/src/promoCard/PromoCard.story.tsx +15 -0
  84. package/src/promoCard/PromoCardGroup.story.tsx +28 -0
  85. package/src/prompt/ActionPrompt/ActionPrompt.css +2 -1
  86. package/src/prompt/ActionPrompt/ActionPrompt.less +2 -1
  87. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +31 -0
  88. package/src/prompt/CriticalBanner/CriticalBanner.accessibility.docs.mdx +9 -0
  89. package/src/prompt/CriticalBanner/CriticalBanner.css +50 -1
  90. package/src/prompt/CriticalBanner/CriticalBanner.less +74 -1
  91. package/src/prompt/CriticalBanner/CriticalBanner.story.tsx +181 -170
  92. package/src/prompt/CriticalBanner/CriticalBanner.test.story.tsx +25 -6
  93. package/src/prompt/CriticalBanner/CriticalBanner.test.tsx +37 -0
  94. package/src/prompt/CriticalBanner/CriticalBanner.tsx +96 -84
  95. package/src/prompt/CriticalBanner/CriticalBanner.vars.less +1 -0
  96. package/src/prompt/InfoPrompt/InfoPrompt.css +2 -1
  97. package/src/prompt/InfoPrompt/InfoPrompt.less +2 -1
  98. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +30 -0
  99. package/src/prompt/InlinePrompt/InlinePrompt.css +2 -1
  100. package/src/prompt/InlinePrompt/InlinePrompt.less +2 -1
  101. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +14 -0
  102. package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +2 -2
  103. package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +1 -1
  104. package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +1 -1
  105. package/src/radio/Radio.story.tsx +34 -0
  106. package/src/radioGroup/RadioGroup.story.tsx +26 -0
  107. package/src/section/Section.story.tsx +15 -0
  108. package/src/segmentedControl/SegmentedControl.story.tsx +27 -0
  109. package/src/sentimentSurface/SentimentSurface.story.tsx +11 -0
  110. package/src/slidingPanel/SlidingPanel.story.tsx +19 -0
  111. package/src/snackbar/Snackbar.story.tsx +24 -0
  112. package/src/statusIcon/StatusIcon.story.tsx +6 -0
  113. package/src/stepper/Stepper.story.tsx +30 -0
  114. package/src/sticky/Sticky.story.tsx +22 -1
  115. package/src/switch/Switch.story.tsx +17 -0
  116. package/src/table/Table.story.tsx +32 -0
  117. package/src/tabs/Tabs.story.tsx +31 -0
  118. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +23 -0
  119. package/src/tile/Tile.story.tsx +13 -0
  120. package/src/title/Title.story.tsx +12 -0
  121. package/src/tooltip/Tooltip.story.tsx +8 -0
  122. package/src/typeahead/Typeahead.story.tsx +33 -0
  123. package/src/upload/Upload.story.tsx +24 -0
  124. package/src/uploadInput/UploadInput.story.tsx +31 -0
@@ -1,11 +1,14 @@
1
- import { useState, useEffect, useRef, type ReactNode } from 'react';
1
+ import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { Meta, StoryObj, Decorator } from '@storybook/react-webpack5';
4
4
  import { fn } from 'storybook/test';
5
5
  import { Bank, Briefcase, Star, Travel } from '@transferwise/icons';
6
+ import {
7
+ createSandboxStory,
8
+ globalScope,
9
+ } from '../../../.storybook/components/sandbox/SandboxEditor';
6
10
  import Button from '../../button';
7
11
  import { CriticalBanner, type CriticalBannerProps } from './CriticalBanner';
8
- import { withVariantConfig } from '../../../.storybook/helpers';
9
12
 
10
13
  /**
11
14
  * **Design guidance**: <a href="https://docs.wise.design/components/critical-banner" target="_blank">wise.design/components/critical-banner</a>
@@ -16,7 +19,7 @@ const meta = {
16
19
  component: CriticalBanner,
17
20
  title: 'Prompts/CriticalBanner',
18
21
  tags: ['new'],
19
- parameters: { docs: { toc: true } },
22
+ parameters: { docs: { toc: true }, padding: '0', },
20
23
  args: {
21
24
  title: 'Your account requires verification',
22
25
  description: 'Please verify your identity to continue using all features.',
@@ -120,6 +123,43 @@ const previewArgTypes = {
120
123
  },
121
124
  } as const;
122
125
 
126
+ const hiddenArgTypes = {
127
+ sentiment: { table: { disable: true } },
128
+ title: { table: { disable: true } },
129
+ description: { table: { disable: true } },
130
+ action: { table: { disable: true } },
131
+ actionSecondary: { table: { disable: true } },
132
+ media: { table: { disable: true } },
133
+ expanded: { table: { disable: true } },
134
+ onToggle: { table: { disable: true } },
135
+ id: { table: { disable: true } },
136
+ className: { table: { disable: true } },
137
+ 'data-testid': { table: { disable: true } },
138
+ } as const;
139
+
140
+ const AnimatedEntryPreview = (args: CriticalBannerProps) => {
141
+ const { id = 'animated-entry' } = args;
142
+ const [bannerInstance, setBannerInstance] = useState(0);
143
+ const animatedBanner = useMemo(
144
+ () => ({
145
+ ...args,
146
+ id: `${id}-${bannerInstance}`,
147
+ }),
148
+ [args, bannerInstance, id],
149
+ );
150
+
151
+ return (
152
+ <>
153
+ <CriticalBanner key={animatedBanner.id} {...animatedBanner} />
154
+ <div className="p-t-3">
155
+ <Button block v2 onClick={() => setBannerInstance((current) => current + 1)}>
156
+ Replay
157
+ </Button>
158
+ </div>
159
+ </>
160
+ );
161
+ };
162
+
123
163
  /** Renders children inside an iframe that inherits the parent page's stylesheets. */
124
164
  function IframeContainer({ width, children }: { width: number; children: ReactNode }) {
125
165
  const iframeRef = useRef<HTMLIFrameElement>(null);
@@ -132,25 +172,11 @@ function IframeContainer({ width, children }: { width: number; children: ReactNo
132
172
  const doc = iframe.contentDocument;
133
173
  if (!doc) return;
134
174
 
135
- doc.head.innerHTML = '';
136
- Array.from(document.styleSheets).forEach((sheet) => {
137
- try {
138
- if (sheet.href) {
139
- const link = doc.createElement('link');
140
- link.rel = 'stylesheet';
141
- link.href = sheet.href;
142
- doc.head.appendChild(link);
143
- } else if (sheet.cssRules) {
144
- const style = doc.createElement('style');
145
- style.textContent = Array.from(sheet.cssRules)
146
- .map((r) => r.cssText)
147
- .join('\n');
148
- doc.head.appendChild(style);
149
- }
150
- } catch {
151
- // Skip cross-origin sheets
152
- }
153
- });
175
+ doc.head.replaceChildren(
176
+ ...Array.from(document.head.querySelectorAll('link[rel="stylesheet"], style')).map((node) =>
177
+ node.cloneNode(true),
178
+ ),
179
+ );
154
180
 
155
181
  doc.body.style.margin = '0';
156
182
  setMountNode(doc.body);
@@ -174,18 +200,20 @@ function IframeContainer({ width, children }: { width: number; children: ReactNo
174
200
  );
175
201
  }
176
202
 
177
- const ContainerDecorator: Decorator = (Story) => (
178
- <div
179
- style={{
180
- width: '100%',
181
- display: 'flex',
182
- flexDirection: 'column',
183
- gap: '1rem',
184
- }}
185
- >
186
- <Story />
187
- </div>
188
- );
203
+ const ContainerDecorator =
204
+ (gap = '1rem'): Decorator =>
205
+ (Story) => (
206
+ <div
207
+ style={{
208
+ width: '100%',
209
+ display: 'flex',
210
+ flexDirection: 'column',
211
+ gap,
212
+ }}
213
+ >
214
+ <Story />
215
+ </div>
216
+ );
189
217
 
190
218
  /**
191
219
  * Interactive playground with all controls.
@@ -220,11 +248,31 @@ export const Playground: StoryObj<PreviewStoryArgs> = {
220
248
  },
221
249
  };
222
250
 
251
+ export const Sandbox = createSandboxStory({
252
+ code: `const App = () => {
253
+ const [expanded, setExpanded] = React.useState(true);
254
+
255
+ return (
256
+ <CriticalBanner
257
+ sentiment="negative"
258
+ title="Your account requires verification"
259
+ description="Please verify your identity to continue using all features."
260
+ action={{ label: 'Verify now', onClick: () => console.log('verify') }}
261
+ actionSecondary={{ label: 'Learn more', onClick: () => console.log('learn more') }}
262
+ media={{ avatar: { asset: <Bank /> } }}
263
+ expanded={expanded}
264
+ onToggle={() => setExpanded((prev) => !prev)}
265
+ />
266
+ );
267
+ };`,
268
+ scope: globalScope,
269
+ });
270
+
223
271
  /**
224
272
  * There are four sentiments, with negative being the default.
225
273
  */
226
274
  export const Sentiments: Story = {
227
- decorators: [ContainerDecorator],
275
+ decorators: [ContainerDecorator('2rem')],
228
276
  parameters: {
229
277
  docs: {
230
278
  source: {
@@ -235,19 +283,7 @@ export const Sentiments: Story = {
235
283
  },
236
284
  },
237
285
  },
238
- argTypes: {
239
- sentiment: { table: { disable: true } },
240
- title: { table: { disable: true } },
241
- description: { table: { disable: true } },
242
- action: { table: { disable: true } },
243
- actionSecondary: { table: { disable: true } },
244
- media: { table: { disable: true } },
245
- expanded: { table: { disable: true } },
246
- onToggle: { table: { disable: true } },
247
- id: { table: { disable: true } },
248
- className: { table: { disable: true } },
249
- 'data-testid': { table: { disable: true } },
250
- },
286
+ argTypes: hiddenArgTypes,
251
287
  render: function Render() {
252
288
  return (
253
289
  <>
@@ -270,7 +306,7 @@ export const Sentiments: Story = {
270
306
  * Banners can have just a primary action, both primary and secondary, or no actions at all.
271
307
  */
272
308
  export const Actions: Story = {
273
- decorators: [ContainerDecorator],
309
+ decorators: [ContainerDecorator('2rem')],
274
310
  parameters: {
275
311
  docs: {
276
312
  source: {
@@ -288,19 +324,7 @@ export const Actions: Story = {
288
324
  },
289
325
  },
290
326
  },
291
- argTypes: {
292
- sentiment: { table: { disable: true } },
293
- title: { table: { disable: true } },
294
- description: { table: { disable: true } },
295
- action: { table: { disable: true } },
296
- actionSecondary: { table: { disable: true } },
297
- media: { table: { disable: true } },
298
- expanded: { table: { disable: true } },
299
- onToggle: { table: { disable: true } },
300
- id: { table: { disable: true } },
301
- className: { table: { disable: true } },
302
- 'data-testid': { table: { disable: true } },
303
- },
327
+ argTypes: hiddenArgTypes,
304
328
  render: function Render() {
305
329
  return (
306
330
  <>
@@ -327,7 +351,7 @@ export const Actions: Story = {
327
351
  * Each sentiment has a default status icon. Override it with a custom image or an AvatarView.
328
352
  */
329
353
  export const MediaTypes: Story = {
330
- decorators: [ContainerDecorator],
354
+ decorators: [ContainerDecorator('2rem')],
331
355
  parameters: {
332
356
  docs: {
333
357
  source: {
@@ -351,19 +375,7 @@ export const MediaTypes: Story = {
351
375
  },
352
376
  },
353
377
  },
354
- argTypes: {
355
- sentiment: { table: { disable: true } },
356
- title: { table: { disable: true } },
357
- description: { table: { disable: true } },
358
- action: { table: { disable: true } },
359
- actionSecondary: { table: { disable: true } },
360
- media: { table: { disable: true } },
361
- expanded: { table: { disable: true } },
362
- onToggle: { table: { disable: true } },
363
- id: { table: { disable: true } },
364
- className: { table: { disable: true } },
365
- 'data-testid': { table: { disable: true } },
366
- },
378
+ argTypes: hiddenArgTypes,
367
379
  render: function Render() {
368
380
  const [expandedStates, setExpandedStates] = useState<Record<string, boolean>>({
369
381
  default: true,
@@ -445,6 +457,31 @@ export const MediaTypes: Story = {
445
457
  * When the banner is shown on wider screens, it will always be fully expanded.
446
458
  */
447
459
  export const Expanded: Story = {
460
+ argTypes: hiddenArgTypes,
461
+ render: function Render() {
462
+ const [expanded, setExpanded] = useState(true);
463
+
464
+ return (
465
+ <>
466
+ <div className="p-b-3">
467
+ <Button v2 onClick={() => setExpanded(!expanded)}>
468
+ {expanded ? 'Collapse externally' : 'Expand externally'}
469
+ </Button>
470
+ </div>
471
+ <IframeContainer width={350}>
472
+ <CriticalBanner
473
+ sentiment="negative"
474
+ title="Your account requires verification"
475
+ description="Please verify your identity to continue using all features. This process typically takes 2–3 minutes."
476
+ action={{ label: 'Verify now', onClick: fn() }}
477
+ actionSecondary={{ label: 'Learn more', onClick: fn() }}
478
+ expanded={expanded}
479
+ onToggle={() => setExpanded(!expanded)}
480
+ />
481
+ </IframeContainer>
482
+ </>
483
+ );
484
+ },
448
485
  parameters: {
449
486
  docs: {
450
487
  source: {
@@ -462,41 +499,6 @@ export const Expanded: Story = {
462
499
  },
463
500
  },
464
501
  },
465
- argTypes: {
466
- sentiment: { table: { disable: true } },
467
- title: { table: { disable: true } },
468
- description: { table: { disable: true } },
469
- action: { table: { disable: true } },
470
- actionSecondary: { table: { disable: true } },
471
- media: { table: { disable: true } },
472
- expanded: { table: { disable: true } },
473
- onToggle: { table: { disable: true } },
474
- id: { table: { disable: true } },
475
- className: { table: { disable: true } },
476
- 'data-testid': { table: { disable: true } },
477
- },
478
- render: function Render() {
479
- const [expanded, setExpanded] = useState(true);
480
-
481
- return (
482
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', width: '100%' }}>
483
- <div>
484
- <Button v2 onClick={() => setExpanded(!expanded)}>
485
- {expanded ? 'Collapse externally' : 'Expand externally'}
486
- </Button>
487
- </div>
488
- <CriticalBanner
489
- sentiment="negative"
490
- title="Your account requires verification"
491
- description="Please verify your identity to continue using all features. This process typically takes 2–3 minutes."
492
- action={{ label: 'Verify now', onClick: fn() }}
493
- actionSecondary={{ label: 'Learn more', onClick: fn() }}
494
- expanded={expanded}
495
- onToggle={() => setExpanded(!expanded)}
496
- />
497
- </div>
498
- );
499
- },
500
502
  };
501
503
 
502
504
  /**
@@ -504,27 +506,15 @@ export const Expanded: Story = {
504
506
  * is clamped to 2 lines. With a title, only the title is shown when collapsed.
505
507
  */
506
508
  export const CollapsedBehaviour: Story = {
507
- decorators: [ContainerDecorator],
509
+ decorators: [ContainerDecorator()],
508
510
 
509
- argTypes: {
510
- sentiment: { table: { disable: true } },
511
- title: { table: { disable: true } },
512
- description: { table: { disable: true } },
513
- action: { table: { disable: true } },
514
- actionSecondary: { table: { disable: true } },
515
- media: { table: { disable: true } },
516
- expanded: { table: { disable: true } },
517
- onToggle: { table: { disable: true } },
518
- id: { table: { disable: true } },
519
- className: { table: { disable: true } },
520
- 'data-testid': { table: { disable: true } },
521
- },
511
+ argTypes: hiddenArgTypes,
522
512
  render: function Render() {
523
513
  const [expanded1, setExpanded1] = useState(false);
524
514
  const [expanded2, setExpanded2] = useState(false);
525
515
 
526
516
  return (
527
- <>
517
+ <IframeContainer width={650}>
528
518
  <CriticalBanner
529
519
  sentiment="negative"
530
520
  title="Collapsed with title"
@@ -534,22 +524,21 @@ export const CollapsedBehaviour: Story = {
534
524
  onToggle={() => setExpanded1(!expanded1)}
535
525
  />
536
526
  <CriticalBanner
527
+ className="m-t-3"
537
528
  sentiment="warning"
538
529
  description="When there is no title, the description stays visible but is clamped to a maximum of two lines. This ensures the user always sees some context even in the collapsed state, because there is no title to summarise the message."
539
530
  action={{ label: 'Take action', onClick: fn() }}
540
531
  expanded={expanded2}
541
532
  onToggle={() => setExpanded2(!expanded2)}
542
533
  />
543
- </>
534
+ </IframeContainer>
544
535
  );
545
536
  },
546
- ...withVariantConfig(['mobile'], {
547
- parameters: {
548
- docs: {
549
- canvas: { sourceState: 'hidden' },
550
- },
537
+ parameters: {
538
+ docs: {
539
+ canvas: { sourceState: 'hidden' },
551
540
  },
552
- }),
541
+ },
553
542
  };
554
543
 
555
544
  /**
@@ -563,27 +552,15 @@ export const Responsiveness: Story = {
563
552
  canvas: { sourceState: 'hidden' },
564
553
  },
565
554
  },
566
- argTypes: {
567
- sentiment: { table: { disable: true } },
568
- title: { table: { disable: true } },
569
- description: { table: { disable: true } },
570
- action: { table: { disable: true } },
571
- actionSecondary: { table: { disable: true } },
572
- media: { table: { disable: true } },
573
- expanded: { table: { disable: true } },
574
- onToggle: { table: { disable: true } },
575
- id: { table: { disable: true } },
576
- className: { table: { disable: true } },
577
- 'data-testid': { table: { disable: true } },
578
- },
555
+ argTypes: hiddenArgTypes,
579
556
  render: function Render() {
580
557
  return (
581
- <div style={{ display: 'flex', gap: 16, alignItems: 'start', minWidth: 1500 }}>
582
- {[200, 350, 600].map((width) => (
558
+ <div style={{ display: 'flex', gap: 16, alignItems: 'start', minWidth: 1700 }}>
559
+ {[200, 350, 800].map((width) => (
583
560
  <IframeContainer key={width} width={width}>
584
561
  <CriticalBanner
585
562
  sentiment="warning"
586
- title="Your account requires verification"
563
+ title={`At ${width}px the buttons are ${width === 200 ? 'stacked' : width === 350 ? 'full-width' : 'left-aligned'}`}
587
564
  description="Please verify your identity to continue using all features."
588
565
  action={{ label: 'Verify now', onClick: fn() }}
589
566
  actionSecondary={{ label: 'Learn more', onClick: fn() }}
@@ -595,6 +572,50 @@ export const Responsiveness: Story = {
595
572
  },
596
573
  };
597
574
 
575
+ /**
576
+ * Mobile viewport regression test for the overhang.
577
+ * The overhang is always applied below 600px.
578
+ */
579
+ export const MobileOverhang: Story = {
580
+ argTypes: hiddenArgTypes,
581
+ render: function Render() {
582
+ return (
583
+ <div style={{ width: 350 }}>
584
+ <CriticalBanner
585
+ sentiment="negative"
586
+ title="Your account requires verification"
587
+ description="Please verify your identity to continue using all features."
588
+ action={{ label: 'Verify now', onClick: fn() }}
589
+ />
590
+ </div>
591
+ );
592
+ },
593
+ parameters: {
594
+ docs: {
595
+ canvas: { sourceState: 'hidden' },
596
+ },
597
+ },
598
+ };
599
+
600
+ /**
601
+ * Mobile-only entry animation: the banner reveals from above.
602
+ */
603
+ export const AnimatedEntry: Story = {
604
+ args: {
605
+ title: 'Your account is restricted.',
606
+ description: 'Review your personal details to get your account active again.',
607
+ action: { label: 'Review details', onClick: fn() },
608
+ },
609
+ argTypes: hiddenArgTypes,
610
+ render: (args) => <AnimatedEntryPreview {...args} />,
611
+ parameters: {
612
+ layout: 'fullscreen',
613
+ docs: {
614
+ canvas: { sourceState: 'hidden' },
615
+ },
616
+ },
617
+ };
618
+
598
619
  /**
599
620
  * The text container is capped at 480px for optimal readability, even though
600
621
  * the banner itself stretches to the full available width.
@@ -605,31 +626,21 @@ export const ParagraphWidth: Story = {
605
626
  canvas: { sourceState: 'hidden' },
606
627
  },
607
628
  },
608
- argTypes: {
609
- sentiment: { table: { disable: true } },
610
- title: { table: { disable: true } },
611
- description: { table: { disable: true } },
612
- action: { table: { disable: true } },
613
- actionSecondary: { table: { disable: true } },
614
- media: { table: { disable: true } },
615
- expanded: { table: { disable: true } },
616
- onToggle: { table: { disable: true } },
617
- id: { table: { disable: true } },
618
- className: { table: { disable: true } },
619
- 'data-testid': { table: { disable: true } },
620
- },
629
+ argTypes: hiddenArgTypes,
621
630
  render: function Render() {
622
631
  const [expanded, setExpanded] = useState(true);
623
632
 
624
633
  return (
625
- <CriticalBanner
626
- sentiment="negative"
627
- title="The text container is capped at 480px for optimal readability, even though the banner itself stretches to the full available width."
628
- description="To restore access, you'll need to verify your identity. This involves confirming your personal details and uploading a valid government-issued ID. The process typically takes 2–3 minutes, and your account will be restored immediately after successful verification."
629
- action={{ label: 'Verify now', onClick: fn() }}
630
- expanded={expanded}
631
- onToggle={() => setExpanded(!expanded)}
632
- />
634
+ <IframeContainer width={1000}>
635
+ <CriticalBanner
636
+ sentiment="negative"
637
+ title="The text container is capped at 480px for optimal readability, even though the banner itself stretches to the full available width."
638
+ description="To restore access, you'll need to verify your identity. This involves confirming your personal details and uploading a valid government-issued ID. The process typically takes 2–3 minutes, and your account will be restored immediately after successful verification."
639
+ action={{ label: 'Verify now', onClick: fn() }}
640
+ expanded={expanded}
641
+ onToggle={() => setExpanded(!expanded)}
642
+ />
643
+ </IframeContainer>
633
644
  );
634
645
  },
635
646
  };
@@ -25,7 +25,7 @@ const ANIMATION_DURATION = 200; // 150ms animation + 50ms buffer
25
25
  const longDescription =
26
26
  'We have detected unusual activity on your account that does not match your typical usage patterns. To protect your funds and personal information, we have temporarily restricted access. Please verify your identity to restore full access to your account.';
27
27
 
28
- function AllVariants() {
28
+ function AllVariants({ gap = '1rem' }: { gap?: string }) {
29
29
  const [states, setStates] = useState({
30
30
  negativeExpanded: true,
31
31
  negativeCollapsed: false,
@@ -42,7 +42,7 @@ function AllVariants() {
42
42
  setStates((prev) => ({ ...prev, [key]: !prev[key] }));
43
43
 
44
44
  return (
45
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', width: '100%' }}>
45
+ <div style={{ display: 'flex', flexDirection: 'column', gap, width: '100%' }}>
46
46
  <CriticalBanner
47
47
  sentiment="negative"
48
48
  title="Negative — expanded"
@@ -133,10 +133,29 @@ export const Variants: Story = {
133
133
 
134
134
  /** Mobile viewport regression test with all variants. */
135
135
  export const MobileVariants: Story = {
136
- render: () => <AllVariants />,
136
+ render: () => <AllVariants gap="2rem" />,
137
137
  ...withVariantConfig(['mobile']),
138
138
  };
139
139
 
140
+ /** Mobile entry animation visual regression test. */
141
+ export const AnimatedEntryMobile: Story = {
142
+ render: function Render() {
143
+ return (
144
+ <CriticalBanner
145
+ sentiment="negative"
146
+ title="Your account is restricted."
147
+ description="Review your personal details to get your account active again."
148
+ action={{ label: 'Review details', onClick: fn() }}
149
+ />
150
+ );
151
+ },
152
+ ...withVariantConfig(['mobile'], {
153
+ parameters: {
154
+ chromatic: { pauseAnimationAtEnd: true },
155
+ },
156
+ }),
157
+ };
158
+
140
159
  /**
141
160
  * Tests keyboard navigation and interaction:
142
161
  * - Tab focuses the first chevron toggle button
@@ -150,7 +169,7 @@ export const KeyboardInteraction: Story = {
150
169
  const [expanded2, setExpanded2] = useState(false);
151
170
 
152
171
  return (
153
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', width: '100%' }}>
172
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem', width: '100%' }}>
154
173
  <CriticalBanner
155
174
  sentiment="negative"
156
175
  title="First banner"
@@ -289,7 +308,7 @@ export const RTL: Story = {
289
308
 
290
309
  /** 400% zoom regression test with all variants. */
291
310
  export const Zoom400: Story = {
292
- render: () => <AllVariants />,
311
+ render: () => <AllVariants gap="2rem" />,
293
312
  ...withVariantConfig(['400%']),
294
313
  };
295
314
 
@@ -353,7 +372,7 @@ export const MultiLineTitleAnimation: Story = {
353
372
  export const UncontrolledState: Story = {
354
373
  render: function Render() {
355
374
  return (
356
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', width: '100%' }}>
375
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '2rem', width: '100%' }}>
357
376
  <CriticalBanner
358
377
  sentiment="negative"
359
378
  title="Uncontrolled banner (no onToggle)"
@@ -0,0 +1,37 @@
1
+ /* eslint-disable testing-library/no-container */
2
+ import { mockMatchMedia, mockResizeObserver, render, screen } from '../../test-utils';
3
+ import { resetLiveRegionAnnouncementQueue } from '../../common/liveRegion/LiveRegion';
4
+ import { CriticalBanner, CriticalBannerProps } from './CriticalBanner';
5
+
6
+ mockMatchMedia();
7
+ mockResizeObserver();
8
+
9
+ describe('CriticalBanner', () => {
10
+ const defaultProps: CriticalBannerProps = {
11
+ description: 'Please verify your identity to continue using all features.',
12
+ };
13
+
14
+ beforeEach(() => {
15
+ resetLiveRegionAnnouncementQueue();
16
+ });
17
+
18
+ it('applies mobile overhang classes by default', () => {
19
+ const { container } = render(
20
+ <CriticalBanner {...defaultProps} data-testid="critical-banner" />,
21
+ );
22
+
23
+ expect(screen.getByTestId('critical-banner')).toBeInTheDocument();
24
+ expect(container.querySelector('.wds-critical-banner-overhang')).toBeInTheDocument();
25
+ expect(container.querySelector('.wds-critical-banner-overhang-query')).toBeInTheDocument();
26
+ });
27
+
28
+ it('wraps the banner in the css entry animation structure', () => {
29
+ const { container } = render(
30
+ <CriticalBanner {...defaultProps} data-testid="critical-banner" />,
31
+ );
32
+
33
+ expect(screen.getByTestId('critical-banner')).toBeInTheDocument();
34
+ expect(container.querySelector('.wds-critical-banner__entry-mask')).toBeInTheDocument();
35
+ expect(container.querySelector('.wds-critical-banner__entry-track')).toBeInTheDocument();
36
+ });
37
+ });