@transferwise/components 0.0.0-experimental-c68416c → 0.0.0-experimental-26bc819

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 (80) hide show
  1. package/build/field/Field.js +10 -0
  2. package/build/field/Field.js.map +1 -1
  3. package/build/field/Field.mjs +10 -0
  4. package/build/field/Field.mjs.map +1 -1
  5. package/build/index.js +2 -0
  6. package/build/index.js.map +1 -1
  7. package/build/index.mjs +1 -0
  8. package/build/index.mjs.map +1 -1
  9. package/build/inputs/SelectInput.js +5 -10
  10. package/build/inputs/SelectInput.js.map +1 -1
  11. package/build/inputs/SelectInput.mjs +6 -11
  12. package/build/inputs/SelectInput.mjs.map +1 -1
  13. package/build/inputs/_BottomSheet.js +2 -5
  14. package/build/inputs/_BottomSheet.js.map +1 -1
  15. package/build/inputs/_BottomSheet.mjs +3 -6
  16. package/build/inputs/_BottomSheet.mjs.map +1 -1
  17. package/build/inputs/_Popover.js +0 -2
  18. package/build/inputs/_Popover.js.map +1 -1
  19. package/build/inputs/_Popover.mjs +0 -2
  20. package/build/inputs/_Popover.mjs.map +1 -1
  21. package/build/listItem/Prompt/ListItemPrompt.js +10 -0
  22. package/build/listItem/Prompt/ListItemPrompt.js.map +1 -1
  23. package/build/listItem/Prompt/ListItemPrompt.mjs +10 -0
  24. package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -1
  25. package/build/main.css +30 -1
  26. package/build/prompt/ActionPrompt/ActionPrompt.js +123 -0
  27. package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -0
  28. package/build/prompt/ActionPrompt/ActionPrompt.mjs +118 -0
  29. package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -0
  30. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js +13 -11
  31. package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
  32. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs +14 -12
  33. package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
  34. package/build/styles/main.css +30 -1
  35. package/build/styles/prompt/ActionPrompt/ActionPrompt.css +20 -0
  36. package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +10 -1
  37. package/build/typeahead/Typeahead.js +10 -0
  38. package/build/typeahead/Typeahead.js.map +1 -1
  39. package/build/typeahead/Typeahead.mjs +10 -0
  40. package/build/typeahead/Typeahead.mjs.map +1 -1
  41. package/build/types/index.d.ts +2 -2
  42. package/build/types/index.d.ts.map +1 -1
  43. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  44. package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
  45. package/build/types/inputs/_Popover.d.ts.map +1 -1
  46. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +26 -0
  47. package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -0
  48. package/build/types/prompt/ActionPrompt/ActionPrompt.test.d.ts +2 -0
  49. package/build/types/prompt/ActionPrompt/ActionPrompt.test.d.ts.map +1 -0
  50. package/build/types/prompt/ActionPrompt/index.d.ts +3 -0
  51. package/build/types/prompt/ActionPrompt/index.d.ts.map +1 -0
  52. package/build/types/prompt/InlinePrompt/InlinePrompt.test.d.ts +2 -0
  53. package/build/types/prompt/InlinePrompt/InlinePrompt.test.d.ts.map +1 -0
  54. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
  55. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.test.d.ts +2 -0
  56. package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.test.d.ts.map +1 -0
  57. package/build/types/prompt/index.d.ts +2 -0
  58. package/build/types/prompt/index.d.ts.map +1 -1
  59. package/package.json +3 -4
  60. package/src/index.ts +2 -2
  61. package/src/inputs/SelectInput.spec.tsx +7 -78
  62. package/src/inputs/SelectInput.tsx +10 -21
  63. package/src/inputs/_BottomSheet.tsx +4 -7
  64. package/src/inputs/_Popover.tsx +0 -2
  65. package/src/main.css +30 -1
  66. package/src/main.less +1 -0
  67. package/src/moneyInput/MoneyInput.spec.tsx +7 -26
  68. package/src/prompt/ActionPrompt/ActionPrompt.css +20 -0
  69. package/src/prompt/ActionPrompt/ActionPrompt.less +22 -0
  70. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +189 -0
  71. package/src/prompt/ActionPrompt/ActionPrompt.test.tsx +256 -0
  72. package/src/prompt/ActionPrompt/ActionPrompt.tsx +129 -0
  73. package/src/prompt/ActionPrompt/index.ts +2 -0
  74. package/src/prompt/InlinePrompt/{InlinePrompt.spec.tsx → InlinePrompt.test.tsx} +3 -38
  75. package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +10 -1
  76. package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +11 -1
  77. package/src/prompt/PrimitivePrompt/PrimitivePrompt.test.tsx +161 -0
  78. package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +15 -11
  79. package/src/prompt/index.ts +3 -0
  80. package/src/ssr.spec.tsx +1 -0
