@transferwise/components 46.160.0 → 46.160.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 (121) hide show
  1. package/build/ExpressiveMoneyInput/Chevron/Chevron.js +10 -2
  2. package/build/ExpressiveMoneyInput/Chevron/Chevron.js.map +1 -1
  3. package/build/ExpressiveMoneyInput/Chevron/Chevron.mjs +11 -3
  4. package/build/ExpressiveMoneyInput/Chevron/Chevron.mjs.map +1 -1
  5. package/build/ProcessIndicator/ProcessIndicator.js +1 -1
  6. package/build/ProcessIndicator/ProcessIndicator.js.map +1 -1
  7. package/build/ProcessIndicator/ProcessIndicator.mjs +1 -1
  8. package/build/ProcessIndicator/ProcessIndicator.mjs.map +1 -1
  9. package/build/Prompt/CriticalBanner/CriticalBanner.js +10 -1
  10. package/build/Prompt/CriticalBanner/CriticalBanner.js.map +1 -1
  11. package/build/Prompt/CriticalBanner/CriticalBanner.mjs +10 -1
  12. package/build/Prompt/CriticalBanner/CriticalBanner.mjs.map +1 -1
  13. package/build/common/Panel/Panel.js +13 -25
  14. package/build/common/Panel/Panel.js.map +1 -1
  15. package/build/common/Panel/Panel.mjs +10 -22
  16. package/build/common/Panel/Panel.mjs.map +1 -1
  17. package/build/common/Panel/Panel.positionUtils.js +68 -0
  18. package/build/common/Panel/Panel.positionUtils.js.map +1 -0
  19. package/build/common/Panel/Panel.positionUtils.mjs +63 -0
  20. package/build/common/Panel/Panel.positionUtils.mjs.map +1 -0
  21. package/build/common/PropsValues/position.js +0 -25
  22. package/build/common/PropsValues/position.js.map +1 -1
  23. package/build/common/PropsValues/position.mjs +1 -24
  24. package/build/common/PropsValues/position.mjs.map +1 -1
  25. package/build/main.css +102 -68
  26. package/build/styles/AvatarView/AvatarView.css +8 -0
  27. package/build/styles/AvatarView/Dot/Dot.css +8 -0
  28. package/build/styles/CheckboxButton/CheckboxButton.css +3 -0
  29. package/build/styles/Nudge/Nudge.css +4 -38
  30. package/build/styles/Prompt/CriticalBanner/CriticalBanner.css +34 -0
  31. package/build/styles/SlidingPanel/SlidingPanel.css +8 -0
  32. package/build/styles/UploadInput/UploadItem/UploadItem.css +4 -5
  33. package/build/styles/_deprecated/Badge/Badge.css +4 -2
  34. package/build/styles/common/Panel/Panel.css +12 -4
  35. package/build/styles/css/accordion.css +3 -0
  36. package/build/styles/css/buttons.css +1 -7
  37. package/build/styles/css/chevron.css +3 -0
  38. package/build/styles/css/neptune.css +25 -19
  39. package/build/styles/css/process.css +15 -9
  40. package/build/styles/css/tick.css +6 -3
  41. package/build/styles/main.css +102 -68
  42. package/build/styles/styles/less/accordion.css +3 -0
  43. package/build/styles/styles/less/buttons.css +1 -7
  44. package/build/styles/styles/less/chevron.css +3 -0
  45. package/build/styles/styles/less/neptune.css +25 -19
  46. package/build/styles/styles/less/process.css +15 -9
  47. package/build/styles/styles/less/tick.css +6 -3
  48. package/build/types/ExpressiveMoneyInput/Chevron/Chevron.d.ts.map +1 -1
  49. package/build/types/Prompt/CriticalBanner/CriticalBanner.d.ts +6 -1
  50. package/build/types/Prompt/CriticalBanner/CriticalBanner.d.ts.map +1 -1
  51. package/build/types/common/Panel/Panel.d.ts +1 -1
  52. package/build/types/common/Panel/Panel.d.ts.map +1 -1
  53. package/build/types/common/Panel/Panel.positionUtils.d.ts +7 -0
  54. package/build/types/common/Panel/Panel.positionUtils.d.ts.map +1 -0
  55. package/build/types/common/PropsValues/position.d.ts +0 -13
  56. package/build/types/common/PropsValues/position.d.ts.map +1 -1
  57. package/build/types/common/ResponsivePanel/ResponsivePanel.d.ts +1 -1
  58. package/package.json +5 -5
  59. package/src/AvatarView/AvatarView.css +8 -0
  60. package/src/AvatarView/Dot/Dot.css +8 -0
  61. package/src/AvatarView/Dot/Dot.less +7 -1
  62. package/src/Button/_stories/Button.test.story.tsx +1 -1
  63. package/src/Checkbox/_stories/Checkbox.story.tsx +1 -1
  64. package/src/Checkbox/_stories/Checkbox.test.story.tsx +3 -2
  65. package/src/CheckboxButton/CheckboxButton.css +3 -0
  66. package/src/CheckboxButton/CheckboxButton.less +4 -0
  67. package/src/Chevron/_stories/Chevron.test.story.tsx +2 -2
  68. package/src/ExpressiveMoneyInput/Chevron/Chevron.tsx +8 -3
  69. package/src/Header/_stories/Header.test.story.tsx +1 -1
  70. package/src/Info/_stories/Info.rtl.test.story.tsx +35 -10
  71. package/src/Info/_stories/Info.story.tsx +1 -1
  72. package/src/Inputs/SelectInput/_stories/SelectInput.test.story.tsx +1 -1
  73. package/src/ListItem/_stories/subcomponents.tsx +1 -1
  74. package/src/Loader/_stories/Loader.test.story.tsx +23 -0
  75. package/src/MoneyInput/_stories/MoneyInput.rtl.test.story.tsx +1 -1
  76. package/src/Nudge/Nudge.css +4 -38
  77. package/src/Nudge/Nudge.less +4 -4
  78. package/src/Nudge/_stories/Nudge.test.story.tsx +29 -12
  79. package/src/PhoneNumberInput/_stories/{PhoneNumberInput.test.story.tsx → PhoneNumberInput.rtl.test.story.tsx} +18 -10
  80. package/src/ProcessIndicator/ProcessIndicator.tsx +1 -1
  81. package/src/ProcessIndicator/_stories/ProcessIndicator.test.story.tsx +32 -0
  82. package/src/ProgressBar/_stories/ProgressBar.test.story.tsx +33 -19
  83. package/src/Prompt/CriticalBanner/CriticalBanner.css +34 -0
  84. package/src/Prompt/CriticalBanner/CriticalBanner.less +45 -0
  85. package/src/Prompt/CriticalBanner/CriticalBanner.test.tsx +26 -3
  86. package/src/Prompt/CriticalBanner/CriticalBanner.tsx +15 -1
  87. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.accessibility.docs.mdx +4 -1
  88. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.story.tsx +52 -0
  89. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.test.story.tsx +22 -1
  90. package/src/SlidingPanel/SlidingPanel.css +8 -0
  91. package/src/SlidingPanel/SlidingPanel.less +8 -0
  92. package/src/SlidingPanel/_stories/SlidingPanel.rtl.test.story.tsx +4 -3
  93. package/src/Snackbar/_stories/Snackbar.rtl.test.story.tsx +14 -16
  94. package/src/Snackbar/_stories/Snackbar.story.tsx +1 -1
  95. package/src/Snackbar/_stories/Snackbar.test.story.tsx +1 -1
  96. package/src/StatusIcon/_stories/StatusIcon.test.story.tsx +12 -4
  97. package/src/UploadInput/UploadItem/UploadItem.css +4 -5
  98. package/src/UploadInput/UploadItem/UploadItem.less +7 -6
  99. package/src/UploadInput/_stories/UploadInput.test.story.tsx +1 -1
  100. package/src/_deprecated/Badge/Badge.css +4 -2
  101. package/src/_deprecated/Badge/Badge.less +7 -14
  102. package/src/_deprecated/CriticalBanner/_stories/CriticalCommsBanner.test.story.tsx +0 -16
  103. package/src/_deprecated/OverlayHeader/_stories/OverlayHeader.test.story.tsx +15 -0
  104. package/src/common/Panel/Panel.css +12 -4
  105. package/src/common/Panel/Panel.less +27 -9
  106. package/src/common/Panel/Panel.positionUtils.ts +79 -0
  107. package/src/common/Panel/Panel.test.tsx +40 -0
  108. package/src/common/Panel/Panel.tsx +13 -24
  109. package/src/common/Panel/_stories/Panel.rtl.test.story.tsx +110 -0
  110. package/src/common/PropsValues/position.ts +0 -50
  111. package/src/main.css +102 -68
  112. package/src/styles/less/accordion.css +3 -0
  113. package/src/styles/less/buttons.css +1 -7
  114. package/src/styles/less/buttons.less +3 -11
  115. package/src/styles/less/chevron.css +3 -0
  116. package/src/styles/less/chevron.less +4 -0
  117. package/src/styles/less/neptune.css +25 -19
  118. package/src/styles/less/process.css +15 -9
  119. package/src/styles/less/process.less +13 -6
  120. package/src/styles/less/tick.css +6 -3
  121. package/src/styles/less/tick.less +7 -3
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { userEvent, within } from 'storybook/test';
2
3
 
