@transferwise/components 46.125.0 → 46.126.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/avatarView/AvatarView.js.map +1 -1
- package/build/avatarView/AvatarView.mjs.map +1 -1
- package/build/common/locale/index.js +13 -0
- package/build/common/locale/index.js.map +1 -1
- package/build/common/locale/index.mjs +13 -1
- package/build/common/locale/index.mjs.map +1 -1
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js +31 -1
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.js.map +1 -1
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs +32 -2
- package/build/expressiveMoneyInput/currencySelector/CurrencySelector.mjs.map +1 -1
- package/build/field/Field.js +1 -0
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +1 -0
- package/build/field/Field.mjs.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/index.mjs +2 -1
- package/build/index.mjs.map +1 -1
- package/build/inputs/Input.js.map +1 -1
- package/build/inputs/Input.mjs.map +1 -1
- package/build/inputs/SearchInput.js.map +1 -1
- package/build/inputs/SearchInput.mjs.map +1 -1
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/inputs/TextArea.js.map +1 -1
- package/build/inputs/TextArea.mjs.map +1 -1
- package/build/listItem/ListItem.js +2 -2
- package/build/listItem/ListItem.js.map +1 -1
- package/build/listItem/ListItem.mjs +2 -2
- package/build/listItem/ListItem.mjs.map +1 -1
- package/build/listItem/Prompt/ListItemPrompt.js +1 -0
- package/build/listItem/Prompt/ListItemPrompt.js.map +1 -1
- package/build/listItem/Prompt/ListItemPrompt.mjs +1 -0
- package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -1
- package/build/main.css +31 -0
- package/build/moneyInput/MoneyInput.js +6 -1
- package/build/moneyInput/MoneyInput.js.map +1 -1
- package/build/moneyInput/MoneyInput.mjs +6 -1
- package/build/moneyInput/MoneyInput.mjs.map +1 -1
- package/build/prompt/ActionPrompt/ActionPrompt.js +27 -4
- package/build/prompt/ActionPrompt/ActionPrompt.js.map +1 -1
- package/build/prompt/ActionPrompt/ActionPrompt.mjs +27 -4
- package/build/prompt/ActionPrompt/ActionPrompt.mjs.map +1 -1
- package/build/prompt/InfoPrompt/InfoPrompt.js +113 -0
- package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -0
- package/build/prompt/InfoPrompt/InfoPrompt.mjs +111 -0
- package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -0
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
- package/build/prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
- package/build/radioOption/RadioOption.js.map +1 -1
- package/build/radioOption/RadioOption.mjs.map +1 -1
- package/build/slidingPanel/SlidingPanel.js.map +1 -1
- package/build/slidingPanel/SlidingPanel.mjs.map +1 -1
- package/build/statusIcon/StatusIcon.js +2 -0
- package/build/statusIcon/StatusIcon.js.map +1 -1
- package/build/statusIcon/StatusIcon.mjs +2 -0
- package/build/statusIcon/StatusIcon.mjs.map +1 -1
- package/build/styles/main.css +31 -0
- package/build/styles/prompt/InfoPrompt/InfoPrompt.css +31 -0
- package/build/table/TableCell.js.map +1 -1
- package/build/table/TableCell.mjs.map +1 -1
- package/build/typeahead/Typeahead.js +1 -0
- package/build/typeahead/Typeahead.js.map +1 -1
- package/build/typeahead/Typeahead.mjs +1 -0
- package/build/typeahead/Typeahead.mjs.map +1 -1
- package/build/types/avatarView/AvatarView.d.ts +1 -1
- package/build/types/avatarView/AvatarView.d.ts.map +1 -1
- package/build/types/common/locale/index.d.ts +8 -0
- package/build/types/common/locale/index.d.ts.map +1 -1
- package/build/types/expressiveMoneyInput/currencySelector/CurrencySelector.d.ts.map +1 -1
- package/build/types/index.d.ts +3 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/Input.d.ts.map +1 -1
- package/build/types/inputs/SearchInput.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/TextArea.d.ts.map +1 -1
- package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/primitives/PrimitiveAnchor/PrimitiveAnchor.types.d.ts.map +1 -1
- package/build/types/primitives/PrimitiveButton/PrimitiveButton.types.d.ts.map +1 -1
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts +4 -2
- package/build/types/prompt/ActionPrompt/ActionPrompt.d.ts.map +1 -1
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +56 -0
- package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -0
- package/build/types/prompt/InfoPrompt/index.d.ts +3 -0
- package/build/types/prompt/InfoPrompt/index.d.ts.map +1 -0
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts +5 -5
- package/build/types/prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
- package/build/types/prompt/index.d.ts +2 -0
- package/build/types/prompt/index.d.ts.map +1 -1
- package/build/types/radioOption/RadioOption.d.ts.map +1 -1
- package/build/types/slidingPanel/SlidingPanel.d.ts.map +1 -1
- package/build/types/statusIcon/StatusIcon.d.ts +2 -1
- package/build/types/statusIcon/StatusIcon.d.ts.map +1 -1
- package/build/types/table/TableCell.d.ts.map +1 -1
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.js.map +1 -1
- package/build/withDisplayFormat/WithDisplayFormat.mjs.map +1 -1
- package/package.json +2 -2
- package/src/avatarLayout/AvatarLayout.story.tsx +3 -3
- package/src/avatarView/AvatarView.story.tsx +29 -24
- package/src/avatarView/AvatarView.tsx +1 -1
- package/src/common/bottomSheet/BottomSheet.test.story.tsx +98 -0
- package/src/common/locale/index.test.ts +36 -1
- package/src/common/locale/index.ts +13 -0
- package/src/expressiveMoneyInput/currencySelector/CurrencySelector.tsx +5 -1
- package/src/index.ts +3 -1
- package/src/inputs/Input.tsx +8 -9
- package/src/inputs/SearchInput.tsx +8 -9
- package/src/inputs/SelectInput.test.story.tsx +86 -0
- package/src/inputs/SelectInput.tsx +1 -1
- package/src/inputs/TextArea.tsx +6 -7
- package/src/listItem/ListItem.tsx +2 -2
- package/src/main.css +31 -0
- package/src/main.less +2 -1
- package/src/moneyInput/MoneyInput.test.story.tsx +104 -0
- package/src/moneyInput/MoneyInput.tsx +20 -2
- package/src/primitives/PrimitiveAnchor/PrimitiveAnchor.types.ts +1 -3
- package/src/primitives/PrimitiveButton/PrimitiveButton.types.ts +1 -3
- package/src/prompt/ActionPrompt/ActionPrompt.accessibility.docs.mdx +65 -0
- package/src/prompt/ActionPrompt/ActionPrompt.less +1 -1
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +4 -1
- package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +147 -0
- package/src/prompt/ActionPrompt/ActionPrompt.test.tsx +2 -7
- package/src/prompt/ActionPrompt/ActionPrompt.tsx +48 -7
- package/src/prompt/InfoPrompt/InfoPrompt.css +31 -0
- package/src/prompt/InfoPrompt/InfoPrompt.less +37 -0
- package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +312 -0
- package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +246 -0
- package/src/prompt/InfoPrompt/InfoPrompt.test.tsx +224 -0
- package/src/prompt/InfoPrompt/InfoPrompt.tsx +148 -0
- package/src/prompt/InfoPrompt/index.ts +2 -0
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.less +1 -1
- package/src/prompt/PrimitivePrompt/PrimitivePrompt.tsx +5 -5
- package/src/prompt/index.ts +5 -0
- package/src/radioOption/RadioOption.tsx +2 -1
- package/src/slidingPanel/SlidingPanel.tsx +4 -2
- package/src/ssr.test.tsx +2 -0
- package/src/statusIcon/StatusIcon.tsx +8 -1
- package/src/table/TableCell.tsx +1 -3
- package/src/withDisplayFormat/WithDisplayFormat.tsx +13 -14
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { useState, MouseEvent } from 'react';
|
|
2
|
+
import { userEvent, within, expect, waitFor } from 'storybook/test';
|
|
3
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
4
|
+
import { InfoPrompt, type InfoPromptProps } from './InfoPrompt';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Prompts/InfoPrompt/Tests',
|
|
8
|
+
component: InfoPrompt,
|
|
9
|
+
tags: ['!autodocs', '!manifest', 'new'],
|
|
10
|
+
args: {
|
|
11
|
+
sentiment: 'neutral',
|
|
12
|
+
description: 'This is an informational message.',
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof InfoPrompt>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
const wait = async (duration = 500) =>
|
|
21
|
+
new Promise<void>((resolve) => {
|
|
22
|
+
setTimeout(resolve, duration);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Test that clicking the dismiss button removes the prompt.
|
|
27
|
+
*/
|
|
28
|
+
export const DismissInteraction: Story = {
|
|
29
|
+
play: async ({ canvasElement, step }) => {
|
|
30
|
+
const canvas = within(canvasElement);
|
|
31
|
+
const dismissButton = canvas.getByRole('button', { name: /close/i });
|
|
32
|
+
|
|
33
|
+
await step('Verify prompt is visible', async () => {
|
|
34
|
+
await waitFor(async () =>
|
|
35
|
+
expect(canvas.getByText('This message can be dismissed.')).toBeInTheDocument(),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await step('Click the dismiss button', async () => {
|
|
40
|
+
await userEvent.click(dismissButton);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await step('Verify prompt is removed', async () => {
|
|
44
|
+
await waitFor(async () =>
|
|
45
|
+
expect(canvas.queryByText('This message can be dismissed.')).not.toBeInTheDocument(),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
render: function Render(args: InfoPromptProps) {
|
|
50
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
51
|
+
|
|
52
|
+
if (!isVisible) {
|
|
53
|
+
return <div data-testid="dismissed">Prompt dismissed</div>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<InfoPrompt
|
|
58
|
+
{...args}
|
|
59
|
+
description="This message can be dismissed."
|
|
60
|
+
onDismiss={() => setIsVisible(false)}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Test keyboard accessibility - dismiss prompt via Enter key.
|
|
68
|
+
*/
|
|
69
|
+
export const DismissViaKeyboard: Story = {
|
|
70
|
+
play: async ({ canvasElement, step }) => {
|
|
71
|
+
const canvas = within(canvasElement);
|
|
72
|
+
|
|
73
|
+
await step('Tab to the dismiss button', async () => {
|
|
74
|
+
await wait();
|
|
75
|
+
await userEvent.tab();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await step('Press Enter to dismiss', async () => {
|
|
79
|
+
await userEvent.keyboard('{Enter}');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await step('Verify prompt is removed', async () => {
|
|
83
|
+
await waitFor(async () =>
|
|
84
|
+
expect(canvas.queryByText('Press Enter to dismiss.')).not.toBeInTheDocument(),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
render: function Render(args: InfoPromptProps) {
|
|
89
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
90
|
+
|
|
91
|
+
if (!isVisible) {
|
|
92
|
+
return <div data-testid="dismissed">Prompt dismissed</div>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<InfoPrompt
|
|
97
|
+
{...args}
|
|
98
|
+
description="Press Enter to dismiss."
|
|
99
|
+
onDismiss={() => setIsVisible(false)}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Test action link click interaction.
|
|
107
|
+
*/
|
|
108
|
+
export const ActionClickInteraction: Story = {
|
|
109
|
+
play: async ({ canvasElement, step }) => {
|
|
110
|
+
const canvas = within(canvasElement);
|
|
111
|
+
const actionLink = canvas.getByRole('link', { name: 'Learn more' });
|
|
112
|
+
|
|
113
|
+
await step('Verify prompt with action is visible', async () => {
|
|
114
|
+
await waitFor(async () =>
|
|
115
|
+
expect(canvas.getByText('Click the action link.')).toBeInTheDocument(),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await step('Click the action link', async () => {
|
|
120
|
+
await userEvent.click(actionLink);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await step('Verify action was triggered', async () => {
|
|
124
|
+
await waitFor(async () => expect(canvas.getByText('Action clicked!')).toBeInTheDocument());
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
render: function Render(args: InfoPromptProps) {
|
|
128
|
+
const [actionClicked, setActionClicked] = useState(false);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<InfoPrompt
|
|
133
|
+
{...args}
|
|
134
|
+
description="Click the action link."
|
|
135
|
+
action={{
|
|
136
|
+
label: 'Learn more',
|
|
137
|
+
href: '#',
|
|
138
|
+
onClick: (e: MouseEvent) => {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
setActionClicked(true);
|
|
141
|
+
},
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
{actionClicked && <div>Action clicked!</div>}
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Test multiple prompts with different sentiments can be dismissed independently.
|
|
152
|
+
*/
|
|
153
|
+
export const MultipleDismissInteraction: Story = {
|
|
154
|
+
play: async ({ canvasElement, step }) => {
|
|
155
|
+
const canvas = within(canvasElement);
|
|
156
|
+
const dismissButtons = canvas.getAllByRole('button', { name: /close/i });
|
|
157
|
+
|
|
158
|
+
await step('Verify all prompts are visible', async () => {
|
|
159
|
+
await waitFor(async () => {
|
|
160
|
+
await expect(canvas.getByText('Success message')).toBeInTheDocument();
|
|
161
|
+
await expect(canvas.getByText('Warning message')).toBeInTheDocument();
|
|
162
|
+
await expect(canvas.getByText('Negative message')).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await step('Dismiss the warning prompt', async () => {
|
|
167
|
+
// Click the second dismiss button (warning)
|
|
168
|
+
await userEvent.click(dismissButtons[1]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await step('Verify only warning prompt is removed', async () => {
|
|
172
|
+
await waitFor(async () => {
|
|
173
|
+
await expect(canvas.getByText('Success message')).toBeInTheDocument();
|
|
174
|
+
await expect(canvas.queryByText('Warning message')).not.toBeInTheDocument();
|
|
175
|
+
await expect(canvas.getByText('Negative message')).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
render: function Render() {
|
|
180
|
+
const [prompts, setPrompts] = useState([
|
|
181
|
+
{ id: 1, sentiment: 'success' as const, description: 'Success message' },
|
|
182
|
+
{ id: 2, sentiment: 'warning' as const, description: 'Warning message' },
|
|
183
|
+
{ id: 3, sentiment: 'negative' as const, description: 'Negative message' },
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const handleDismiss = (id: number) => {
|
|
187
|
+
setPrompts((prev) => prev.filter((p) => p.id !== id));
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
192
|
+
{prompts.map((prompt) => (
|
|
193
|
+
<InfoPrompt
|
|
194
|
+
key={prompt.id}
|
|
195
|
+
sentiment={prompt.sentiment}
|
|
196
|
+
description={prompt.description}
|
|
197
|
+
onDismiss={() => handleDismiss(prompt.id)}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Test that touch interactions work for navigation (mobile).
|
|
207
|
+
*/
|
|
208
|
+
export const TouchInteraction: Story = {
|
|
209
|
+
play: async ({ canvasElement, step }) => {
|
|
210
|
+
const canvas = within(canvasElement);
|
|
211
|
+
const actionLink = canvas.getByRole('link', { name: 'Navigate' });
|
|
212
|
+
|
|
213
|
+
await step('Verify prompt with action is visible', async () => {
|
|
214
|
+
await waitFor(async () => expect(canvas.getByText('Tap the prompt.')).toBeInTheDocument());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await step('Click the action (simulating touch)', async () => {
|
|
218
|
+
await userEvent.click(actionLink);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await step('Verify navigation was triggered', async () => {
|
|
222
|
+
await waitFor(async () => expect(canvas.getByText('Navigated!')).toBeInTheDocument());
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
render: function Render(args: InfoPromptProps) {
|
|
226
|
+
const [navigated, setNavigated] = useState(false);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<>
|
|
230
|
+
<InfoPrompt
|
|
231
|
+
{...args}
|
|
232
|
+
description="Tap the prompt."
|
|
233
|
+
action={{
|
|
234
|
+
label: 'Navigate',
|
|
235
|
+
href: '#',
|
|
236
|
+
onClick: (e: MouseEvent) => {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
setNavigated(true);
|
|
239
|
+
},
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
{navigated && <div>Navigated!</div>}
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { fireEvent, mockMatchMedia, render, screen, userEvent } from '../../test-utils';
|
|
2
|
+
import { InfoPrompt, InfoPromptProps } from './InfoPrompt';
|
|
3
|
+
|
|
4
|
+
mockMatchMedia();
|
|
5
|
+
|
|
6
|
+
const CUSTOM_MEDIA = <span data-testid="custom-media">Custom Media</span>;
|
|
7
|
+
|
|
8
|
+
describe('InfoPrompt', () => {
|
|
9
|
+
const defaultProps: InfoPromptProps = {
|
|
10
|
+
description: 'Prompt description',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('renders description', () => {
|
|
14
|
+
render(<InfoPrompt {...defaultProps} />);
|
|
15
|
+
expect(screen.getByText('Prompt description')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders title when provided', () => {
|
|
19
|
+
render(<InfoPrompt {...defaultProps} title="Test Title" />);
|
|
20
|
+
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('Prompt description')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders with GiftBox icon as default for `proposition` sentiment', () => {
|
|
25
|
+
render(<InfoPrompt {...defaultProps} sentiment="proposition" data-testid="info-prompt" />);
|
|
26
|
+
expect(screen.getByText('Prompt description')).toBeInTheDocument();
|
|
27
|
+
// GiftBox icon should be rendered for proposition sentiment
|
|
28
|
+
const prompt = screen.getByTestId('info-prompt');
|
|
29
|
+
expect(prompt.querySelector('svg')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('sentiments', () => {
|
|
33
|
+
[
|
|
34
|
+
{ sentiment: 'negative' as const, surfaceClass: 'wds-prompt--negative' },
|
|
35
|
+
{ sentiment: 'warning' as const, surfaceClass: 'wds-prompt--warning' },
|
|
36
|
+
{ sentiment: 'neutral' as const, surfaceClass: 'wds-prompt--neutral' },
|
|
37
|
+
{ sentiment: 'success' as const, surfaceClass: 'wds-prompt--success' },
|
|
38
|
+
{ sentiment: 'proposition' as const, surfaceClass: 'wds-prompt--proposition' },
|
|
39
|
+
].forEach(({ sentiment, surfaceClass }) => {
|
|
40
|
+
describe(sentiment, () => {
|
|
41
|
+
it('should apply correct styles', () => {
|
|
42
|
+
render(<InfoPrompt {...defaultProps} sentiment={sentiment} data-testid="info-prompt" />);
|
|
43
|
+
expect(screen.getByTestId('info-prompt')).toHaveClass(surfaceClass);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('custom media', () => {
|
|
50
|
+
it('should render custom media when provided', () => {
|
|
51
|
+
render(<InfoPrompt {...defaultProps} media={{ asset: CUSTOM_MEDIA }} />);
|
|
52
|
+
expect(screen.getByTestId('custom-media')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should render custom media for any sentiment', () => {
|
|
56
|
+
render(<InfoPrompt {...defaultProps} sentiment="neutral" media={{ asset: CUSTOM_MEDIA }} />);
|
|
57
|
+
expect(screen.getByTestId('custom-media')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('action', () => {
|
|
62
|
+
it('should render action link when action is provided with href', () => {
|
|
63
|
+
render(
|
|
64
|
+
<InfoPrompt {...defaultProps} action={{ label: 'Learn more', href: '/learn-more' }} />,
|
|
65
|
+
);
|
|
66
|
+
const actionLink = screen.getByRole('link', { name: 'Learn more' });
|
|
67
|
+
expect(actionLink).toBeInTheDocument();
|
|
68
|
+
expect(actionLink).toHaveAttribute('href', '/learn-more');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should render action link with target when provided', () => {
|
|
72
|
+
render(
|
|
73
|
+
<InfoPrompt
|
|
74
|
+
{...defaultProps}
|
|
75
|
+
action={{ label: 'External link', href: 'https://example.com', target: '_blank' }}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
const actionLink = screen.getByRole('link', { name: /External link/i });
|
|
79
|
+
expect(actionLink).toHaveAttribute('target', '_blank');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should call onClick when action button is clicked', async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const onClick = jest.fn();
|
|
85
|
+
render(<InfoPrompt {...defaultProps} action={{ label: 'Click me', onClick }} />);
|
|
86
|
+
const actionButton = screen.getByRole('button', { name: 'Click me' });
|
|
87
|
+
await user.click(actionButton);
|
|
88
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not render action when not provided', () => {
|
|
92
|
+
render(<InfoPrompt {...defaultProps} />);
|
|
93
|
+
expect(screen.queryByRole('link', { name: 'Learn more' })).not.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('HTML attributes', () => {
|
|
98
|
+
it('applies custom className, id, and data-testid', () => {
|
|
99
|
+
render(
|
|
100
|
+
<InfoPrompt
|
|
101
|
+
{...defaultProps}
|
|
102
|
+
className="custom-class"
|
|
103
|
+
id="custom-id"
|
|
104
|
+
data-testid="custom-test"
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
const el = screen.getByTestId('custom-test');
|
|
108
|
+
expect(el).toHaveClass('custom-class');
|
|
109
|
+
expect(el).toHaveAttribute('id', 'custom-id');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('SentimentSurface integration', () => {
|
|
114
|
+
it('maps success sentiment correctly for SentimentSurface', () => {
|
|
115
|
+
render(<InfoPrompt {...defaultProps} sentiment="success" data-testid="prompt" />);
|
|
116
|
+
const el = screen.getByTestId('prompt');
|
|
117
|
+
expect(el).toHaveClass('wds-prompt--success');
|
|
118
|
+
expect(el).toHaveClass('wds-sentiment-surface');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('passes through other sentiments unchanged', () => {
|
|
122
|
+
render(<InfoPrompt {...defaultProps} sentiment="negative" data-testid="prompt" />);
|
|
123
|
+
const el = screen.getByTestId('prompt');
|
|
124
|
+
expect(el).toHaveClass('wds-prompt--negative');
|
|
125
|
+
expect(el).toHaveClass('wds-sentiment-surface');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('touch interactions', () => {
|
|
130
|
+
const originalLocation = window.location;
|
|
131
|
+
|
|
132
|
+
beforeAll(() => {
|
|
133
|
+
jest.spyOn(window, 'open').mockImplementation();
|
|
134
|
+
Object.defineProperty(window, 'location', {
|
|
135
|
+
configurable: true,
|
|
136
|
+
value: {
|
|
137
|
+
...originalLocation,
|
|
138
|
+
assign: jest.fn(),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
afterEach(() => {
|
|
144
|
+
jest.clearAllMocks();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterAll(() => {
|
|
148
|
+
Object.defineProperty(window, 'location', {
|
|
149
|
+
configurable: true,
|
|
150
|
+
value: originalLocation,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should navigate to action href on touch tap', () => {
|
|
155
|
+
render(
|
|
156
|
+
<InfoPrompt
|
|
157
|
+
{...defaultProps}
|
|
158
|
+
action={{ label: 'Learn more', href: '/learn-more' }}
|
|
159
|
+
data-testid="prompt"
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
const prompt = screen.getByTestId('prompt');
|
|
163
|
+
|
|
164
|
+
fireEvent.touchStart(prompt);
|
|
165
|
+
expect(window.location.assign).not.toHaveBeenCalled();
|
|
166
|
+
fireEvent.touchEnd(prompt);
|
|
167
|
+
expect(window.location.assign).toHaveBeenCalledWith('/learn-more');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should open in new tab when action target is _blank', () => {
|
|
171
|
+
render(
|
|
172
|
+
<InfoPrompt
|
|
173
|
+
{...defaultProps}
|
|
174
|
+
action={{ label: 'External link', href: 'https://example.com', target: '_blank' }}
|
|
175
|
+
data-testid="prompt"
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
const prompt = screen.getByTestId('prompt');
|
|
179
|
+
|
|
180
|
+
fireEvent.touchStart(prompt);
|
|
181
|
+
expect(window.open).not.toHaveBeenCalled();
|
|
182
|
+
fireEvent.touchEnd(prompt);
|
|
183
|
+
expect(window.open).toHaveBeenCalledWith(
|
|
184
|
+
'https://example.com',
|
|
185
|
+
'_blank',
|
|
186
|
+
'noopener, noreferrer',
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should not navigate if touch move occurs (scrolling)', () => {
|
|
191
|
+
render(
|
|
192
|
+
<InfoPrompt
|
|
193
|
+
{...defaultProps}
|
|
194
|
+
action={{ label: 'Learn more', href: '/learn-more' }}
|
|
195
|
+
data-testid="prompt"
|
|
196
|
+
/>,
|
|
197
|
+
);
|
|
198
|
+
const prompt = screen.getByTestId('prompt');
|
|
199
|
+
|
|
200
|
+
fireEvent.touchStart(prompt);
|
|
201
|
+
expect(window.location.assign).not.toHaveBeenCalled();
|
|
202
|
+
fireEvent.touchMove(prompt);
|
|
203
|
+
expect(window.location.assign).not.toHaveBeenCalled();
|
|
204
|
+
fireEvent.touchEnd(prompt);
|
|
205
|
+
expect(window.location.assign).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should not navigate if no action href is provided', () => {
|
|
209
|
+
render(
|
|
210
|
+
<InfoPrompt
|
|
211
|
+
{...defaultProps}
|
|
212
|
+
action={{ label: 'Click me', onClick: jest.fn() }}
|
|
213
|
+
data-testid="prompt"
|
|
214
|
+
/>,
|
|
215
|
+
);
|
|
216
|
+
const prompt = screen.getByTestId('prompt');
|
|
217
|
+
|
|
218
|
+
fireEvent.touchStart(prompt);
|
|
219
|
+
fireEvent.touchEnd(prompt);
|
|
220
|
+
expect(window.location.assign).not.toHaveBeenCalled();
|
|
221
|
+
expect(window.open).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode, useState } from 'react';
|
|
2
|
+
import { Sentiment, Typography } from '../../common';
|
|
3
|
+
import { GiftBox } from '@transferwise/icons';
|
|
4
|
+
import type { Sentiment as SurfaceSentiment } from '../../sentimentSurface';
|
|
5
|
+
import StatusIcon from '../../statusIcon';
|
|
6
|
+
import { clsx } from 'clsx';
|
|
7
|
+
import Body from '../../body';
|
|
8
|
+
import Link, { LinkProps } from '../../link';
|
|
9
|
+
import { PrimitivePrompt, PrimitivePromptProps } from '../PrimitivePrompt';
|
|
10
|
+
|
|
11
|
+
export type InfoPromptAction = Pick<LinkProps, 'href' | 'target' | 'onClick'> & {
|
|
12
|
+
/**
|
|
13
|
+
* The label text for the action link
|
|
14
|
+
*/
|
|
15
|
+
label: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type InfoPromptMedia = {
|
|
19
|
+
/**
|
|
20
|
+
* The icon/image asset to display.
|
|
21
|
+
* The asset should include its own accessibility attributes (e.g. title, aria-label)
|
|
22
|
+
* if it conveys meaning, or aria-hidden="true" if decorative.
|
|
23
|
+
*/
|
|
24
|
+
asset: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type InfoPromptProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> &
|
|
28
|
+
Pick<PrimitivePromptProps, 'data-testid'> & {
|
|
29
|
+
/**
|
|
30
|
+
* The sentiment determines the colour scheme
|
|
31
|
+
* @default 'neutral'
|
|
32
|
+
*/
|
|
33
|
+
sentiment?: SurfaceSentiment;
|
|
34
|
+
/**
|
|
35
|
+
* Handler called when the close button is clicked.
|
|
36
|
+
* If not provided, the close button is hidden.
|
|
37
|
+
*/
|
|
38
|
+
onDismiss?: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* Custom media to override the default status icon.
|
|
41
|
+
* Success and proposition sentiments support 2 status variations: standard checkmark & confetti.
|
|
42
|
+
*/
|
|
43
|
+
media?: InfoPromptMedia;
|
|
44
|
+
/**
|
|
45
|
+
* Action link to be displayed below the description
|
|
46
|
+
*/
|
|
47
|
+
action?: InfoPromptAction;
|
|
48
|
+
/**
|
|
49
|
+
* Title content for the prompt
|
|
50
|
+
*/
|
|
51
|
+
title?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Description text for the prompt (required)
|
|
54
|
+
*/
|
|
55
|
+
description: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* InfoPrompt displays important contextual messages to users within a screen.
|
|
60
|
+
* It provides a visually distinct way to communicate information, warnings, errors,
|
|
61
|
+
* or positive feedback with optional actions and dismissal capabilities.
|
|
62
|
+
*
|
|
63
|
+
* Use this component to replace the deprecated Alert component.
|
|
64
|
+
*/
|
|
65
|
+
export const InfoPrompt = ({
|
|
66
|
+
sentiment = 'neutral',
|
|
67
|
+
onDismiss,
|
|
68
|
+
media,
|
|
69
|
+
action,
|
|
70
|
+
title,
|
|
71
|
+
description,
|
|
72
|
+
className,
|
|
73
|
+
'data-testid': dataTestId,
|
|
74
|
+
...restProps
|
|
75
|
+
}: InfoPromptProps) => {
|
|
76
|
+
const [shouldFire, setShouldFire] = useState<boolean>();
|
|
77
|
+
const statusIconSentiment =
|
|
78
|
+
sentiment === 'success'
|
|
79
|
+
? Sentiment.POSITIVE
|
|
80
|
+
: (sentiment as Exclude<SurfaceSentiment, 'proposition'>);
|
|
81
|
+
|
|
82
|
+
const handleTouchStart = () => {
|
|
83
|
+
setShouldFire(true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleTouchEnd = () => {
|
|
87
|
+
if (shouldFire && action?.href) {
|
|
88
|
+
if (action.target === '_blank') {
|
|
89
|
+
window.top?.open(action.href, '_blank', 'noopener, noreferrer');
|
|
90
|
+
} else {
|
|
91
|
+
window.top?.location.assign(action.href);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
setShouldFire(false);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleTouchMove = () => {
|
|
98
|
+
setShouldFire(false);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const renderMedia = () => {
|
|
102
|
+
if (media) {
|
|
103
|
+
return <span className="wds-info-prompt__media">{media.asset}</span>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (sentiment === 'proposition') {
|
|
107
|
+
return <GiftBox size={24} />;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return <StatusIcon size={24} sentiment={statusIconSentiment} />;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<PrimitivePrompt
|
|
115
|
+
sentiment={sentiment}
|
|
116
|
+
media={renderMedia()}
|
|
117
|
+
data-testid={dataTestId}
|
|
118
|
+
className={clsx('wds-info-prompt', className)}
|
|
119
|
+
{...restProps}
|
|
120
|
+
onTouchStart={handleTouchStart}
|
|
121
|
+
onTouchEnd={handleTouchEnd}
|
|
122
|
+
onTouchMove={handleTouchMove}
|
|
123
|
+
onDismiss={onDismiss}
|
|
124
|
+
>
|
|
125
|
+
<div className="wds-info-prompt__content">
|
|
126
|
+
{title && (
|
|
127
|
+
<Body className="wds-info-prompt__title" type={Typography.BODY_LARGE_BOLD} as="span">
|
|
128
|
+
{title}
|
|
129
|
+
</Body>
|
|
130
|
+
)}
|
|
131
|
+
<Body as="span" className="wds-info-prompt__description">
|
|
132
|
+
{description}
|
|
133
|
+
</Body>
|
|
134
|
+
{action && (
|
|
135
|
+
<Link
|
|
136
|
+
href={action.href}
|
|
137
|
+
target={action.target}
|
|
138
|
+
type={Typography.LINK_DEFAULT}
|
|
139
|
+
className="wds-info-prompt__action"
|
|
140
|
+
onClick={action.onClick}
|
|
141
|
+
>
|
|
142
|
+
{action.label}
|
|
143
|
+
</Link>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
</PrimitivePrompt>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
@@ -4,9 +4,9 @@ import SentimentSurface, { Sentiment } from '../../sentimentSurface';
|
|
|
4
4
|
import IconButton from '../../iconButton';
|
|
5
5
|
import { useIntl } from 'react-intl';
|
|
6
6
|
import closeBtnMessages from '../../common/closeButton/CloseButton.messages';
|
|
7
|
-
import { ReactNode } from 'react';
|
|
7
|
+
import { HTMLAttributes, ReactNode } from 'react';
|
|
8
8
|
|
|
9
|
-
export type PrimitivePromptProps = {
|
|
9
|
+
export type PrimitivePromptProps = HTMLAttributes<HTMLDivElement> & {
|
|
10
10
|
/**
|
|
11
11
|
* The sentiment determines the colour scheme
|
|
12
12
|
* @default success
|
|
@@ -24,10 +24,10 @@ export type PrimitivePromptProps = {
|
|
|
24
24
|
* Handler called when the close button is clicked. If not provided, then the close button is hidden.
|
|
25
25
|
*/
|
|
26
26
|
onDismiss?: () => void;
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Test ID for testing tools
|
|
29
|
+
*/
|
|
29
30
|
'data-testid'?: string;
|
|
30
|
-
children: ReactNode;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
package/src/prompt/index.ts
CHANGED
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
export type { InlinePromptProps } from './InlinePrompt';
|
|
6
6
|
export { InlinePrompt } from './InlinePrompt';
|
|
7
7
|
|
|
8
|
+
// ActionPrompt
|
|
8
9
|
export type { ActionPromptProps } from './ActionPrompt';
|
|
9
10
|
export { ActionPrompt } from './ActionPrompt';
|
|
11
|
+
|
|
12
|
+
// InfoPrompt
|
|
13
|
+
export type { InfoPromptProps, InfoPromptAction, InfoPromptMedia } from './InfoPrompt';
|
|
14
|
+
export { InfoPrompt } from './InfoPrompt';
|
|
@@ -3,7 +3,8 @@ import RadioButton from '../common/RadioButton';
|
|
|
3
3
|
import { RadioButtonProps } from '../common/RadioButton/RadioButton';
|
|
4
4
|
|
|
5
5
|
export interface RadioOptionProps<T extends string | number = string>
|
|
6
|
-
extends
|
|
6
|
+
extends
|
|
7
|
+
Required<Pick<RadioButtonProps<T>, 'id' | 'name' | 'onChange'>>,
|
|
7
8
|
Omit<RadioButtonProps<T>, 'readOnly' | 'id' | 'name' | 'onChange'> {
|
|
8
9
|
'aria-label'?: string;
|
|
9
10
|
media?: React.ReactNode;
|
|
@@ -6,8 +6,10 @@ import { Position } from '../common';
|
|
|
6
6
|
|
|
7
7
|
export const EXIT_ANIMATION = 350;
|
|
8
8
|
|
|
9
|
-
export interface SlidingPanelProps
|
|
10
|
-
|
|
9
|
+
export interface SlidingPanelProps extends Pick<
|
|
10
|
+
React.ComponentPropsWithRef<'div'>,
|
|
11
|
+
'ref' | 'className' | 'children'
|
|
12
|
+
> {
|
|
11
13
|
position?: `${Position.TOP | Position.RIGHT | Position.BOTTOM | Position.LEFT}`;
|
|
12
14
|
open: boolean;
|
|
13
15
|
showSlidingPanelBorder?: boolean;
|