@@ -0,0 +1,189 @@
1
+ import { ReactElement, useState } from 'react';
2
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
3
+ import { fn } from 'storybook/test';
4
+ import { Bank } from '@transferwise/icons';
5
+ import { ActionPrompt } from './ActionPrompt';
6
+ import { lorem10 } from '../../test-utils';
7
+ import { allModes } from '../../../.storybook/modes';
8
+
9
+ const meta: Meta<typeof ActionPrompt> = {
10
+ title: 'Prompts/ActionPrompt',
11
+ component: ActionPrompt,
12
+ tags: ['new'],
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof ActionPrompt>;
17
+
18
+ const withComponentGrid =
19
+ ({ maxWidth = 'auto', gap = '1rem' } = {}) =>
20
+ (Story: () => ReactElement) => (
21
+ <div
22
+ style={{
23
+ width: '100%',
24
+ display: 'flex',
25
+ flexDirection: 'column',
26
+ justifyContent: 'flex-start',
27
+ alignItems: 'flex-start',
28
+ alignContent: 'flex-start',
29
+ gap,
30
+ maxWidth,
31
+ }}
32
+ >
33
+ <Story />
34
+ </div>
35
+ );
36
+
37
+ export const AllSentiments: Story = {
38
+ render: () => {
39
+ return (
40
+ <>
41
+ {(['negative', 'warning', 'neutral', 'success', 'proposition'] as const).map(
42
+ (sentiment) => (
43
+ <ActionPrompt
44
+ key={sentiment}
45
+ sentiment={sentiment}
46
+ title="Title"
47
+ description={lorem10}
48
+ actionPrimary={{ label: 'Primary', onClick: fn() }}
49
+ actionSecondary={{ label: 'Secondary', onClick: fn() }}
50
+ className="m-b-2"
51
+ onDismiss={fn()}
52
+ />
53
+ ),
54
+ )}
55
+ </>
56
+ );
57
+ },
58
+ decorators: [withComponentGrid()],
59
+ };
60
+
61
+ export const Mobile: Story = {
62
+ ...AllSentiments,
63
+ parameters: {
64
+ variants: ['mobile'],
65
+ chromatic: {
66
+ mobile: allModes.largeMobile,
67
+ },
68
+ },
69
+ };
70
+
71
+ export const PrimaryActionOnly: Story = {
72
+ args: {
73
+ sentiment: 'proposition',
74
+ title: 'Payment successful',
75
+ description: 'Your money is on its way',
76
+ actionPrimary: { label: 'View details', onClick: fn() },
77
+ onDismiss: fn(),
78
+ },
79
+ };
80
+
81
+ export const WithoutDescription: Story = {
82
+ render: () => {
83
+ return (
84
+ <>
85
+ <ActionPrompt
86
+ sentiment="warning"
87
+ title="Session expiring soon"
88
+ actionPrimary={{ label: 'Stay logged in', onClick: fn() }}
89
+ actionSecondary={{ label: 'Log out', onClick: fn() }}
90
+ onDismiss={fn()}
91
+ />
92
+ <ActionPrompt
93
+ sentiment="success"
94
+ title="Session expiring soon"
95
+ actionPrimary={{ label: 'Stay logged in', onClick: fn() }}
96
+ />
97
+ <ActionPrompt
98
+ title="Session expiring soon"
99
+ actionPrimary={{ label: 'Stay logged in', onClick: fn() }}
100
+ />
101
+ </>
102
+ );
103
+ },
104
+ decorators: [withComponentGrid()],
105
+ };
106
+
107
+ export const Media: Story = {
108
+ render: () => {
109
+ return (
110
+ <>
111
+ <ActionPrompt
112
+ sentiment="neutral"
113
+ title="Custom image"
114
+ description={lorem10}
115
+ media={{ imgSrc: '../../wise-card.svg' }}
116
+ actionPrimary={{ label: 'Invite now', onClick: fn() }}
117
+ />
118
+ <ActionPrompt
119
+ sentiment="neutral"
120
+ title="Profile Image"
121
+ description={lorem10}
122
+ media={{ avatar: { imgSrc: '../../avatar-rectangle-fox.webp' } }}
123
+ actionPrimary={{ label: 'Invite now', onClick: fn() }}
124
+ />
125
+ <ActionPrompt
126
+ sentiment="proposition"
127
+ title="Business Profile"
128
+ description={lorem10}
129
+ media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
130
+ actionPrimary={{ label: 'Invite now', onClick: fn() }}
131
+ />
132
+ <ActionPrompt
133
+ sentiment="negative"
134
+ title="Personal Profile"
135
+ description={lorem10}
136
+ media={{ avatar: { profileType: 'PERSONAL', badge: { flagCode: 'EU' } } }}
137
+ actionPrimary={{ label: 'Invite now', onClick: fn() }}
138
+ />
139
+ <ActionPrompt
140
+ sentiment="warning"
141
+ title="Custom Icon"
142
+ description={lorem10}
143
+ media={{ avatar: { asset: <Bank /> } }}
144
+ actionPrimary={{ label: 'Invite now', onClick: fn() }}
145
+ />
146
+ <ActionPrompt
147
+ sentiment="success"
148
+ title="Initials"
149
+ description={lorem10}
150
+ media={{ avatar: { profileName: 'John Doe' } }}
151
+ actionPrimary={{ label: 'Invite now', onClick: fn() }}
152
+ actionSecondary={{ label: 'Maybe later', onClick: fn() }}
153
+ />
154
+ </>
155
+ );
156
+ },
157
+ decorators: [withComponentGrid()],
158
+ };
159
+
160
+ export const OnDismiss: Story = {
161
+ render: () => {
162
+ const [showNeutralPrompt, setShowNeutralPrompt] = useState(true);
163
+ const [showSuccessPrompt, setShowSuccessPrompt] = useState(true);
164
+
165
+ return (
166
+ <>
167
+ {showNeutralPrompt && (
168
+ <ActionPrompt
169
+ sentiment="proposition"
170
+ title="Payment successful"
171
+ description="Your money is on its way"
172
+ actionPrimary={{ label: 'View details', onClick: fn() }}
173
+ className="m-b-2"
174
+ onDismiss={() => setShowNeutralPrompt(false)}
175
+ />
176
+ )}
177
+ {showSuccessPrompt && (
178
+ <ActionPrompt
179
+ sentiment="success"
180
+ title="Payment successful"
181
+ description="Your money is on its way"
182
+ actionPrimary={{ label: 'View details', onClick: fn() }}
183
+ onDismiss={() => setShowSuccessPrompt(false)}
184
+ />
185
+ )}
186
+ </>
187
+ );
188
+ },
189
+ };
@@ -0,0 +1,256 @@
1
+ import { Sentiment } from '../../common';
2
+ import { mockMatchMedia, render, screen } from '../../test-utils';
3
+ import { ActionPrompt, ActionPromptProps } from './ActionPrompt';
4
+ import { userEvent } from '@testing-library/user-event';
5
+
6
+ mockMatchMedia();
7
+
8
+ describe('ActionPrompt', () => {
9
+ const defaultProps: ActionPromptProps = {
10
+ title: 'Test Title',
11
+ actionPrimary: {
12
+ label: 'Primary Action',
13
+ onClick: jest.fn(),
14
+ },
15
+ };
16
+
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ describe('Basic rendering', () => {
22
+ it('renders title', () => {
23
+ render(<ActionPrompt {...defaultProps} />);
24
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
25
+ });
26
+
27
+ it('renders description when provided', () => {
28
+ render(<ActionPrompt {...defaultProps} description="Test description" />);
29
+ expect(screen.getByText('Test description')).toBeInTheDocument();
30
+ });
31
+ });
32
+
33
+ [
34
+ { sentiment: 'negative' as const, statusIconLabel: 'Error:' },
35
+ { sentiment: 'warning' as const, statusIconLabel: 'Warning:' },
36
+ { sentiment: 'neutral' as const, statusIconLabel: 'Information:' },
37
+ { sentiment: 'success' as const, statusIconLabel: 'Success:' },
38
+ { sentiment: 'proposition' as const, statusIconLabel: '' },
39
+ ].forEach(({ sentiment, statusIconLabel }) => {
40
+ describe(sentiment, () => {
41
+ if (statusIconLabel) {
42
+ it('should render StatusIcon', () => {
43
+ render(<ActionPrompt {...defaultProps} sentiment={sentiment} />);
44
+ expect(screen.getByLabelText(statusIconLabel)).toBeInTheDocument();
45
+ });
46
+ }
47
+ });
48
+ });
49
+
50
+ describe('Actions', () => {
51
+ it('renders primary action button with correct label', () => {
52
+ render(<ActionPrompt {...defaultProps} />);
53
+ expect(screen.getByText('Primary Action')).toBeInTheDocument();
54
+ });
55
+
56
+ it('calls primary action onClick when clicked', async () => {
57
+ const onClick = jest.fn();
58
+ const user = userEvent.setup();
59
+ render(
60
+ <ActionPrompt {...defaultProps} actionPrimary={{ label: 'Primary Action', onClick }} />,
61
+ );
62
+ const button = screen.getByRole('button', { name: 'Primary Action' });
63
+ await user.click(button);
64
+ expect(onClick).toHaveBeenCalledTimes(1);
65
+ });
66
+
67
+ it('renders primary action as link when href is provided', () => {
68
+ render(
69
+ <ActionPrompt
70
+ {...defaultProps}
71
+ actionPrimary={{ label: 'Primary Action', href: '/test-url' }}
72
+ />,
73
+ );
74
+ const link = screen.getByRole('link', { name: 'Primary Action' });
75
+ expect(link).toHaveAttribute('href', '/test-url');
76
+ });
77
+
78
+ it('renders secondary action when provided', () => {
79
+ render(
80
+ <ActionPrompt
81
+ {...defaultProps}
82
+ actionSecondary={{ label: 'Secondary Action', onClick: jest.fn() }}
83
+ />,
84
+ );
85
+ expect(screen.getByText('Secondary Action')).toBeInTheDocument();
86
+ });
87
+
88
+ it('calls secondary action onClick when clicked', async () => {
89
+ const onClick = jest.fn();
90
+ const user = userEvent.setup();
91
+ render(
92
+ <ActionPrompt {...defaultProps} actionSecondary={{ label: 'Secondary Action', onClick }} />,
93
+ );
94
+ const button = screen.getByRole('button', { name: 'Secondary Action' });
95
+ await user.click(button);
96
+ expect(onClick).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ it('renders secondary action as link when href is provided', () => {
100
+ render(
101
+ <ActionPrompt
102
+ {...defaultProps}
103
+ actionSecondary={{ label: 'Secondary Action', href: '/secondary-url' }}
104
+ />,
105
+ );
106
+ const link = screen.getByRole('link', { name: 'Secondary Action' });
107
+ expect(link).toHaveAttribute('href', '/secondary-url');
108
+ });
109
+ });
110
+
111
+ describe('Media', () => {
112
+ describe('Image media', () => {
113
+ it('renders custom image when imgSrc is provided', () => {
114
+ render(
115
+ <ActionPrompt
116
+ {...defaultProps}
117
+ media={{ imgSrc: '/test-image.jpg', 'aria-label': 'Test image' }}
118
+ />,
119
+ );
120
+
121
+ const img = screen.getByRole('img', { name: 'Test image' });
122
+
123
+ expect(img).toBeInTheDocument();
124
+ expect(img).toHaveAttribute('src', '/test-image.jpg');
125
+ });
126
+
127
+ it('renders image with empty alt when aria-label is not provided', () => {
128
+ const { container } = render(
129
+ <ActionPrompt {...defaultProps} media={{ imgSrc: '/test-image.jpg' }} />,
130
+ );
131
+ const img = container.querySelector('img[src="/test-image.jpg"]');
132
+ expect(img).toHaveAttribute('alt', '');
133
+ });
134
+ });
135
+
136
+ describe('Avatar media', () => {
137
+ it('renders avatar with image', () => {
138
+ const { container } = render(
139
+ <ActionPrompt {...defaultProps} media={{ avatar: { imgSrc: '/avatar.jpg' } }} />,
140
+ );
141
+ const img = container.querySelector('img[src="/avatar.jpg"]');
142
+ expect(img).toBeInTheDocument();
143
+ expect(img).toHaveAttribute('src', '/avatar.jpg');
144
+ });
145
+
146
+ it('renders avatar with profile name', () => {
147
+ render(<ActionPrompt {...defaultProps} media={{ avatar: { profileName: 'John Doe' } }} />);
148
+ expect(screen.getByText('JD')).toBeInTheDocument();
149
+ });
150
+
151
+ it('renders avatar with custom asset', () => {
152
+ render(
153
+ <ActionPrompt
154
+ {...defaultProps}
155
+ media={{ avatar: { asset: <span data-testid="custom-icon">Icon</span> } }}
156
+ />,
157
+ );
158
+
159
+ expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
160
+ });
161
+
162
+ it('renders avatar with badge', () => {
163
+ render(
164
+ <ActionPrompt
165
+ {...defaultProps}
166
+ media={{ avatar: { profileType: 'BUSINESS', badge: { flagCode: 'GB' } } }}
167
+ />,
168
+ );
169
+
170
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
171
+ });
172
+
173
+ it('applies status badge for non-proposition sentiment when no badge is provided', () => {
174
+ render(
175
+ <ActionPrompt
176
+ {...defaultProps}
177
+ sentiment="success"
178
+ media={{ avatar: { profileType: 'PERSONAL' } }}
179
+ />,
180
+ );
181
+
182
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
183
+ });
184
+
185
+ it('renders avatar for proposition sentiment when no badge is provided', () => {
186
+ render(
187
+ <ActionPrompt
188
+ {...defaultProps}
189
+ sentiment="proposition"
190
+ media={{ avatar: { profileType: 'PERSONAL' } }}
191
+ />,
192
+ );
193
+
194
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
195
+ });
196
+ });
197
+
198
+ describe('Default media', () => {
199
+ it('renders GiftBox for proposition sentiment with no media', () => {
200
+ render(<ActionPrompt {...defaultProps} sentiment="proposition" />);
201
+ expect(screen.getByTestId('gift-box-icon')).toBeInTheDocument();
202
+ });
203
+
204
+ it('renders StatusIcon for non-proposition sentiment with no media', () => {
205
+ render(<ActionPrompt {...defaultProps} sentiment="success" />);
206
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
207
+ });
208
+ });
209
+ });
210
+
211
+ describe('Two actions styling', () => {
212
+ it('applies two actions class when secondary action is provided', () => {
213
+ render(
214
+ <ActionPrompt
215
+ {...defaultProps}
216
+ actionSecondary={{ label: 'Secondary', onClick: jest.fn() }}
217
+ data-testid="prompt"
218
+ />,
219
+ );
220
+
221
+ expect(screen.getByTestId('prompt')).toHaveClass('wds-action-prompt--with-two-actions');
222
+ });
223
+
224
+ it('does not apply two actions class when only primary action is provided', () => {
225
+ render(<ActionPrompt {...defaultProps} data-testid="prompt" />);
226
+ expect(screen.getByTestId('prompt')).not.toHaveClass('wds-action-prompt--with-two-actions');
227
+ });
228
+ });
229
+
230
+ describe('Responsive behavior', () => {
231
+ it('renders buttons as block on mobile', () => {
232
+ // Mock mobile viewport
233
+ const mockMatchMediaFn = window.matchMedia as jest.Mock;
234
+ mockMatchMediaFn.mockImplementation((query: string) => ({
235
+ matches: query === '(max-width: 768px)',
236
+ media: query,
237
+ onchange: null,
238
+ addListener: jest.fn(),
239
+ removeListener: jest.fn(),
240
+ addEventListener: jest.fn(),
241
+ removeEventListener: jest.fn(),
242
+ dispatchEvent: jest.fn(),
243
+ }));
244
+
245
+ render(
246
+ <ActionPrompt
247
+ {...defaultProps}
248
+ actionSecondary={{ label: 'Secondary', onClick: jest.fn() }}
249
+ />,
250
+ );
251
+
252
+ expect(screen.getByText('Primary Action')).toBeInTheDocument();
253
+ expect(screen.getByText('Secondary')).toBeInTheDocument();
254
+ });
255
+ });
256
+ });
@@ -0,0 +1,129 @@
1
+ import { ReactNode } from 'react';
2
+ import { clsx } from 'clsx';
3
+
4
+ import StatusIcon from '../../statusIcon';
5
+ import Body from '../../body';
6
+ import Button from '../../button';
7
+ import { Breakpoint, Typography } from '../../common';
8
+ import AvatarView, { AvatarViewProps } from '../../avatarView';
9
+ import Image from '../../image';
10
+ import { ButtonProps } from '../../button/Button.types';
11
+ import { PrimitivePrompt, PrimitivePromptProps } from '../PrimitivePrompt';
12
+ import { BadgeAssetsProps } from '../../badge';
13
+ import { GiftBox } from '@transferwise/icons';
14
+ import { useScreenSize } from '../../common/hooks/useScreenSize';
15
+
16
+ export type ActionPromptProps = {
17
+ title: ReactNode;
18
+ description?: ReactNode;
19
+ media?: {
20
+ imgSrc?: string;
21
+ avatar?: Pick<AvatarViewProps, 'imgSrc' | 'profileName' | 'profileType'> & {
22
+ asset?: AvatarViewProps['children'];
23
+ badge?: Pick<BadgeAssetsProps, 'flagCode'>;
24
+ };
25
+ 'aria-label'?: string;
26
+ };
27
+ actionPrimary: Pick<ButtonProps, 'onClick' | 'href' | 'target'> & {
28
+ label: ButtonProps['children'];
29
+ };
30
+ actionSecondary?: Pick<ButtonProps, 'onClick' | 'href' | 'target'> & {
31
+ label: ButtonProps['children'];
32
+ };
33
+ } & Pick<PrimitivePromptProps, 'id' | 'className' | 'data-testid' | 'sentiment' | 'onDismiss'>;
34
+
35
+ export const ActionPrompt = ({
36
+ sentiment = 'neutral',
37
+ title,
38
+ description,
39
+ onDismiss,
40
+ media,
41
+ actionPrimary,
42
+ actionSecondary,
43
+ id,
44
+ className,
45
+ 'data-testid': testId,
46
+ }: ActionPromptProps) => {
47
+ const isMobile = !useScreenSize(Breakpoint.MEDIUM);
48
+
49
+ const renderMedia = () => {
50
+ if (media?.imgSrc) {
51
+ return (
52
+ <Image
53
+ src={media.imgSrc}
54
+ className="wds-action-prompt--media-image"
55
+ alt={media['aria-label'] ?? ''}
56
+ />
57
+ );
58
+ }
59
+ if (media?.avatar) {
60
+ const badge = media.avatar.badge
61
+ ? media.avatar.badge
62
+ : sentiment === 'proposition'
63
+ ? {}
64
+ : { status: sentiment };
65
+ return (
66
+ <AvatarView {...media.avatar} badge={badge} size={48}>
67
+ {media.avatar.asset}
68
+ </AvatarView>
69
+ );
70
+ }
71
+ return sentiment === 'proposition' ? (
72
+ <AvatarView size={48}>
73
+ <GiftBox />
74
+ </AvatarView>
75
+ ) : (
76
+ <StatusIcon size={48} sentiment={sentiment} />
77
+ );
78
+ };
79
+
80
+ return (
81
+ <PrimitivePrompt
82
+ id={id}
83
+ sentiment={sentiment}
84
+ data-testid={testId}
85
+ className={clsx(
86
+ 'wds-action-prompt',
87
+ { 'wds-action-prompt--with-two-actions': !!actionSecondary },
88
+ className,
89
+ )}
90
+ media={renderMedia()}
91
+ actions={
92
+ <>
93
+ {actionSecondary && (
94
+ // @ts-expect-error onClick type mismatch
95
+ <Button
96
+ v2
97
+ size="md"
98
+ priority="secondary"
99
+ href={actionSecondary.href}
100
+ block={isMobile}
101
+ onClick={actionSecondary?.onClick}
102
+ >
103
+ {actionSecondary.label}
104
+ </Button>
105
+ )}
106
+ {/* @ts-expect-error onClick type mismatch */}
107
+ <Button
108
+ v2
109
+ size="md"
110
+ priority="primary"
111
+ href={actionPrimary.href}
112
+ block={isMobile}
113
+ onClick={actionPrimary.onClick}
114
+ >
115
+ {actionPrimary.label}
116
+ </Button>
117
+ </>
118
+ }
119
+ onDismiss={onDismiss}
120
+ >
121
+ <div className={clsx('d-flex', 'flex-column', 'justify-content-center')}>
122
+ <Body type={Typography.BODY_LARGE_BOLD}>{title}</Body>
123
+ {description && <Body>{description}</Body>}
124
+ </div>
125
+ </PrimitivePrompt>
126
+ );
127
+ };
128
+
129
+ export default ActionPrompt;
@@ -0,0 +1,2 @@
1
+ export type { ActionPromptProps } from './ActionPrompt';
2
+ export { ActionPrompt } from './ActionPrompt';
@@ -30,13 +30,6 @@ describe('InlinePrompt', () => {
30
30
  { sentiment: 'proposition' as const, statusIconLabel: '' },
31
31
  ].forEach(({ sentiment, statusIconLabel }) => {
32
32
  describe(sentiment, () => {
33
- it('should apply correct styles', () => {
34
- render(<InlinePrompt {...defaultProps} sentiment={sentiment} />);
35
- expect(screen.getByText('Prompt message').parentElement?.parentElement).toHaveClass(
36
- `wds-prompt--${sentiment === Sentiment.POSITIVE ? 'success' : sentiment}`,
37
- );
38
- });
39
-
40
33
  if (statusIconLabel) {
41
34
  it('should render StatusIcon', () => {
42
35
  render(<InlinePrompt {...defaultProps} sentiment={sentiment} />);
@@ -113,39 +106,11 @@ describe('InlinePrompt', () => {
113
106
  });
114
107
  });
115
108
 
116
- it('applies custom className, id, and data-testid', () => {
117
- render(
118
- <InlinePrompt
119
- {...defaultProps}
120
- className="custom-class"
121
- id="custom-id"
122
- data-testid="custom-test"
123
- />,
124
- );
109
+ it('forwards props to PrimitivePrompt', () => {
110
+ render(<InlinePrompt {...defaultProps} className="custom-class" data-testid="custom-test" />);
125
111
  const el = screen.getByTestId('custom-test');
126
112
  expect(el).toHaveClass('custom-class');
127
- expect(el).toHaveAttribute('id', 'custom-id');
128
- });
129
-
130
- describe('SentimentSurface integration', () => {
131
- it('maps positive sentiment to success for SentimentSurface', () => {
132
- render(
133
- <InlinePrompt {...defaultProps} sentiment={Sentiment.POSITIVE} data-testid="prompt" />,
134
- );
135
- const el = screen.getByTestId('prompt');
136
- // The component should have both the original class and the SentimentSurface class
137
- expect(el).toHaveClass('wds-prompt--success');
138
- expect(el).toHaveClass('wds-sentiment-surface');
139
- });
140
-
141
- it('passes through other sentiments unchanged', () => {
142
- render(
143
- <InlinePrompt {...defaultProps} sentiment={Sentiment.NEGATIVE} data-testid="prompt" />,
144
- );
145
- const el = screen.getByTestId('prompt');
146
- expect(el).toHaveClass('wds-prompt--negative');
147
- expect(el).toHaveClass('wds-sentiment-surface');
148
- });
113
+ expect(el).toBeInTheDocument();
149
114
  });
150
115
 
151
116
  describe('width', () => {
@@ -2,6 +2,8 @@
2
2
  border-radius: 10px;
3
3
  border-radius: var(--radius-small);
4
4
  display: flex;
5
+ gap: 16px;
6
+ gap: var(--Prompt-gap, var(--size-16));
5
7
  word-wrap: break-word;
6
8
  padding: 8px;
7
9
  padding: var(--Prompt-padding, var(--padding-x-small));
@@ -17,6 +19,9 @@
17
19
  grid-template-columns: auto 1fr;
18
20
  width: 100%;
19
21
  }
22
+ .wds-prompt__content-wrapper--with-dismiss {
23
+ grid-template-columns: auto 1fr auto;
24
+ }
20
25
  .wds-prompt__media-wrapper {
21
26
  align-self: flex-start;
22
27
  padding-top: calc(4px - 1px);
@@ -32,10 +37,14 @@
32
37
  gap: 8px;
33
38
  gap: var(--Prompt-actions-gap, var(--size-8));
34
39
  grid-column-start: 2;
40
+ grid-column-end: 3;
35
41
  }
36
- @media (max-width: 991px) {
42
+ @media (max-width: 767px) {
37
43
  .wds-prompt__actions-wrapper {
38
44
  grid-column: span 2;
39
45
  width: 100%;
40
46
  }
47
+ .wds-prompt__content-wrapper--with-dismiss .wds-prompt__actions-wrapper {
48
+ grid-column: span 3;
49
+ }
41
50
  }