@transferwise/components 46.139.0 → 46.140.0

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/src/main.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly, this file was auto-generated.
3
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
3
+ * Generated on Wed, 13 May 2026 12:45:11 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -144,7 +144,7 @@
144
144
 
145
145
  /**
146
146
  * Do not edit directly, this file was auto-generated.
147
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
147
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
148
148
  */
149
149
 
150
150
  .np-theme-personal {
@@ -326,75 +326,9 @@
326
326
  --font-family-display: 'Wise Sans', 'Inter', sans-serif;
327
327
  }
328
328
 
329
- /**
330
- * We added new shape theme into tokens to prevent breaking changes. This is a temporary measure.
331
- *
332
- * We delete this hack once all consumers no longer import tokens in their projects (e.g Next.js app.tsx or Storybook's preivew.tsx)
333
- */
334
-
335
- @media (max-width: 320px) {
336
- .np-theme-personal {
337
- --delta: 2;
338
- --size-4: calc(4px / var(--delta));
339
- --size-5: calc(5px / var(--delta));
340
- --size-8: calc(8px / var(--delta));
341
- --size-10: calc(10px / var(--delta));
342
- --size-12: calc(12px / var(--delta));
343
- --size-14: calc(14px / var(--delta));
344
- --size-16: calc(16px / var(--delta));
345
- --size-24: calc(24px / var(--delta));
346
- --size-32: calc(32px / var(--delta));
347
- --size-40: calc(40px / var(--delta));
348
- --size-48: calc(48px / var(--delta));
349
- --size-52: calc(52px / var(--delta));
350
- --size-56: calc(56px / var(--delta));
351
- --size-60: calc(60px / var(--delta));
352
- --size-64: calc(64px / var(--delta));
353
- --size-72: calc(72px / var(--delta));
354
- --size-80: calc(80px / var(--delta));
355
- --size-88: calc(88px / var(--delta));
356
- --size-96: calc(96px / var(--delta));
357
- --size-104: calc(104px / var(--delta));
358
- --size-112: calc(112px / var(--delta));
359
- --size-120: calc(120px / var(--delta));
360
- --size-126: calc(126px / var(--delta));
361
- --size-128: calc(128px / var(--delta));
362
- --size-146: calc(146px / var(--delta));
363
- --size-154: calc(154px / var(--delta));
364
- --size-x-small: calc(24px / var(--delta));
365
- --size-small: calc(32px / var(--delta));
366
- --size-medium: calc(40px / var(--delta));
367
- --size-large: calc(48px / var(--delta));
368
- --size-x-large: calc(56px / var(--delta));
369
- --size-2x-large: calc(72px / var(--delta));
370
- --space-content-horizontal: calc(16px / var(--delta));
371
- --space-small: calc(16px / var(--delta));
372
- --space-medium: calc(32px / var(--delta));
373
- --space-large: calc(40px / var(--delta));
374
- --space-x-large: calc(56px / var(--delta));
375
- --padding-x-small: var(--size-8);
376
- --padding-small: var(--size-16);
377
- --padding-medium: var(--size-24);
378
- --padding-large: var(--size-32);
379
- --input-height-base: var(--size-32);
380
- --input-height-large: var(--input-height-small);
381
- --input-padding: var(--input-padding-small);
382
- --input-padding-large: var(--input-padding-small);
383
- --input-group-addon-padding: var(--input-group-addon-sm-padding);
384
- --input-group-addon-lg-padding: var(--input-group-addon-sm-padding);
385
- --btn-height: var(--input-height-base);
386
- --btn-lg-height: var(--btn-height);
387
- --btn-sm-height: var(--btn-height);
388
- --btn-padding: var(--input-padding);
389
- --btn-sm-padding: var(--btn-padding);
390
- --btn-lg-padding: var(--btn-padding);
391
- --dropdown-link-padding: var(--size-12) var(--size-16);
392
- }
393
- }
394
-
395
329
  /**
396
330
  * Do not edit directly, this file was auto-generated.
397
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
331
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
398
332
  */