3
4
  import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
4
5
  import Field from '../../Field/Field';
@@ -6,21 +7,13 @@ import PhoneNumberInput from '..';
6
7
 
7
8
  export default {
8
9
  component: PhoneNumberInput,
9
- title: 'Forms/PhoneNumberInput/Tests',
10
+ title: 'Forms/PhoneNumberInput/Tests/RTL Diff',
10
11
  tags: ['!autodocs', '!manifest'],
11
12
  } satisfies Meta<typeof PhoneNumberInput>;
12
13
 
13
14
  type Story = StoryObj<typeof PhoneNumberInput>;
14
15
 
15
- /**
16
- * The country code selector sits on the inline-start side of the phone number input.
17
- * In RTL the two elements should swap sides — the selector moves to the right and
18
- * the text field moves to the left. This diff verifies the flex row reverses correctly.
19
- *
20
- * Size variants are included because the size prop changes the input-group class and
21
- * may affect spacing between the two sub-inputs.
22
- */
23
- export const RTLDiff: Story = {
16
+ export const Sizes: Story = {
24
17
  render: () => (
25
18
  <>
26
19
  <Field label="Default (md)">
@@ -42,3 +35,18 @@ export const RTLDiff: Story = {
42
35
  ),
43
36
  ...withRTLDiff(),
44
37
  };
38
+
39
+ export const DropdownOpen: Story = {
40
+ render: () => (
41
+ <Field label="Phone number">
42
+ <PhoneNumberInput onChange={() => {}} />
43
+ </Field>
44
+ ),
45
+ play: async ({ canvasElement }) => {
46
+ const canvas = within(canvasElement);
47
+ const trigger = canvas.queryByRole('combobox');
48
+ if (!trigger) return;
49
+ await userEvent.click(trigger);
50
+ },
51
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'center' }),
52
+ };
@@ -3,7 +3,7 @@ import { Component } from 'react';
3
3
 
