@transferwise/components 46.145.0 → 46.146.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.
Files changed (27) hide show
  1. package/build/main.css +503 -558
  2. package/build/prompt/InfoPrompt/InfoPrompt.js +1 -1
  3. package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
  4. package/build/prompt/InfoPrompt/InfoPrompt.mjs +2 -2
  5. package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
  6. package/build/sentimentSurface/SentimentSurface.js +9 -2
  7. package/build/sentimentSurface/SentimentSurface.js.map +1 -1
  8. package/build/sentimentSurface/SentimentSurface.mjs +9 -2
  9. package/build/sentimentSurface/SentimentSurface.mjs.map +1 -1
  10. package/build/styles/main.css +503 -558
  11. package/build/styles/sentimentSurface/SentimentSurface.css +325 -337
  12. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +3 -2
  13. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
  14. package/build/types/sentimentSurface/SentimentSurface.d.ts.map +1 -1
  15. package/package.json +3 -3
  16. package/src/criticalBanner/CriticalCommsBanner.test.tsx +1 -1
  17. package/src/main.css +503 -558
  18. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +42 -0
  19. package/src/prompt/InfoPrompt/InfoPrompt.test.tsx +65 -1
  20. package/src/prompt/InfoPrompt/InfoPrompt.tsx +15 -4
  21. package/src/sentimentSurface/SentimentSurface.css +325 -337
  22. package/src/sentimentSurface/SentimentSurface.docs.mdx +2 -0
  23. package/src/sentimentSurface/SentimentSurface.less +2 -322
  24. package/src/sentimentSurface/SentimentSurface.story.tsx +4 -0
  25. package/src/sentimentSurface/SentimentSurface.test.story.tsx +1 -5
  26. package/src/sentimentSurface/SentimentSurface.test.tsx +84 -3
  27. package/src/sentimentSurface/SentimentSurface.tsx +10 -2
@@ -6,6 +6,7 @@ import { Confetti, GiftBox, Star, Suitcase, Briefcase, Plane } from '@transferwi
6
6
  import { lorem10, lorem20 } from '../../test-utils';
7
7
  import Button from '../../button';
8
8
  import Title from '../../title';
9
+ import Link from '../../link';
9
10
  import { InfoPrompt, InfoPromptProps, InfoPromptMedia } from './InfoPrompt';
10
11
 
11
12
  const withComponentGrid =
@@ -232,6 +233,47 @@ export const WithAction: StoryObj<InfoPromptProps> = {
232
233
  ),
233
234
  };
234
235
 