399
333
 
400
334
  .np-theme-personal--forest-green {
@@ -576,75 +510,9 @@
576
510
  --font-family-display: 'Wise Sans', 'Inter', sans-serif;
577
511
  }
578
512
 
579
- /**
580
- * We added new shape theme into tokens to prevent breaking changes. This is a temporary measure.
581
- *
582
- * We delete this hack once all consumers no longer import tokens in their projects (e.g Next.js app.tsx or Storybook's preivew.tsx)
583
- */
584
-
585
- @media (max-width: 320px) {
586
- .np-theme-personal {
587
- --delta: 2;
588
- --size-4: calc(4px / var(--delta));
589
- --size-5: calc(5px / var(--delta));
590
- --size-8: calc(8px / var(--delta));
591
- --size-10: calc(10px / var(--delta));
592
- --size-12: calc(12px / var(--delta));
593
- --size-14: calc(14px / var(--delta));
594
- --size-16: calc(16px / var(--delta));
595
- --size-24: calc(24px / var(--delta));
596
- --size-32: calc(32px / var(--delta));
597
- --size-40: calc(40px / var(--delta));
598
- --size-48: calc(48px / var(--delta));
599
- --size-52: calc(52px / var(--delta));
600
- --size-56: calc(56px / var(--delta));
601
- --size-60: calc(60px / var(--delta));
602
- --size-64: calc(64px / var(--delta));
603
- --size-72: calc(72px / var(--delta));
604
- --size-80: calc(80px / var(--delta));
605
- --size-88: calc(88px / var(--delta));
606
- --size-96: calc(96px / var(--delta));
607
- --size-104: calc(104px / var(--delta));
608
- --size-112: calc(112px / var(--delta));
609
- --size-120: calc(120px / var(--delta));
610
- --size-126: calc(126px / var(--delta));
611
- --size-128: calc(128px / var(--delta));
612
- --size-146: calc(146px / var(--delta));
613
- --size-154: calc(154px / var(--delta));
614
- --size-x-small: calc(24px / var(--delta));
615
- --size-small: calc(32px / var(--delta));
616
- --size-medium: calc(40px / var(--delta));
617
- --size-large: calc(48px / var(--delta));
618
- --size-x-large: calc(56px / var(--delta));
619
- --size-2x-large: calc(72px / var(--delta));
620
- --space-content-horizontal: calc(16px / var(--delta));
621
- --space-small: calc(16px / var(--delta));
622
- --space-medium: calc(32px / var(--delta));
623
- --space-large: calc(40px / var(--delta));
624
- --space-x-large: calc(56px / var(--delta));
625
- --padding-x-small: var(--size-8);
626
- --padding-small: var(--size-16);
627
- --padding-medium: var(--size-24);
628
- --padding-large: var(--size-32);
629
- --input-height-base: var(--size-32);
630
- --input-height-large: var(--input-height-small);
631
- --input-padding: var(--input-padding-small);
632
- --input-padding-large: var(--input-padding-small);
633
- --input-group-addon-padding: var(--input-group-addon-sm-padding);
634
- --input-group-addon-lg-padding: var(--input-group-addon-sm-padding);
635
- --btn-height: var(--input-height-base);
636
- --btn-lg-height: var(--btn-height);
637
- --btn-sm-height: var(--btn-height);
638
- --btn-padding: var(--input-padding);
639
- --btn-sm-padding: var(--btn-padding);
640
- --btn-lg-padding: var(--btn-padding);
641
- --dropdown-link-padding: var(--size-12) var(--size-16);
642
- }
643
- }
644
-
645
513
  /**
646
514
  * Do not edit directly, this file was auto-generated.
647
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
515
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
648
516
  */
649
517
 
650
518
  .np-theme-personal--bright-green {
@@ -826,75 +694,9 @@
826
694
  --font-family-display: 'Wise Sans', 'Inter', sans-serif;
827
695
  }
828
696
 