4
4
  import { Status, Size } from '../common';
5
5
 
6
- const radius = { xxs: 6, xs: 11, sm: 22, xl: 61 };
6
+ const radius = { xxs: 5, xs: 11, sm: 22, xl: 61 };
7
7
 
8
8
  const ANIMATION_DURATION_IN_MS = 1500;
9
9
 
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+
3
+ import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
4
+ import ProcessIndicator from '..';
5
+
6
+ export default {
7
+ component: ProcessIndicator,
8
+ title: 'Loading/ProcessIndicator/Tests',
9
+ tags: ['!autodocs', '!manifest'],
10
+ } satisfies Meta<typeof ProcessIndicator>;
11
+
12
+ type Story = StoryObj<typeof ProcessIndicator>;
13
+
14
+ const sizes = ['xxs', 'xs', 'sm', 'xl'] as const;
15
+ const statuses = ['succeeded', 'failed'] as const;
16
+
17
+ const rowStyle = { display: 'flex', alignItems: 'center', gap: '16px' } as const;
18
+
19
+ export const RTLDiff: Story = {
20
+ render: () => (
21
+ <>
22
+ {statuses.map((status) => (
23
+ <div key={status} style={rowStyle}>
24
+ {sizes.map((size) => (
25
+ <ProcessIndicator key={size} size={size} status={status} />
26
+ ))}
27
+ </div>
28
+ ))}
29
+ </>
30
+ ),
31
+ ...withRTLDiff(),
32
+ };
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
 
3
- import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
3
+ import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
4
4
  import ProgressBar from '..';
5
5
 
