@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.
- package/build/field/Field.js +10 -0
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +10 -0
- package/build/field/Field.mjs.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/index.mjs +1 -0
- package/build/index.mjs.map +1 -1
- package/build/inputs/SelectInput.js +5 -10
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +6 -11
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/inputs/_BottomSheet.js +2 -5
- package/build/inputs/_BottomSheet.js.map +1 -1
- package/build/inputs/_BottomSheet.mjs +3 -6
- package/build/inputs/_BottomSheet.mjs.map +1 -1
- package/build/inputs/_Popover.js +0 -2
- package/build/inputs/_Popover.js.map +1 -1
- package/build/inputs/_Popover.mjs +0 -2
- package/build/inputs/_Popover.mjs.map +1 -1
- package/build/listItem/Prompt/ListItemPrompt.js +10 -0
- package/build/listItem/Prompt/ListItemPrompt.js.map +1 -1
- package/build/listItem/Prompt/ListItemPrompt.mjs +10 -0
- package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -1
- package/build/main.css +30 -1
- package/build/prompt/ActionPrompt/ActionPrompt.js +123 -0
- package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -0
- package/build/prompt/ActionPrompt/ActionPrompt.mjs +118 -0
- package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js +13 -11
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs +14 -12
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
- package/build/styles/main.css +30 -1
- package/build/styles/prompt/ActionPrompt/ActionPrompt.css +20 -0
- package/build/styles/prompt/PrimitivePrompt/PrimitivePrompt.css +10 -1
- package/build/typeahead/Typeahead.js +10 -0
- package/build/typeahead/Typeahead.js.map +1 -1
- package/build/typeahead/Typeahead.mjs +10 -0
- package/build/typeahead/Typeahead.mjs.map +1 -1
- package/build/types/index.d.ts +2 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/_BottomSheet.d.ts.map +1 -1
- package/build/types/inputs/_Popover.d.ts.map +1 -1
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +26 -0
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -0
- package/build/types/prompt/ActionPrompt/ActionPrompt.test.d.ts +2 -0
- package/build/types/prompt/ActionPrompt/ActionPrompt.test.d.ts.map +1 -0
- package/build/types/prompt/ActionPrompt/index.d.ts +3 -0
- package/build/types/prompt/ActionPrompt/index.d.ts.map +1 -0
- package/build/types/prompt/InlinePrompt/InlinePrompt.test.d.ts +2 -0
- package/build/types/prompt/InlinePrompt/InlinePrompt.test.d.ts.map +1 -0
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.test.d.ts +2 -0
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.test.d.ts.map +1 -0
- package/build/types/prompt/index.d.ts +2 -0
- package/build/types/prompt/index.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/index.ts +2 -2
- package/src/inputs/SelectInput.spec.tsx +7 -78
- package/src/inputs/SelectInput.tsx +10 -21
- package/src/inputs/_BottomSheet.tsx +4 -7
- package/src/inputs/_Popover.tsx +0 -2
- package/src/main.css +30 -1
- package/src/main.less +1 -0
- package/src/moneyInput/MoneyInput.spec.tsx +7 -26
- package/src/prompt/ActionPrompt/ActionPrompt.css +20 -0
- package/src/prompt/ActionPrompt/ActionPrompt.less +22 -0
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +189 -0
- package/src/prompt/ActionPrompt/ActionPrompt.test.tsx +256 -0
- package/src/prompt/ActionPrompt/ActionPrompt.tsx +129 -0
- package/src/prompt/ActionPrompt/index.ts +2 -0
- package/src/prompt/InlinePrompt/{InlinePrompt.spec.tsx → InlinePrompt.test.tsx} +3 -38
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.css +10 -1
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +11 -1
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.test.tsx +161 -0
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +15 -11
- package/src/prompt/index.ts +3 -0
- 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;
|
|
@@ -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('
|
|
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).
|
|
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:
|
|
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
|
}
|