829
- /**
830
- * We added new shape theme into tokens to prevent breaking changes. This is a temporary measure.
831
- *
832
- * We delete this hack once all consumers no longer import tokens in their projects (e.g Next.js app.tsx or Storybook's preivew.tsx)
833
- */
834
-
835
- @media (max-width: 320px) {
836
- .np-theme-personal {
837
- --delta: 2;
838
- --size-4: calc(4px / var(--delta));
839
- --size-5: calc(5px / var(--delta));
840
- --size-8: calc(8px / var(--delta));
841
- --size-10: calc(10px / var(--delta));
842
- --size-12: calc(12px / var(--delta));
843
- --size-14: calc(14px / var(--delta));
844
- --size-16: calc(16px / var(--delta));
845
- --size-24: calc(24px / var(--delta));
846
- --size-32: calc(32px / var(--delta));
847
- --size-40: calc(40px / var(--delta));
848
- --size-48: calc(48px / var(--delta));
849
- --size-52: calc(52px / var(--delta));
850
- --size-56: calc(56px / var(--delta));
851
- --size-60: calc(60px / var(--delta));
852
- --size-64: calc(64px / var(--delta));
853
- --size-72: calc(72px / var(--delta));
854
- --size-80: calc(80px / var(--delta));
855
- --size-88: calc(88px / var(--delta));
856
- --size-96: calc(96px / var(--delta));
857
- --size-104: calc(104px / var(--delta));
858
- --size-112: calc(112px / var(--delta));
859
- --size-120: calc(120px / var(--delta));
860
- --size-126: calc(126px / var(--delta));
861
- --size-128: calc(128px / var(--delta));
862
- --size-146: calc(146px / var(--delta));
863
- --size-154: calc(154px / var(--delta));
864
- --size-x-small: calc(24px / var(--delta));
865
- --size-small: calc(32px / var(--delta));
866
- --size-medium: calc(40px / var(--delta));
867
- --size-large: calc(48px / var(--delta));
868
- --size-x-large: calc(56px / var(--delta));
869
- --size-2x-large: calc(72px / var(--delta));
870
- --space-content-horizontal: calc(16px / var(--delta));
871
- --space-small: calc(16px / var(--delta));
872
- --space-medium: calc(32px / var(--delta));
873
- --space-large: calc(40px / var(--delta));
874
- --space-x-large: calc(56px / var(--delta));
875
- --padding-x-small: var(--size-8);
876
- --padding-small: var(--size-16);
877
- --padding-medium: var(--size-24);
878
- --padding-large: var(--size-32);
879
- --input-height-base: var(--size-32);
880
- --input-height-large: var(--input-height-small);
881
- --input-padding: var(--input-padding-small);
882
- --input-padding-large: var(--input-padding-small);
883
- --input-group-addon-padding: var(--input-group-addon-sm-padding);
884
- --input-group-addon-lg-padding: var(--input-group-addon-sm-padding);
885
- --btn-height: var(--input-height-base);
886
- --btn-lg-height: var(--btn-height);
887
- --btn-sm-height: var(--btn-height);
888
- --btn-padding: var(--input-padding);
889
- --btn-sm-padding: var(--btn-padding);
890
- --btn-lg-padding: var(--btn-padding);
891
- --dropdown-link-padding: var(--size-12) var(--size-16);
892
- }
893
- }
894
-
895
697
  /**
896
698
  * Do not edit directly, this file was auto-generated.
897
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
699
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
898
700
  */
899
701
 
900
702
  .np-theme-personal--dark {
@@ -1076,75 +878,9 @@
1076
878
  --font-family-display: 'Wise Sans', 'Inter', sans-serif;
1077
879
  }
1078
880
 