6
6
  export default {
@@ -12,22 +12,36 @@ export default {
12
12
  type Story = StoryObj<typeof ProgressBar>;
13
13
 
14
14
  export const RTLDiff: Story = {
15
- render: () => (
16
- <>
17
- <ProgressBar
18
- id="progress-bar-with-description"
19
- title="Send 5,550 GBP"
20
- description="You have 3 days to reach your goal."
21
- progress={{ value: 50, max: 100 }}
22
- textEnd="200 GBP to go"
23
- />
24
- <ProgressBar
25
- id="progress-bar-without-description"
26
- title="Total limit"
27
- progress={{ value: 800, max: 1000 }}
28
- textEnd="200 GBP remaining"
29
- />
30
- </>
31
- ),
32
- ...withRTLDiff(),
15
+ render: () => {
16
+ const i18n = useRtlDiffI18n();
17
+ return (
18
+ <>
19
+ <ProgressBar
20
+ id="progress-bar-with-description"
21
+ title={i18n.title1}
22
+ description={i18n.description}
23
+ progress={{ value: 50, max: 100 }}
24
+ textEnd={i18n.textEnd1}
25
+ />
26
+ <ProgressBar
27
+ id="progress-bar-without-description"
28
+ title={i18n.title2}
29
+ progress={{ value: 800, max: 1000 }}
30
+ textEnd={i18n.textEnd2}
31
+ />
32
+ </>
33
+ );
34
+ },
35
+ ...withRTLDiff({
36
+ i18n: {
37
+ title1: { ltr: 'Send 5,550 GBP', rtl: 'أرسل ٥٬٥٥٠ جنيه إسترليني' },
38
+ description: {
39
+ ltr: 'You have 3 days to reach your goal.',
40
+ rtl: 'لديك ٣ أيام للوصول إلى هدفك.',
41
+ },
42
+ textEnd1: { ltr: '200 GBP to go', rtl: '٢٠٠ جنيه إسترليني متبقي' },
43
+ title2: { ltr: 'Total limit', rtl: 'الحد الإجمالي' },
44
+ textEnd2: { ltr: '200 GBP remaining', rtl: '٢٠٠ جنيه إسترليني متبقي' },
45
+ },
46
+ }),
33
47
  };
@@ -155,6 +155,35 @@
155
155
  mask: var(--critical-banner-overhang-mask);
156
156
  }
157
157
  }
158
+ .wds-critical-banner__entry-mask {
159
+ display: grid;
160
+ grid-template-rows: 1fr;
161
+ overflow: visible;
162
+ }
163
+ .wds-critical-banner__entry-mask--animated {
164
+ grid-template-rows: 0fr;
165
+ overflow: hidden;
166
+ animation: wds-critical-banner-reveal-height 400ms cubic-bezier(0.3, 0, 0.1, 1) 500ms both, wds-critical-banner-release-overflow 1ms linear 899ms both;
167
+ }
168
+ .wds-critical-banner__entry-track {
169
+ min-block-size: 0;
170
+ }
171
+ @keyframes wds-critical-banner-reveal-height {
172
+ from {
173
+ grid-template-rows: 0fr;
174
+ }
175
+ to {
176
+ grid-template-rows: 1fr;
177
+ }
178
+ }
179
+ @keyframes wds-critical-banner-release-overflow {
180
+ from {
181
+ overflow: hidden;
182
+ }
183
+ to {
184
+ overflow: visible;
185
+ }
186
+ }
158
187
  @media (prefers-reduced-motion: reduce) {
159
188
  .wds-critical-banner__description,
160
189
  .wds-critical-banner__title,
@@ -162,4 +191,9 @@
162
191
  .wds-critical-banner__toggle {
163
192
  transition: none !important;
164
193
  }
194
+ .wds-critical-banner__entry-mask--animated {
195
+ animation: none;
196
+ grid-template-rows: 1fr;
197
+ overflow: visible;
198
+ }
165
199
  }
@@ -193,6 +193,45 @@
193
193
  }
194
194
  }
195
195
 
196
+ .wds-critical-banner__entry-mask {
197
+ display: grid;
198
+ grid-template-rows: 1fr;
199
+ overflow: visible;
200
+
201
+ // Clip only during the reveal, then release overflow so actions remain visible.
202
+ &--animated {
203
+ grid-template-rows: 0fr;
204
+ overflow: hidden;
205
+ animation:
206
+ wds-critical-banner-reveal-height 400ms cubic-bezier(0.3, 0, 0.1, 1) 500ms both,
207
+ wds-critical-banner-release-overflow 1ms linear 899ms both;
208
+ }
209
+ }
210
+
211
+ .wds-critical-banner__entry-track {
212
+ min-height: 0;
213
+ }
214
+
215
+ @keyframes wds-critical-banner-reveal-height {
216
+ from {
217
+ grid-template-rows: 0fr;
218
+ }
219
+
220
+ to {
221
+ grid-template-rows: 1fr;
222
+ }
223
+ }
224
+
225
+ @keyframes wds-critical-banner-release-overflow {
226
+ from {
227
+ overflow: hidden;
228
+ }
229
+
230
+ to {
231
+ overflow: visible;
232
+ }
233
+ }
234
+
196
235
  // Reduced motion support