236
+ /**
237
+ * The `description` prop accepts rich content (ReactNode), allowing inline interactive elements
238
+ * such as links that can trigger a popover.
239
+ *
240
+ * **Important**: If the description contains an interactive element, do not use the `action` prop
241
+ * simultaneously — InfoPrompt should have at most one interaction.
242
+ */
243
+ export const WithRichDescription: StoryObj<InfoPromptProps> = {
244
+ render: (args: InfoPromptProps) => (
245
+ <>
246
+ <InfoPrompt
247
+ {...args}
248
+ sentiment="neutral"
249
+ description={
250
+ <>
251
+ Your transfer is subject to our{' '}
252
+ <Link href="/fees" target="_blank" type="link-default">
253
+ fee schedule
254
+ </Link>
255
+ .
256
+ </>
257
+ }
258
+ />
259
+ <InfoPrompt
260
+ {...args}
261
+ sentiment="warning"
262
+ title="Verification Required"
263
+ description={
264
+ <>
265
+ Please{' '}
266
+ <Link href="/verify" type="link-default">
267
+ verify your identity
268
+ </Link>{' '}
269
+ to continue sending money.
270
+ </>
271
+ }
272
+ />
273
+ </>
274
+ ),
275
+ };
276
+
235
277
  /**
236
278
  * When `onDismiss` is provided, a close button appears allowing users to dismiss the prompt.
237
279
  * Note: The component itself is not automatically removed - you must handle state management.
@@ -1,4 +1,5 @@
1
- import { fireEvent, mockMatchMedia, render, screen, userEvent, waitFor } from '../../test-utils';
1
+ import { act, fireEvent, mockMatchMedia, render, screen, userEvent } from '../../test-utils';
2
+ import { WDS_LIVE_REGION_DELAY_MS } from '../../common/constants';
2
3
  import { resetLiveRegionAnnouncementQueue } from '../../common/liveRegion/LiveRegion';
3
4
  import { InfoPrompt, InfoPromptProps } from './InfoPrompt';
4
5
 
@@ -15,11 +16,74 @@ describe('InfoPrompt', () => {
15
16
  resetLiveRegionAnnouncementQueue();
16
17
  });
17
18
 
19
+ afterEach(() => {
20
+ jest.clearAllTimers();
21
+ jest.useRealTimers();
22
+ });
23
+
18
24
  it('renders description', () => {
19
25
  render(<InfoPrompt {...defaultProps} />);
20
26
  expect(screen.getByText('Prompt description')).toBeInTheDocument();
21
27
  });
22
28
 
29
+ it('renders rich-content description (ReactNode)', () => {
30
+ render(
31
+ <InfoPrompt
32
+ description={
33
+ <>
34
+ See our <a href="/fees">fee schedule</a>.
35
+ </>
36
+ }
37
+ />,
38
+ );
39
+ expect(screen.getByRole('link', { name: 'fee schedule', hidden: true })).toBeInTheDocument();
40
+ expect(screen.getByRole('link', { name: 'fee schedule', hidden: true })).toHaveAttribute(
41
+ 'href',
42
+ '/fees',
43
+ );
44
+ });
45
+
46
+ it('re-announces when rich-content description text changes', () => {
47
+ jest.useFakeTimers();
48
+
49
+ const { rerender } = render(
50
+ <InfoPrompt
51
+ description={
52
+ <>
53
+ See our <a href="/fees">fee schedule</a>.
54
+ </>
55
+ }
56
+ />,
57
+ );
58
+
59
+ act(() => {
60
+ jest.advanceTimersByTime(WDS_LIVE_REGION_DELAY_MS);
61
+ });
62
+
63
+ const liveRegion = screen.getByRole('status');
64
+ expect(liveRegion.firstElementChild).not.toHaveAttribute('aria-hidden');
65
+ expect(liveRegion).toHaveTextContent('See our fee schedule.');
66
+
67
+ rerender(
68
+ <InfoPrompt
69
+ description={
70
+ <>
71
+ Read the updated <a href="/fees">pricing guide</a>.
72
+ </>
73
+ }
74
+ />,
75
+ );
76
+
77
+ expect(liveRegion.firstElementChild).toHaveAttribute('aria-hidden', 'true');
78
+
79
+ act(() => {
80
+ jest.advanceTimersByTime(WDS_LIVE_REGION_DELAY_MS);
81
+ });
82
+
83
+ expect(liveRegion.firstElementChild).not.toHaveAttribute('aria-hidden');
84
+ expect(liveRegion).toHaveTextContent('Read the updated pricing guide.');
85
+ });
86
+
23
87
  it('renders title when provided', () => {
24
88
  render(<InfoPrompt {...defaultProps} title="Test Title" />);
25
89
  expect(screen.getByText('Test Title')).toBeInTheDocument();
@@ -1,4 +1,4 @@
1
- import { HTMLAttributes, ReactNode, useState } from 'react';
1
+ import { Children, HTMLAttributes, ReactNode, isValidElement, useState } from 'react';
2
2
  import { LiveRegion, Sentiment, Typography } from '../../common';
3
3
  import type { AriaLive } from '../../common';
4
4
  import { GiftBox } from '@transferwise/icons';
@@ -50,9 +50,10 @@ export type InfoPromptProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'ar
50
50
  */
51
51
  title?: string;
52
52
  /**
53
- * Description text for the prompt (required)
53
+ * Description content for the prompt (required). Accepts plain text or rich content (ReactNode).
54
+ * If the description contains an interactive element, the `action` prop must not be used simultaneously.
54
55
  */
55
- description: string;
56
+ description: ReactNode;
56
57
  className?: string;
57
58
  /**
58
59
  * Sets the ARIA live region politeness level.
@@ -86,7 +87,17 @@ export const InfoPrompt = ({
86
87
  ...restProps
87
88
  }: InfoPromptProps) => {
88
89
  const [shouldFire, setShouldFire] = useState<boolean>();
89
- const announceOnChange = [title, description, action?.label].filter(Boolean).join('|');
90
+ const announceOnChange = [
91
+ title,
92
+ Children.toArray(description)
93
+ .map((child) =>
94
+ isValidElement<{ children?: ReactNode }>(child) ? child.props.children : child,
95
+ )
96
+ .join(''),
97
+ action?.label,
98
+ ]
99
+ .filter(Boolean)
100
+ .join('|');
90
101
  const statusIconSentiment =
91
102
  sentiment === 'success'
92
103
  ? Sentiment.POSITIVE