1079
- /**
1080
- * We added new shape theme into tokens to prevent breaking changes. This is a temporary measure.
1081
- *
1082
- * We delete this hack once all consumers no longer import tokens in their projects (e.g Next.js app.tsx or Storybook's preivew.tsx)
1083
- */
1084
-
1085
- @media (max-width: 320px) {
1086
- .np-theme-personal {
1087
- --delta: 2;
1088
- --size-4: calc(4px / var(--delta));
1089
- --size-5: calc(5px / var(--delta));
1090
- --size-8: calc(8px / var(--delta));
1091
- --size-10: calc(10px / var(--delta));
1092
- --size-12: calc(12px / var(--delta));
1093
- --size-14: calc(14px / var(--delta));
1094
- --size-16: calc(16px / var(--delta));
1095
- --size-24: calc(24px / var(--delta));
1096
- --size-32: calc(32px / var(--delta));
1097
- --size-40: calc(40px / var(--delta));
1098
- --size-48: calc(48px / var(--delta));
1099
- --size-52: calc(52px / var(--delta));
1100
- --size-56: calc(56px / var(--delta));
1101
- --size-60: calc(60px / var(--delta));
1102
- --size-64: calc(64px / var(--delta));
1103
- --size-72: calc(72px / var(--delta));
1104
- --size-80: calc(80px / var(--delta));
1105
- --size-88: calc(88px / var(--delta));
1106
- --size-96: calc(96px / var(--delta));
1107
- --size-104: calc(104px / var(--delta));
1108
- --size-112: calc(112px / var(--delta));
1109
- --size-120: calc(120px / var(--delta));
1110
- --size-126: calc(126px / var(--delta));
1111
- --size-128: calc(128px / var(--delta));
1112
- --size-146: calc(146px / var(--delta));
1113
- --size-154: calc(154px / var(--delta));
1114
- --size-x-small: calc(24px / var(--delta));
1115
- --size-small: calc(32px / var(--delta));
1116
- --size-medium: calc(40px / var(--delta));
1117
- --size-large: calc(48px / var(--delta));
1118
- --size-x-large: calc(56px / var(--delta));
1119
- --size-2x-large: calc(72px / var(--delta));
1120
- --space-content-horizontal: calc(16px / var(--delta));
1121
- --space-small: calc(16px / var(--delta));
1122
- --space-medium: calc(32px / var(--delta));
1123
- --space-large: calc(40px / var(--delta));
1124
- --space-x-large: calc(56px / var(--delta));
1125
- --padding-x-small: var(--size-8);
1126
- --padding-small: var(--size-16);
1127
- --padding-medium: var(--size-24);
1128
- --padding-large: var(--size-32);
1129
- --input-height-base: var(--size-32);
1130
- --input-height-large: var(--input-height-small);
1131
- --input-padding: var(--input-padding-small);
1132
- --input-padding-large: var(--input-padding-small);
1133
- --input-group-addon-padding: var(--input-group-addon-sm-padding);
1134
- --input-group-addon-lg-padding: var(--input-group-addon-sm-padding);
1135
- --btn-height: var(--input-height-base);
1136
- --btn-lg-height: var(--btn-height);
1137
- --btn-sm-height: var(--btn-height);
1138
- --btn-padding: var(--input-padding);
1139
- --btn-sm-padding: var(--btn-padding);
1140
- --btn-lg-padding: var(--btn-padding);
1141
- --dropdown-link-padding: var(--size-12) var(--size-16);
1142
- }
1143
- }
1144
-
1145
881
  /**
1146
882
  * Do not edit directly, this file was auto-generated.
1147
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
883
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
1148
884
  */
1149
885
 