197
236
  @media (prefers-reduced-motion: reduce) {
198
237
  .wds-critical-banner__description,
@@ -201,4 +240,10 @@
201
240
  .wds-critical-banner__toggle {
202
241
  transition: none !important;
203
242
  }
243
+
244
+ .wds-critical-banner__entry-mask--animated {
245
+ animation: none;
246
+ grid-template-rows: 1fr;
247
+ overflow: visible;
248
+ }
204
249
  }
@@ -15,7 +15,7 @@ describe('CriticalBanner', () => {
15
15
  resetLiveRegionAnnouncementQueue();
16
16
  });
17
17
 
18
- it('renders mobile overhang markup without entry animation markup', () => {
18
+ it('applies mobile overhang classes by default', () => {
19
19
  const { container } = render(
20
20
  <CriticalBanner {...defaultProps} data-testid="critical-banner" />,
21
21
  );
@@ -23,7 +23,30 @@ describe('CriticalBanner', () => {
23
23
  expect(screen.getByTestId('critical-banner')).toBeInTheDocument();
24
24
  expect(container.querySelector('.wds-critical-banner-overhang')).toBeInTheDocument();
25
25
  expect(container.querySelector('.wds-critical-banner-overhang-query')).toBeInTheDocument();
26
- expect(container.querySelector('.wds-critical-banner__entry-mask')).not.toBeInTheDocument();
27
- expect(container.querySelector('.wds-critical-banner__entry-track')).not.toBeInTheDocument();
26
+ });
27
+
28
+ it('applies the entry animation by default', () => {
29
+ const { container } = render(
30
+ <CriticalBanner {...defaultProps} data-testid="critical-banner" />,
31
+ );
32
+
33
+ expect(screen.getByTestId('critical-banner')).toBeInTheDocument();
34
+ expect(
35
+ container.querySelector('.wds-critical-banner__entry-mask--animated'),
36
+ ).toBeInTheDocument();
37
+ expect(container.querySelector('.wds-critical-banner__entry-track')).toBeInTheDocument();
38
+ });
39
+
40
+ it('can render without the entry animation', () => {
41
+ const { container } = render(
42
+ <CriticalBanner {...defaultProps} animateEntry={false} data-testid="critical-banner" />,
43
+ );
44
+
45
+ expect(screen.getByTestId('critical-banner')).toBeInTheDocument();
46
+ expect(container.querySelector('.wds-critical-banner__entry-mask')).toBeInTheDocument();
47
+ expect(
48
+ container.querySelector('.wds-critical-banner__entry-mask--animated'),
49
+ ).not.toBeInTheDocument();
50
+ expect(container.querySelector('.wds-critical-banner__entry-track')).toBeInTheDocument();
28
51
  });
29
52
  });
@@ -24,6 +24,11 @@ export type CriticalBannerProps = {
24
24
  label: ButtonProps['children'];
25
25
  };
26
26
  sentiment?: Exclude<PrimitivePromptProps['sentiment'], 'proposition'>;
27
+ /**
28
+ * Controls whether the banner plays its entry animation 500ms after mounting.
29
+ * Expand and collapse animations are unaffected.
30
+ */
31
+ animateEntry?: boolean;
27
32
  /**
28
33
  * Controls whether the description and actions are visible, as a controlled component.
29
34
  * When collapsed with a title, only the title is shown.
@@ -53,6 +58,7 @@ export const CriticalBanner = ({
53
58
  media = {},
54
59
  action,
55
60
  actionSecondary,
61
+ animateEntry = true,
56
62
  expanded: expandedProp,
57
63
  onToggle,
58
64
  id,
@@ -179,7 +185,15 @@ export const CriticalBanner = ({
179
185
  actionSecondaryLabel: actionSecondary?.label,
180
186
  })}
181
187
  >
182
- <div className="wds-critical-banner-overhang-query">{bannerSurface}</div>
188
+ <div className="wds-critical-banner-overhang-query">
189
+ <div
190
+ className={clsx('wds-critical-banner__entry-mask', {
191
+ 'wds-critical-banner__entry-mask--animated': animateEntry,
192
+ })}
193
+ >
194
+ <div className="wds-critical-banner__entry-track">{bannerSurface}</div>
195
+ </div>
196
+ </div>
183
197
  </LiveRegion>
184
198
  );
185
199
  };
@@ -66,7 +66,10 @@ Screen readers announce the banner content whenever:
66
66
 
67
67
  ## Reduced Motion
68
68
 
69
- `CriticalBanner` respects the user's reduced motion preference. When reduced motion is on, the component does not show the collapse and expand animation used when the user toggles the banner content.
69
+ `CriticalBanner` respects the user's reduced motion preference. When reduced motion is on, the component does not show:
70
+
71
+ - the entry animation that reveals the banner from above when it first appears
72
+ - the collapse and expand animation used when the user toggles the banner content
70
73
 
71
74
  The same content is still rendered and announced. Reduced motion only removes the visual movement.
72
75
 
@@ -28,6 +28,7 @@ const meta = {
28
28
  action: { label: 'Verify now', onClick: fn() },
29
29
  actionSecondary: undefined,
30
30
  media: undefined,
31
+ animateEntry: true,
31
32
  id: undefined,
32
33
  className: undefined,
33
34
  'data-testid': undefined,
@@ -47,6 +48,7 @@ const meta = {
47
48
  table: { type: { summary: 'ReactNode' } },
48
49
  },
49
50
  expanded: { control: 'boolean' },
51
+ animateEntry: { control: 'boolean' },
50
52
  media: {
51
53
  control: false,
52
54
  description: 'Replaces the default Status Icon with a custom image or AvatarView.',
@@ -131,12 +133,28 @@ const hiddenArgTypes = {
131
133
  actionSecondary: { table: { disable: true } },
132
134
  media: { table: { disable: true } },
133
135
  expanded: { table: { disable: true } },
136
+ animateEntry: { table: { disable: true } },
134
137
  onToggle: { table: { disable: true } },
135
138
  id: { table: { disable: true } },
136
139
  className: { table: { disable: true } },
137
140
  'data-testid': { table: { disable: true } },
138
141
  } as const;
139
142
 
143
+ const AnimatedEntryPreview = (args: CriticalBannerProps) => {
144
+ const [bannerInstance, setBannerInstance] = useState(0);
145
+
146
+ return (
147
+ <>
148
+ <CriticalBanner key={bannerInstance} {...args} />
149
+ <div className="p-t-3">
150
+ <Button block v2 onClick={() => setBannerInstance((current) => current + 1)}>
151
+ Replay
152
+ </Button>
153
+ </div>
154
+ </>
155
+ );
156
+ };
157
+
140
158
  /** Renders children inside an iframe that inherits the parent page's stylesheets. */
141
159
  function IframeContainer({ width, children }: { width: number; children: ReactNode }) {
142
160
  const iframeRef = useRef<HTMLIFrameElement>(null);
@@ -585,6 +603,40 @@ export const MobileOverhang: Story = {
585
603
  },
586
604
  };
587
605
 
606
+ /** The banner waits 500ms, then reveals from above when it enters the page. */
607
+ export const AnimatedEntry: Story = {
608
+ args: {
609
+ title: 'Your account is restricted.',
610
+ description: 'Review your personal details to get your account active again.',
611
+ action: { label: 'Review details', onClick: fn() },
612
+ },
613
+ argTypes: hiddenArgTypes,
614
+ render: (args) => <AnimatedEntryPreview {...args} />,
615
+ parameters: {
616
+ layout: 'fullscreen',
617
+ docs: {
618
+ canvas: { sourceState: 'hidden' },
619
+ },
620
+ },
621
+ };
622
+
623
+ /** The banner can mount immediately without replaying its entry animation. */
624
+ export const WithoutEntryAnimation: Story = {
625
+ args: {
626
+ animateEntry: false,
627
+ title: 'Your account is restricted.',
628
+ description: 'Review your personal details to get your account active again.',
629
+ action: { label: 'Review details', onClick: fn() },
630
+ },
631
+ argTypes: hiddenArgTypes,
632
+ parameters: {
633
+ layout: 'fullscreen',
634
+ docs: {
635
+ canvas: { sourceState: 'hidden' },
636
+ },
637
+ },
638
+ };
639
+
588
640
  /**
589
641
  * The text container is capped at 480px for optimal readability, even though
590
642
  * the banner itself stretches to the full available width.
@@ -22,6 +22,7 @@ const wait = async (ms: number) =>
22
22
  });
23
23
 
24
24
  const ANIMATION_DURATION = 200; // 150ms animation + 50ms buffer
25
+ const ENTRY_ANIMATION_DURATION = 950; // 500ms delay + 400ms animation + 50ms buffer
25
26
 
26
27
  const longDescription =
27
28
  '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.';
@@ -151,7 +152,7 @@ export const MobileVariants: Story = {
151
152
  ...withVariantConfig(['mobile']),
152
153
  };
153
154
 
154
- /** Neutral background regression test with page content scrolled under the mobile overhang. */
155
+ /** Neutral background regression test with page content scrolled under the sticky banner. */
155
156
  export const BackgroundIsNotTransparent: Story = {
156
157
  args: {
157
158
  sentiment: 'neutral',
@@ -198,6 +199,7 @@ export const BackgroundIsNotTransparent: Story = {
198
199
 
199
200
  if (scrollContainer) {
200
201
  scrollContainer.scrollTop = 250;
202
+ await wait(ENTRY_ANIMATION_DURATION);
201
203
  }
202
204
  },
203
205
  ...withVariantConfig(['mobile'], {
@@ -207,6 +209,25 @@ export const BackgroundIsNotTransparent: Story = {
207
209
  }),
208
210
  };
209
211
 
212
+ /** Mobile entry animation visual regression test. */
213
+ export const AnimatedEntryMobile: Story = {
214
+ render: function Render() {
215
+ return (
216
+ <CriticalBanner
217
+ sentiment="negative"
218
+ title="Your account is restricted."
219
+ description="Review your personal details to get your account active again."
220
+ action={{ label: 'Review details', onClick: fn() }}
221
+ />
222
+ );
223
+ },
224
+ ...withVariantConfig(['mobile'], {
225
+ parameters: {
226
+ chromatic: { pauseAnimationAtEnd: true },
227
+ },
228
+ }),
229
+ };
230
+
210
231
  /**
211
232
  * Tests keyboard navigation and interaction:
212
233
  * - Tab focuses the first chevron toggle button
@@ -73,6 +73,10 @@
73
73
  box-shadow: inset 1px 0 0 0 rgba(0,0,0,0.10196);
74
74
  box-shadow: inset 1px 0 0 0 var(--color-border-neutral);
75
75
  }
76
+ [dir="rtl"] .np-theme-personal .sliding-panel.sliding-panel--border-right {
77
+ box-shadow: inset -1px 0 0 0 rgba(0,0,0,0.10196);
78
+ box-shadow: inset -1px 0 0 0 var(--color-border-neutral);
79
+ }
76
80
  .np-theme-personal .sliding-panel.sliding-panel--border-bottom {
77
81
  box-shadow: inset 0 1px 0 0 rgba(0,0,0,0.10196);
78
82
  box-shadow: inset 0 1px 0 0 var(--color-border-neutral);
@@ -81,3 +85,7 @@
81
85
  box-shadow: inset -1px 0 0 0 rgba(0,0,0,0.10196);
82
86
  box-shadow: inset -1px 0 0 0 var(--color-border-neutral);
83
87
  }
88
+ [dir="rtl"] .np-theme-personal .sliding-panel.sliding-panel--border-left {
89
+ box-shadow: inset 1px 0 0 0 rgba(0,0,0,0.10196);
90
+ box-shadow: inset 1px 0 0 0 var(--color-border-neutral);
91
+ }
@@ -85,6 +85,10 @@
85
85
 
86
86
  &.sliding-panel--border-right {
87
87
  box-shadow: inset 1px 0 0 0 var(--color-border-neutral);
88
+
89
+ [dir="rtl"] & {
90
+ box-shadow: inset -1px 0 0 0 var(--color-border-neutral);
91
+ }
88
92
  }
89
93
 
90
94
  &.sliding-panel--border-bottom {
@@ -93,6 +97,10 @@
93
97
 
94
98
  &.sliding-panel--border-left {
95
99
  box-shadow: inset -1px 0 0 0 var(--color-border-neutral);
100
+
101
+ [dir="rtl"] & {
102
+ box-shadow: inset 1px 0 0 0 var(--color-border-neutral);
103
+ }
96
104
  }
97
105
  }
98
106
  }
@@ -11,14 +11,15 @@ export default {
11
11
 
12
12
  type Story = StoryObj<typeof SlidingPanel>;
13
13
 
14
- const iframeDiff = withRTLDiff({ renderAs: 'iframe' });
14
+ const iframeDiff = withRTLDiff({ renderAs: 'iframe', width: 'stretch' });
15
15
 
16
16
  const containerStyle: React.CSSProperties = {
17
17
  position: 'relative',
18
- width: 300,
18
+ width: '100%',
19
19
  height: 120,
20
20
  overflow: 'hidden',
21
- border: '1px dashed #ccc',
21
+ border: '1px solid #f5f5f5',
22
+ background: '#f5f5f5',
22
23
  };
23
24
 
24
25
  const panelContentStyle: React.CSSProperties = {
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-webpack5';
3
3
 
4
4
  import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
@@ -6,40 +6,38 @@ import { Snackbar } from '../Snackbar';
6
6
 
7
7
  export default {
8
8
  component: Snackbar,
9
- title: 'Dialogs/Snackbar/Tests/RTL Diff',
9
+ title: 'Prompts/Snackbar/Tests/RTL Diff',
10
10
  tags: ['!autodocs', '!manifest'],
11
11
  } satisfies Meta<typeof Snackbar>;
12
12
 
13
13
  type Story = StoryObj<typeof Snackbar>;
14
14
 
15
15
  const TIMEOUT = 10_000_000;
16
- const iframeDiff = withRTLDiff({ renderAs: 'iframe' });
17
16
 
18
- function RenderWithAction() {
19
- const [timestamp] = useState(() => Date.now());
17
+ function SnackbarStory({ action }: { action?: { label: string } }) {
18
+ const [timestamp, setTimestamp] = useState(0);
19
+
20
+ useEffect(() => {
21
+ // eslint-disable-next-line react-hooks/set-state-in-effect
22
+ setTimestamp(1);
23
+ }, []);
20
24
 
21
25
  return (
22
26
  <Snackbar
23
27
  text={<span>Transfer completed successfully</span>}
24
28
  timestamp={timestamp}
25
29
  timeout={TIMEOUT}
26
- action={{ label: 'View' }}
30
+ action={action}
27
31
  />
28
32
  );
29
33
  }
30
34
 
31
- function RenderWithoutAction() {
32
- const [timestamp] = useState(() => Date.now());
33
-
34
- return <Snackbar text={<span>Settings saved</span>} timestamp={timestamp} timeout={TIMEOUT} />;
35
- }
36
-
37
35
  export const WithAction: Story = {
38
- render: () => <RenderWithAction />,
39
- ...iframeDiff,
36
+ render: () => <SnackbarStory action={{ label: 'View' }} />,
37
+ ...withRTLDiff({ renderAs: 'iframe' }),
40
38
  };
41
39
 
42
40
  export const WithoutAction: Story = {
43
- render: () => <RenderWithoutAction />,
44
- ...iframeDiff,
41
+ render: () => <SnackbarStory />,
42
+ ...withRTLDiff({ renderAs: 'iframe' }),
45
43
  };
@@ -13,7 +13,7 @@ import SnackbarProvider from '../SnackbarProvider';
13
13
 
14
14
  const meta: Meta = {
15
15
  component: Snackbar,
16
- title: 'Dialogs/Snackbar',
16
+ title: 'Prompts/Snackbar',
17
17
  };
18
18
  export default meta;
19
19
 
@@ -9,7 +9,7 @@ import { SnackbarConsumer } from '../SnackbarContext';
9
9
  import SnackbarProvider from '../SnackbarProvider';
10
10
 
11
11
  export default {
12
- title: 'Dialogs/Snackbar/Tests',
12
+ title: 'Prompts/Snackbar/Tests',
13
13
  component: Snackbar,
14
14
  argTypes: {},
15
15
  tags: ['!autodocs', '!manifest'],
@@ -1,8 +1,8 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
 
3
+ import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
3
4
  import { Sentiment, Size } from '../../common';
4
5
  import SentimentSurface from '../../SentimentSurface';
5
- import { withVariantConfig } from '../../../.storybook/helpers';
6
6
 
7
7
  import StatusIcon, { StatusIconSentiment } from '../StatusIcon';
8
8
 
@@ -90,9 +90,17 @@ export const Variants: Story = {
90
90
  },
91
91
  };
92
92
 
93
- export const RTL: Story = {
94
- render: Variants.render,
95
- ...withVariantConfig(['rtl']),
93
+ export const RTLDiff: Story = {
94
+ render: () => (
95
+ <>
96
+ <StatusIcon sentiment="positive" />
97
+ <StatusIcon sentiment="negative" />
98
+ <StatusIcon sentiment="warning" />
99
+ <StatusIcon sentiment="neutral" />
100
+ <StatusIcon sentiment="pending" />
101
+ </>
102
+ ),
103
+ ...withRTLDiff(),
96
104
  };
97
105
 
98
106
  /**