1150
886
  .np-theme-platform {
@@ -1328,7 +1064,7 @@
1328
1064
 
1329
1065
  /**
1330
1066
  * Do not edit directly, this file was auto-generated.
1331
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
1067
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
1332
1068
  */
1333
1069
 
1334
1070
  .np-theme-platform--forest-green {
@@ -1512,7 +1248,7 @@
1512
1248
 
1513
1249
  /**
1514
1250
  * Do not edit directly, this file was auto-generated.
1515
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
1251
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
1516
1252
  */
1517
1253
 
1518
1254
  .np-theme-business {
@@ -1697,7 +1433,7 @@
1697
1433
 
1698
1434
  /**
1699
1435
  * Do not edit directly, this file was auto-generated.
1700
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
1436
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
1701
1437
  */
1702
1438
 
1703
1439
  .np-theme-business--dark {
@@ -1882,7 +1618,7 @@
1882
1618
 
1883
1619
  /**
1884
1620
  * Do not edit directly, this file was auto-generated.
1885
- * Generated on Fri, 10 Apr 2026 14:46:00 GMT
1621
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
1886
1622
  */
1887
1623
 
1888
1624
  .np-theme-business--forest-green {
@@ -2067,7 +1803,7 @@
2067
1803
 
2068
1804
  /**
2069
1805
  * Do not edit directly, this file was auto-generated.
2070
- * Generated on Fri, 10 Apr 2026 14:46:01 GMT
1806
+ * Generated on Wed, 13 May 2026 12:45:12 GMT
2071
1807
  */
2072
1808
 
2073
1809
  .np-theme-business--bright-green {
@@ -28156,11 +27892,11 @@ a[data-toggle="tooltip"] {
28156
27892
  align-items: stretch;
28157
27893
  background-color: rgba(134,167,189,0.10196);
28158
27894
  background-color: var(--Card-background-color);
28159
- border-radius: calc(32px / 2);
27895
+ border-radius: 32px;
28160
27896
  border-radius: var(--Card-border-radius);
28161
- gap: calc(16px / 2);
27897
+ gap: 16px;
28162
27898
  gap: var(--Card-flex-gap);
28163
- padding: calc(24px / 2);
27899
+ padding: 24px;
28164
27900
  padding: var(--Card-padding);
28165
27901
  position: relative;
28166
27902
  box-sizing: border-box;
@@ -31480,7 +31216,7 @@ html:not([dir="rtl"]) .np-navigation-option {
31480
31216
  border-radius: var(--nudge-border-radius);
31481
31217
  display: flex;
31482
31218
  flex: 1;
31483
- gap: calc(16px / 2);
31219
+ gap: 16px;
31484
31220
  gap: var(--nudge-flex-gap);
31485
31221
  min-height: 106px;
31486
31222
  min-height: var(--nudge-min-height);
@@ -34678,9 +34414,9 @@ html:not([dir="rtl"]) .np-navigation-option {
34678
34414
  --buttonTopRightOffset: var(--size-16);
34679
34415
  --clickAreaTopRightOffset: calc((var(--clickAreaSize) - var(--iconSize)) * -0.5);
34680
34416
  position: absolute;
34681
- right: calc(16px / 2);
34417
+ right: 16px;
34682
34418
  right: var(--buttonTopRightOffset);
34683
- top: calc(16px / 2);
34419
+ top: 16px;
34684
34420
  top: var(--buttonTopRightOffset);
34685
34421
  }
34686
34422
 
@@ -1,5 +1,6 @@
1
1
  import { action } from 'storybook/actions';
2
2
  import { Meta, StoryObj } from '@storybook/react-webpack5';
3
+ import { expect, userEvent, within } from 'storybook/test';
3
4
  import { ScreenMode, ThemeProvider } from '@wise/components-theming';
4
5
  import { useState } from 'react';
5
6
  import { Button, Modal, ModalProps } from '..';
@@ -123,3 +124,63 @@ export const WithThemeProviderInContentMobile: Story = {
123
124
  ),
124
125
  ...withVariantConfig(['mobile']),
125
126
  };
127
+
128
+ const wait = async (duration = 500) =>
129
+ new Promise<void>((resolve) => {
130
+ setTimeout(resolve, duration);
131
+ });
132
+
133
+ /**
134
+ * This test insures that there's no unintended page scroll after the modal
135
+ * is closed without selecting an option (e.g. clicking outside).
136
+ */
137
+ export const NoScrollAfterClose: Story = {
138
+ args: {
139
+ title: 'Modal Title',
140
+ body: lorem100,
141
+ scroll: Scroll.VIEWPORT,
142
+ },
143
+ render: function Render(args) {
144
+ const [open, setOpen] = useState(false);
145
+
146
+ return (
147
+ <div>
148
+ <style>
149
+ {`
150
+ .storybook-container{
151
+ min-height: unset;
152
+ height: unset;
153
+ }
154
+ `}
155
+ </style>
156
+ <p style={{ height: '70vh', background: 'blanchedalmond' }} />
157
+
158
+ <Button v2 onClick={() => setOpen(true)}>
159
+ Open Modal
160
+ </Button>
161
+ <Modal {...args} open={open} onClose={() => setOpen(false)} />
162
+
163
+ <p style={{ margin: '1rem 0', height: '150vh', background: 'blanchedalmond' }} />
164
+ </div>
165
+ );
166
+ },
167
+ play: async ({ canvasElement, step }) => {
168
+ const canvas = within(canvasElement);
169
+ const trigger = canvas.getByRole('button', { name: /Open Modal/i });
170
+
171
+ await step('Open the modal', async () => {
172
+ await userEvent.click(trigger);
173
+ await wait();
174
+ });
175
+
176
+ await step('Close the modal with Escape', async () => {
177
+ await userEvent.keyboard('{Escape}');
178
+ await wait();
179
+ });
180
+
181
+ await step('Verify the page has not scrolled', async () => {
182
+ const scrollParent = canvasElement.ownerDocument.documentElement;
183
+ await expect(scrollParent.scrollTop).toBe(0);
184
+ });
185
+ },
186
+ };
@@ -71,3 +71,48 @@ export const WithoutVisibleTitle: Story = {
71
71
  ),
72
72
  },
73
73
  } satisfies Meta<typeof Popover>;
74
+
75
+ /**
76
+ * The `preferredPlacement` prop indicates where the Popover should appear
77
+ * relative to its trigger element. However, it is a preference, not a guarantee.
78
+ *
79
+ * If there isn't enough viewport space in the preferred direction, the Popover
80
+ * automatically flips to an alternative placement. The fallback order is:
81
+ * - `top` → tries `bottom`, then `right`, then `left`
82
+ * - `bottom` → tries `top`, then `right`, then `left`
83
+ * - `left` → tries `right`, then `top`, then `bottom`
84
+ * - `right` → tries `left`, then `top`, then `bottom`
85
+ *
86
+ * For example, if you set `preferredPlacement="top"` but the trigger is near
87
+ * the top of the viewport, the Popover will flip to `bottom` instead.
88
+ *
89
+ * Try scrolling this story so triggers are near viewport edges to see the
90
+ * flip behaviour in action.
91
+ */
92
+ export const PreferredPlacement: Story = {
93
+ render: () => (
94
+ <div
95
+ style={{
96
+ display: 'flex',
97
+ flexWrap: 'wrap',
98
+ gap: '48px',
99
+ justifyContent: 'center',
100
+ alignItems: 'center',
101
+ padding: '120px 80px',
102
+ }}
103
+ >
104
+ {([Position.TOP, Position.RIGHT, Position.BOTTOM, Position.LEFT] as const).map(
105
+ (placement) => (
106
+ <Popover
107
+ key={placement}
108
+ preferredPlacement={placement}
109
+ title="Guaranteed rate"
110
+ content={<Content />}
111
+ >
112
+ <Button v2>{placement}</Button>
113
+ </Popover>
114
+ ),
115
+ )}
116
+ </div>
117
+ ),
118
+ };
@@ -0,0 +1,124 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { expect, userEvent, within } from 'storybook/test';
3
+
4
+ import { withVariantConfig } from '../../.storybook/helpers';
5
+ import { Button } from '..';
6
+ import { Position } from '../common';
7
+
8
+ import Popover, { PopoverPreferredPlacement } from './Popover';
9
+
10
+ const meta = {
11
+ title: 'Dialogs/Popover/Tests',
12
+ component: Popover,
13
+ tags: ['!autodocs', '!manifest'],
14
+ } satisfies Meta<typeof Popover>;
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof Popover>;
18
+
19
+ const wait = async (duration = 500) =>
20
+ new Promise<void>((resolve) => {
21
+ setTimeout(resolve, duration);
22
+ });
23
+
24
+ const createPlacementStory = (
25
+ placement: PopoverPreferredPlacement,
26
+ containerStyle?: React.CSSProperties,
27
+ ): Story => ({
28
+ parameters: {
29
+ chromatic: {
30
+ delay: 1000,
31
+ },
32
+ },
33
+ render: () => (
34
+ <div
35
+ style={
36
+ containerStyle
37
+ ? { position: 'absolute', ...containerStyle }
38
+ : { display: 'flex', justifyContent: 'center', padding: '150px' }
39
+ }
40
+ >
41
+ <Popover preferredPlacement={placement} title="Info" content="Details">
42
+ <Button v2>{placement}</Button>
43
+ </Popover>
44
+ </div>
45
+ ),
46
+ play: async ({ canvasElement }) => {
47
+ const canvas = within(canvasElement);
48
+ await userEvent.click(canvas.getByRole('button'));
49
+ },
50
+ });
51
+
52
+ /**
53
+ * This test insures that there's no unintended page scroll after the popover
54
+ * is closed without selecting an option (e.g. clicking outside).
55
+ * It tests 2 scenarios:
56
+ * 1. when the popover fits naturally on the screen below the trigger
57
+ * 2. when there's not enough space below the trigger and it renders above it
58
+ */
59
+ const generateNoScrollTest = (inverted?: boolean): Story => ({
60
+ render: function Render() {
61
+ return (
62
+ <div>
63
+ <style>
64
+ {`
65
+ .storybook-container{
66
+ min-height: unset;
67
+ height: unset;
68
+ }
69
+ `}
70
+ </style>
71
+ {inverted ? <p style={{ height: '70vh', background: 'blanchedalmond' }} /> : null}
72
+
73
+ <Popover
74
+ content="Popover content for testing"
75
+ title="Popover Title"
76
+ preferredPlacement="top"
77
+ >
78
+ <Button v2>Open Popover</Button>
79
+ </Popover>
80
+
81
+ <p style={{ margin: '1rem 0', height: '150vh', background: 'blanchedalmond' }} />
82
+ </div>
83
+ );
84
+ },
85
+ play: async ({ canvasElement, step }) => {
86
+ const canvas = within(canvasElement);
87
+ const trigger = canvas.getByRole('button', { name: /Open Popover/i });
88
+
89
+ await step('Open the popover', async () => {
90
+ await userEvent.click(trigger);
91
+ await wait();
92
+ });
93
+
94
+ await step('Close the popover with Escape', async () => {
95
+ await userEvent.keyboard('{Escape}');
96
+ await wait();
97
+ });
98
+
99
+ await step('Verify the page has not scrolled', async () => {
100
+ const scrollParent = canvasElement.ownerDocument.documentElement;
101
+ await expect(scrollParent.scrollTop).toBe(0);
102
+ });
103
+ },
104
+ });
105
+
106
+ export const PlacementTop: Story = createPlacementStory(Position.TOP);
107
+ export const PlacementRight: Story = createPlacementStory(Position.RIGHT);
108
+ export const PlacementBottom: Story = createPlacementStory(Position.BOTTOM);
109
+ export const PlacementLeft: Story = createPlacementStory(Position.LEFT);
110
+
111
+ export const FlipTop: Story = createPlacementStory(Position.TOP, { top: 0, left: '50%' });
112
+ export const FlipRight: Story = createPlacementStory(Position.RIGHT, { top: '50%', right: 0 });
113
+ export const FlipBottom: Story = createPlacementStory(Position.BOTTOM, { bottom: 0, left: '50%' });
114
+ export const FlipLeft: Story = createPlacementStory(Position.LEFT, { top: '50%', left: 0 });
115
+
116
+ export const NoScrollAfterCloseTop: Story = {
117
+ ...generateNoScrollTest(),
118
+ ...withVariantConfig(['default']),
119
+ };
120
+
121
+ export const NoScrollAfterCloseBottom: Story = {
122
+ ...generateNoScrollTest(true),
123
+ ...withVariantConfig(['default']),
124
+ };