@transferwise/components 46.135.2 → 46.135.3
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/avatarWrapper/AvatarWrapper.js.map +1 -1
- package/build/avatarWrapper/AvatarWrapper.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/package.json +1 -1
- package/src/actionButton/ActionButton.story.tsx +4 -4
- package/src/actionButton/ActionButton.test.story.tsx +4 -4
- package/src/avatarWrapper/AvatarWrapper.tsx +3 -3
- package/src/common/circle/Circle.story.tsx +3 -3
- package/src/iconButton/IconButton.story.tsx +5 -6
- package/src/iconButton/IconButton.test.story.tsx +8 -8
- package/src/icons/Icons.story.tsx +381 -0
- package/src/listItem/ListItem.test.tsx +24 -0
- package/src/listItem/ListItem.tsx +2 -2
- package/src/listItem/_stories/ListItem.context.test.story.tsx +63 -0
- package/src/listItem/_stories/ListItem.scenarios.story.tsx +3 -3
- package/src/moneyInput/MoneyInput.story.tsx +2 -2
- package/src/navigationOption/NavigationOption.story.tsx +3 -3
- package/src/overlayHeader/OverlayHeader.story.tsx +2 -2
- package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +3 -3
- package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +3 -3
- package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +1 -1
- package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +3 -3
- package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +1 -1
- package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +5 -5
- package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +2 -2
- package/src/select/Select.story.tsx +3 -3
- package/src/select/option/Option.test.tsx +3 -3
- package/src/summary/Summary.story.tsx +5 -5
- package/src/summary/Summary.test.story.tsx +2 -2
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import React, { useState, useCallback, memo, useRef } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
import * as AllIcons from '@transferwise/icons';
|
|
4
|
+
import iconsPkg from '@transferwise/icons/package.json';
|
|
5
|
+
import { ThemeProvider } from '@wise/components-theming';
|
|
6
|
+
|
|
7
|
+
import Chips, { type ChipValue } from '../chips/Chips';
|
|
8
|
+
import { SearchInput } from '../inputs/SearchInput';
|
|
9
|
+
import SnackbarProvider from '../snackbar/SnackbarProvider';
|
|
10
|
+
import useSnackbar from '../snackbar/useSnackbar';
|
|
11
|
+
|
|
12
|
+
const ICONS_VERSION: string = iconsPkg.version;
|
|
13
|
+
|
|
14
|
+
type IconComponent = React.FunctionComponent<{
|
|
15
|
+
size?: '16' | '24' | '32' | 16 | 24 | 32;
|
|
16
|
+
className?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
const ALIAS_TO_CANONICAL: Record<string, string> = {
|
|
21
|
+
Investments: 'Balance',
|
|
22
|
+
Holidays: 'Beach',
|
|
23
|
+
Rent: 'Building',
|
|
24
|
+
Insights: 'Bulb',
|
|
25
|
+
CardTransferwise: 'CardWise',
|
|
26
|
+
SalesAndRoyalties: 'CashRegister',
|
|
27
|
+
Settings: 'Cog',
|
|
28
|
+
Boxes: 'CostOfGoodsSold',
|
|
29
|
+
Car: 'DriversLicense',
|
|
30
|
+
Invite: 'GiftBox',
|
|
31
|
+
ContractServices: 'Handshake',
|
|
32
|
+
Do: 'HappyEmoji',
|
|
33
|
+
Emoji: 'HappyEmoji',
|
|
34
|
+
Cs: 'Headset',
|
|
35
|
+
Home: 'House',
|
|
36
|
+
Picture: 'Image',
|
|
37
|
+
Atm: 'InsertCard',
|
|
38
|
+
Activity: 'List',
|
|
39
|
+
TwoStep: 'MobileLock',
|
|
40
|
+
Unlock: 'PadlockUnlocked',
|
|
41
|
+
Lock: 'Padlock',
|
|
42
|
+
Salary: 'PayIn',
|
|
43
|
+
Recipients: 'People',
|
|
44
|
+
Tax: 'PercentageCircle',
|
|
45
|
+
Profile: 'Person',
|
|
46
|
+
Expenses: 'PieChart',
|
|
47
|
+
Help: 'QuestionMark',
|
|
48
|
+
HelpCircle: 'QuestionMarkCircle',
|
|
49
|
+
Refresh: 'Reload',
|
|
50
|
+
Dont: 'SadEmoji',
|
|
51
|
+
ECommerce: 'ShoppingBag',
|
|
52
|
+
Chat: 'SpeechBubble',
|
|
53
|
+
Pending: 'SpeechBubblePending',
|
|
54
|
+
Feedback: 'SpeechBubbleMessage',
|
|
55
|
+
Comments: 'SpeechBubbles',
|
|
56
|
+
OfficeExpenses: 'Stationery',
|
|
57
|
+
Travel: 'Suitcase',
|
|
58
|
+
Marketing: 'Target',
|
|
59
|
+
ExchangeRate: 'UpwardGraph',
|
|
60
|
+
OwnersWithdrawal: 'Withdrawal',
|
|
61
|
+
Family: 'Heart',
|
|
62
|
+
CalendarSuccess: 'CalendarCheck',
|
|
63
|
+
Dial: 'PinCode',
|
|
64
|
+
PendingCircle: 'Clock',
|
|
65
|
+
Verified: 'Check',
|
|
66
|
+
SoftwareAndWebHosting: 'SoftwareAndHosting',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const CANONICAL_TO_ALIASES: Record<string, string[]> = {};
|
|
70
|
+
for (const [alias, canonical] of Object.entries(ALIAS_TO_CANONICAL)) {
|
|
71
|
+
if (!CANONICAL_TO_ALIASES[canonical]) {
|
|
72
|
+
CANONICAL_TO_ALIASES[canonical] = [];
|
|
73
|
+
}
|
|
74
|
+
CANONICAL_TO_ALIASES[canonical].push(alias);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type IconEntry = {
|
|
78
|
+
name: string;
|
|
79
|
+
component: IconComponent;
|
|
80
|
+
aliases: string[];
|
|
81
|
+
searchKey: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const iconEntries: IconEntry[] = Object.entries(AllIcons as Record<string, IconComponent>)
|
|
85
|
+
.filter(([name]) => !ALIAS_TO_CANONICAL[name])
|
|
86
|
+
.map(([name, component]) => {
|
|
87
|
+
const aliases = CANONICAL_TO_ALIASES[name] ?? [];
|
|
88
|
+
return {
|
|
89
|
+
name,
|
|
90
|
+
component,
|
|
91
|
+
aliases,
|
|
92
|
+
searchKey: [name, ...aliases].join(' ').toLowerCase(),
|
|
93
|
+
};
|
|
94
|
+
})
|
|
95
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
|
|
97
|
+
const sizeChips = [
|
|
98
|
+
{ value: 16, label: '16' },
|
|
99
|
+
{ value: 24, label: '24' },
|
|
100
|
+
{ value: 32, label: '32' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const themeChips = [
|
|
104
|
+
{ value: 'personal', label: 'Personal' },
|
|
105
|
+
{ value: 'personal-dark', label: 'Dark' },
|
|
106
|
+
{ value: 'bright-green', label: 'Bright Green' },
|
|
107
|
+
{ value: 'forest-green', label: 'Forest Green' },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
type ThemeSelection = {
|
|
111
|
+
theme: 'personal' | 'bright-green' | 'forest-green';
|
|
112
|
+
screenMode: 'light' | 'dark';
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function resolveTheme(value: string): ThemeSelection {
|
|
116
|
+
if (value === 'personal-dark') return { theme: 'personal', screenMode: 'dark' };
|
|
117
|
+
return { theme: value as ThemeSelection['theme'], screenMode: 'light' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const IconCell = memo(function IconCell({
|
|
121
|
+
name,
|
|
122
|
+
component: Icon,
|
|
123
|
+
aliases,
|
|
124
|
+
iconSize,
|
|
125
|
+
onCopy,
|
|
126
|
+
}: {
|
|
127
|
+
name: string;
|
|
128
|
+
component: IconComponent;
|
|
129
|
+
aliases: string[];
|
|
130
|
+
iconSize: 16 | 24 | 32;
|
|
131
|
+
onCopy: (name: string) => void;
|
|
132
|
+
}) {
|
|
133
|
+
const [hovered, setHovered] = useState(false);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
style={{
|
|
138
|
+
display: 'flex',
|
|
139
|
+
flexDirection: 'column',
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
gap: '4px',
|
|
142
|
+
padding: '12px 4px',
|
|
143
|
+
borderRadius: '8px',
|
|
144
|
+
cursor: 'pointer',
|
|
145
|
+
flex: 1,
|
|
146
|
+
background: hovered ? 'var(--color-background-neutral)' : 'transparent',
|
|
147
|
+
transition: 'background 150ms ease',
|
|
148
|
+
}}
|
|
149
|
+
role="button"
|
|
150
|
+
tabIndex={0}
|
|
151
|
+
aria-label={`Copy import for ${name}`}
|
|
152
|
+
onClick={() => onCopy(name)}
|
|
153
|
+
onMouseEnter={() => setHovered(true)}
|
|
154
|
+
onMouseLeave={() => setHovered(false)}
|
|
155
|
+
onKeyDown={(e) => {
|
|
156
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
onCopy(name);
|
|
159
|
+
}
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<div style={{ padding: '8px' }}>
|
|
163
|
+
<Icon size={iconSize} />
|
|
164
|
+
</div>
|
|
165
|
+
<span
|
|
166
|
+
style={{
|
|
167
|
+
textAlign: 'center',
|
|
168
|
+
wordBreak: 'break-word',
|
|
169
|
+
fontSize: '12px',
|
|
170
|
+
lineHeight: '1.3',
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
{name}
|
|
174
|
+
</span>
|
|
175
|
+
{aliases.length > 0 && (
|
|
176
|
+
<span
|
|
177
|
+
style={{
|
|
178
|
+
textAlign: 'center',
|
|
179
|
+
wordBreak: 'break-word',
|
|
180
|
+
fontSize: '11px',
|
|
181
|
+
lineHeight: '1.3',
|
|
182
|
+
color: 'var(--color-content-tertiary)',
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
alias: {aliases.join(', ')}
|
|
186
|
+
</span>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function IconGallery() {
|
|
193
|
+
const [size, setSize] = useState<ChipValue>(32);
|
|
194
|
+
const [theme, setTheme] = useState<ChipValue>('personal');
|
|
195
|
+
const resolved = resolveTheme(String(theme));
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<SnackbarProvider timeout={2000}>
|
|
199
|
+
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
|
200
|
+
<Toolbar size={size} theme={theme} onSizeChange={setSize} onThemeChange={setTheme} />
|
|
201
|
+
<ThemeProvider theme={resolved.theme} screenMode={resolved.screenMode}>
|
|
202
|
+
<IconGrid size={size} screenMode={resolved.screenMode} />
|
|
203
|
+
</ThemeProvider>
|
|
204
|
+
</div>
|
|
205
|
+
</SnackbarProvider>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function Toolbar({
|
|
210
|
+
size,
|
|
211
|
+
onSizeChange,
|
|
212
|
+
theme,
|
|
213
|
+
onThemeChange,
|
|
214
|
+
}: {
|
|
215
|
+
size: ChipValue;
|
|
216
|
+
onSizeChange: (v: ChipValue) => void;
|
|
217
|
+
theme: ChipValue;
|
|
218
|
+
onThemeChange: (v: ChipValue) => void;
|
|
219
|
+
}) {
|
|
220
|
+
return (
|
|
221
|
+
<div
|
|
222
|
+
style={{
|
|
223
|
+
display: 'flex',
|
|
224
|
+
flexWrap: 'wrap',
|
|
225
|
+
alignItems: 'center',
|
|
226
|
+
gap: '12px',
|
|
227
|
+
padding: '12px 24px',
|
|
228
|
+
borderBottom: '1px solid var(--color-border-neutral)',
|
|
229
|
+
background: 'var(--color-background-elevated)',
|
|
230
|
+
flexShrink: 0,
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', paddingRight: '12px' }}>
|
|
234
|
+
<span style={{ fontSize: '13px', fontWeight: 500, whiteSpace: 'nowrap' }}>Size</span>
|
|
235
|
+
<Chips
|
|
236
|
+
chips={sizeChips}
|
|
237
|
+
selected={size}
|
|
238
|
+
aria-label="Icon size"
|
|
239
|
+
onChange={({ selectedValue }) => onSizeChange(selectedValue)}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
243
|
+
<span style={{ fontSize: '13px', fontWeight: 500, whiteSpace: 'nowrap' }}>Theme</span>
|
|
244
|
+
<Chips
|
|
245
|
+
chips={themeChips}
|
|
246
|
+
selected={theme}
|
|
247
|
+
aria-label="Theme"
|
|
248
|
+
onChange={({ selectedValue }) => onThemeChange(selectedValue)}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function IconGrid({ size, screenMode }: { size: ChipValue; screenMode: 'light' | 'dark' }) {
|
|
256
|
+
const [search, setSearch] = useState('');
|
|
257
|
+
const gridRef = useRef<HTMLDivElement>(null);
|
|
258
|
+
const countRef = useRef<HTMLSpanElement>(null);
|
|
259
|
+
const createSnackbar = useSnackbar();
|
|
260
|
+
|
|
261
|
+
const iconSize = Number(size) as 16 | 24 | 32;
|
|
262
|
+
|
|
263
|
+
const handleCopy = useCallback(
|
|
264
|
+
async (name: string) => {
|
|
265
|
+
try {
|
|
266
|
+
await navigator.clipboard.writeText(`<${name} />`);
|
|
267
|
+
createSnackbar({
|
|
268
|
+
text: `Copied`,
|
|
269
|
+
theme: screenMode,
|
|
270
|
+
});
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('Failed to copy icon import to clipboard', error);
|
|
273
|
+
createSnackbar({
|
|
274
|
+
text: `Failed to copy`,
|
|
275
|
+
theme: screenMode,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[createSnackbar, screenMode],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const applyFilter = useCallback((query: string) => {
|
|
283
|
+
const grid = gridRef.current;
|
|
284
|
+
if (!grid) return;
|
|
285
|
+
const q = query.toLowerCase();
|
|
286
|
+
let visible = 0;
|
|
287
|
+
const children = grid.children as HTMLCollectionOf<HTMLElement>;
|
|
288
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
289
|
+
const el = children[i];
|
|
290
|
+
const match = !q || (el.dataset.search?.includes(q) ?? false);
|
|
291
|
+
el.style.display = match ? '' : 'none';
|
|
292
|
+
if (match) visible += 1;
|
|
293
|
+
}
|
|
294
|
+
if (countRef.current) {
|
|
295
|
+
countRef.current.textContent = `${visible} icon${visible !== 1 ? 's' : ''}`;
|
|
296
|
+
}
|
|
297
|
+
}, []);
|
|
298
|
+
|
|
299
|
+
const handleSearchChange = useCallback(
|
|
300
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
301
|
+
const val = e.target.value;
|
|
302
|
+
setSearch(val);
|
|
303
|
+
applyFilter(val);
|
|
304
|
+
},
|
|
305
|
+
[applyFilter],
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div
|
|
310
|
+
style={{
|
|
311
|
+
display: 'flex',
|
|
312
|
+
flexDirection: 'column',
|
|
313
|
+
gap: '16px',
|
|
314
|
+
padding: '24px',
|
|
315
|
+
background: 'var(--color-background-screen)',
|
|
316
|
+
color: 'var(--color-content-primary)',
|
|
317
|
+
flex: 1,
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap' }}>
|
|
321
|
+
<div style={{ maxWidth: '320px', flex: 1 }}>
|
|
322
|
+
<SearchInput
|
|
323
|
+
placeholder="Search icons..."
|
|
324
|
+
value={search}
|
|
325
|
+
aria-label="Search icons"
|
|
326
|
+
onChange={handleSearchChange}
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
<span ref={countRef} style={{ fontSize: '13px', color: 'var(--color-content-secondary)' }}>
|
|
330
|
+
{iconEntries.length} icons
|
|
331
|
+
</span>
|
|
332
|
+
<code>@transferwise/icons@{ICONS_VERSION}</code>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div style={{ fontSize: '11px', color: 'var(--color-content-tertiary)', lineHeight: '1.8' }}>
|
|
336
|
+
<div>
|
|
337
|
+
Icons package must be imported first:{' '}
|
|
338
|
+
<code>{`import { Bank } from '@transferwise/icons'`}</code>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div
|
|
343
|
+
ref={gridRef}
|
|
344
|
+
style={{
|
|
345
|
+
display: 'grid',
|
|
346
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
347
|
+
gap: '12px',
|
|
348
|
+
gridAutoRows: '1fr',
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
{iconEntries.map(({ name, component, aliases, searchKey }) => (
|
|
352
|
+
<div key={name} data-search={searchKey} style={{ display: 'flex' }}>
|
|
353
|
+
<IconCell
|
|
354
|
+
name={name}
|
|
355
|
+
component={component}
|
|
356
|
+
aliases={aliases}
|
|
357
|
+
iconSize={iconSize}
|
|
358
|
+
onCopy={handleCopy}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
))}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const meta: Meta = {
|
|
368
|
+
title: 'Foundations/Icons',
|
|
369
|
+
tags: ['!autodocs', '!manifest'],
|
|
370
|
+
parameters: {
|
|
371
|
+
padding: '0',
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export default meta;
|
|
376
|
+
|
|
377
|
+
type Story = StoryObj;
|
|
378
|
+
|
|
379
|
+
export const Gallery: Story = {
|
|
380
|
+
render: () => <IconGallery />,
|
|
381
|
+
};
|
|
@@ -1541,5 +1541,29 @@ describe('ListItem', () => {
|
|
|
1541
1541
|
).toHaveAttribute('id', expect.stringMatching(/_prompt$/));
|
|
1542
1542
|
});
|
|
1543
1543
|
});
|
|
1544
|
+
|
|
1545
|
+
describe('dynamic context updates', () => {
|
|
1546
|
+
it('should update Switch disabled state when ListItem disabled prop changes', () => {
|
|
1547
|
+
const { rerender } = render(
|
|
1548
|
+
<ListItem title={title} disabled={false} control={<ListItem.Switch onClick={cb} />} />,
|
|
1549
|
+
);
|
|
1550
|
+
|
|
1551
|
+
const switchControl = screen.getByRole('switch');
|
|
1552
|
+
expect(switchControl).toBeEnabled();
|
|
1553
|
+
expect(screen.getByRole('listitem')).not.toHaveAttribute('aria-disabled', 'true');
|
|
1554
|
+
|
|
1555
|
+
rerender(<ListItem title={title} disabled control={<ListItem.Switch onClick={cb} />} />);
|
|
1556
|
+
|
|
1557
|
+
expect(switchControl).toBeDisabled();
|
|
1558
|
+
expect(screen.getByRole('listitem')).toHaveAttribute('aria-disabled', 'true');
|
|
1559
|
+
|
|
1560
|
+
rerender(
|
|
1561
|
+
<ListItem title={title} disabled={false} control={<ListItem.Switch onClick={cb} />} />,
|
|
1562
|
+
);
|
|
1563
|
+
|
|
1564
|
+
expect(switchControl).toBeEnabled();
|
|
1565
|
+
expect(screen.getByRole('listitem')).not.toHaveAttribute('aria-disabled', 'true');
|
|
1566
|
+
});
|
|
1567
|
+
});
|
|
1544
1568
|
});
|
|
1545
1569
|
});
|
|
@@ -163,7 +163,7 @@ export const ListItem = ({
|
|
|
163
163
|
return isFullyInteractive && !isButtonAsLink
|
|
164
164
|
? additionalInfoPrompt
|
|
165
165
|
: `${titlesAndValues} ${additionalInfoPrompt}`;
|
|
166
|
-
}, [isFullyInteractive]);
|
|
166
|
+
}, [additionalInfoPrompt, isButtonAsLink, isFullyInteractive, titlesAndValues]);
|
|
167
167
|
const listItemContext = useMemo(
|
|
168
168
|
() => ({
|
|
169
169
|
setControlType,
|
|
@@ -175,7 +175,7 @@ export const ListItem = ({
|
|
|
175
175
|
isPartiallyInteractive,
|
|
176
176
|
describedByIds,
|
|
177
177
|
}),
|
|
178
|
-
[describedByIds, mediaSize],
|
|
178
|
+
[describedByIds, mediaSize, disabled, inverted, disabledPromptMessage, isPartiallyInteractive],
|
|
179
179
|
);
|
|
180
180
|
const gridColumnsStyle = {
|
|
181
181
|
'--wds-list-item-body-left': valueColumnWidth ? `${100 - valueColumnWidth}fr` : '50fr',
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
4
|
+
import { MultiCurrency } from '@transferwise/icons';
|
|
5
|
+
import List from '../../list';
|
|
6
|
+
import { ListItem, type ListItemProps } from '../ListItem';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
component: ListItem,
|
|
10
|
+
title: 'Content/ListItem/Tests/Context Updates',
|
|
11
|
+
tags: ['!autodocs', '!manifest'],
|
|
12
|
+
parameters: {
|
|
13
|
+
controls: { disable: true },
|
|
14
|
+
actions: { disable: true },
|
|
15
|
+
knobs: { disable: true },
|
|
16
|
+
},
|
|
17
|
+
} satisfies Meta<ListItemProps>;
|
|
18
|
+
|
|
19
|
+
type Story = StoryObj<ListItemProps>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Test: Dynamic state updates should propagate through listItemContext to control subcomponents.
|
|
23
|
+
* The Switch control toggles the disabled state of the ListItem.
|
|
24
|
+
* When disabled changes, the Switch should correctly update its disabled state via the context.
|
|
25
|
+
*/
|
|
26
|
+
export const DisabledToggle: Story = {
|
|
27
|
+
render: () => {
|
|
28
|
+
const [disabled, setDisabled] = useState(false);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<List>
|
|
32
|
+
<ListItem
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
title="Payment notifications"
|
|
35
|
+
subtitle={`ListItem disabled: ${disabled ? 'true' : 'false'}`}
|
|
36
|
+
media={
|
|
37
|
+
<ListItem.AvatarView>
|
|
38
|
+
<MultiCurrency />
|
|
39
|
+
</ListItem.AvatarView>
|
|
40
|
+
}
|
|
41
|
+
control={<ListItem.Switch checked={!disabled} onClick={() => setDisabled(!disabled)} />}
|
|
42
|
+
/>
|
|
43
|
+
</List>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
play: async ({ canvasElement }) => {
|
|
47
|
+
const canvas = within(canvasElement);
|
|
48
|
+
const switchControl = canvas.getByRole('switch');
|
|
49
|
+
|
|
50
|
+
// Initial state: disabled should be false, switch should be enabled and checked
|
|
51
|
+
await expect(switchControl).toBeEnabled();
|
|
52
|
+
await expect(switchControl).toBeChecked();
|
|
53
|
+
await expect(canvas.getByRole('listitem')).not.toHaveAttribute('aria-disabled', 'true');
|
|
54
|
+
|
|
55
|
+
// Click switch to set disabled to true
|
|
56
|
+
await userEvent.click(switchControl);
|
|
57
|
+
|
|
58
|
+
// Verify switch is now disabled and unchecked
|
|
59
|
+
await expect(switchControl).toBeDisabled();
|
|
60
|
+
await expect(switchControl).not.toBeChecked();
|
|
61
|
+
await expect(canvas.getByRole('listitem')).toHaveAttribute('aria-disabled', 'true');
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
InfoCircle,
|
|
7
7
|
Documents,
|
|
8
8
|
Warning,
|
|
9
|
-
|
|
9
|
+
House,
|
|
10
10
|
Globe,
|
|
11
11
|
People,
|
|
12
12
|
Link as LinkIcon,
|
|
@@ -273,7 +273,7 @@ export const Summary: Story = storySourceWithoutNoise({
|
|
|
273
273
|
subtitle="Update your payment address"
|
|
274
274
|
media={
|
|
275
275
|
<ListItem.AvatarView size={32}>
|
|
276
|
-
<
|
|
276
|
+
<House />
|
|
277
277
|
</ListItem.AvatarView>
|
|
278
278
|
}
|
|
279
279
|
additionalInfo={
|
|
@@ -287,7 +287,7 @@ export const Summary: Story = storySourceWithoutNoise({
|
|
|
287
287
|
subtitle="Update your payment address"
|
|
288
288
|
media={
|
|
289
289
|
<ListItem.AvatarView size={32}>
|
|
290
|
-
<
|
|
290
|
+
<House />
|
|
291
291
|
</ListItem.AvatarView>
|
|
292
292
|
}
|
|
293
293
|
additionalInfo={
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
2
|
import { within, userEvent, expect } from 'storybook/test';
|
|
3
|
-
import {
|
|
3
|
+
import { Padlock } from '@transferwise/icons';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
|
|
6
6
|
import MoneyInput, { CurrencyOptionItem } from '.';
|
|
@@ -117,7 +117,7 @@ export const SingleCurrency: Story = {
|
|
|
117
117
|
|
|
118
118
|
export const MultipleCurrencies: Story = {
|
|
119
119
|
args: {
|
|
120
|
-
addon: <
|
|
120
|
+
addon: <Padlock />,
|
|
121
121
|
currencies: [
|
|
122
122
|
{
|
|
123
123
|
header: 'Popular currencies',
|
|
@@ -4,7 +4,7 @@ import { Illustration, Assets, Flag } from '@wise/art';
|
|
|
4
4
|
import {
|
|
5
5
|
FastFlag as FastFlagIcon,
|
|
6
6
|
Bank as BankIcon,
|
|
7
|
-
|
|
7
|
+
Person,
|
|
8
8
|
UpwardGraph as UpwardGraphIcon,
|
|
9
9
|
FastFlag,
|
|
10
10
|
} from '@transferwise/icons';
|
|
@@ -106,7 +106,7 @@ export const Variants: StoryObj<StoryArgs> = {
|
|
|
106
106
|
showMediaCircle={false}
|
|
107
107
|
media={
|
|
108
108
|
<AvatarView badge={{ type: 'reference' }}>
|
|
109
|
-
<
|
|
109
|
+
<Person />
|
|
110
110
|
</AvatarView>
|
|
111
111
|
}
|
|
112
112
|
content={hasTitleOnly ? null : args.content}
|
|
@@ -248,7 +248,7 @@ export const NewContactContent: StoryObj<StoryArgs> = {
|
|
|
248
248
|
showMediaCircle={false}
|
|
249
249
|
media={
|
|
250
250
|
<AvatarView badge={{ type: 'reference' }}>
|
|
251
|
-
<
|
|
251
|
+
<Person size="24" />
|
|
252
252
|
</AvatarView>
|
|
253
253
|
}
|
|
254
254
|
onClick={fn()}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
2
|
import { action } from 'storybook/actions';
|
|
3
|
-
import { Person
|
|
3
|
+
import { Person, Briefcase as BriefcaseIcon } from '@transferwise/icons';
|
|
4
4
|
import AvatarView from '../avatarView';
|
|
5
5
|
import { ProfileType } from '../common';
|
|
6
6
|
import Logo from '../logo';
|
|
@@ -28,7 +28,7 @@ export default meta;
|
|
|
28
28
|
const avatarProfiles = {
|
|
29
29
|
'': null,
|
|
30
30
|
Business: <BriefcaseIcon size="24" />,
|
|
31
|
-
Profile: <
|
|
31
|
+
Profile: <Person size="24" />,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export const Basic: StoryObj<StoryArgs> = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReactElement, useState } from 'react';
|
|
2
2
|
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
3
3
|
import { fn } from 'storybook/test';
|
|
4
|
-
import { Bank, Star,
|
|
4
|
+
import { Bank, Star, Suitcase, Briefcase } from '@transferwise/icons';
|
|
5
5
|
import { lorem10, lorem20 } from '../../test-utils';
|
|
6
6
|
import Button from '../../button';
|
|
7
7
|
import Title from '../../title';
|
|
@@ -116,7 +116,7 @@ const MEDIA_OPTIONS: Record<string, ActionPromptProps['media']> = {
|
|
|
116
116
|
},
|
|
117
117
|
'Avatar: Icon (Bank)': { avatar: { asset: <Bank /> } },
|
|
118
118
|
'Avatar: Icon (Star)': { avatar: { asset: <Star /> } },
|
|
119
|
-
'Avatar: Icon (
|
|
119
|
+
'Avatar: Icon (Suitcase)': { avatar: { asset: <Suitcase /> } },
|
|
120
120
|
'Avatar: Icon (Briefcase)': { avatar: { asset: <Briefcase /> } },
|
|
121
121
|
'Avatar: Initials': { avatar: { profileName: 'John Doe' } },
|
|
122
122
|
};
|
|
@@ -133,7 +133,7 @@ const previewArgTypes = {
|
|
|
133
133
|
'Avatar: Personal (EU badge)',
|
|
134
134
|
'Avatar: Icon (Bank)',
|
|
135
135
|
'Avatar: Icon (Star)',
|
|
136
|
-
'Avatar: Icon (
|
|
136
|
+
'Avatar: Icon (Suitcase)',
|
|
137
137
|
'Avatar: Icon (Briefcase)',
|
|
138
138
|
'Avatar: Initials',
|
|
139
139
|
],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
|
-
import { Freeze, People,
|
|
2
|
+
import { Freeze, People, Suitcase } from '@transferwise/icons';
|
|
3
3
|
import { action } from 'storybook/actions';
|
|
4
4
|
import { fn } from 'storybook/test';
|
|
5
5
|
import ActionPrompt from './ActionPrompt';
|
|
@@ -248,7 +248,7 @@ export const Responsiveness: Story = {
|
|
|
248
248
|
sentiment="proposition"
|
|
249
249
|
title="Stacked (320px)"
|
|
250
250
|
description={lorem10}
|
|
251
|
-
media={{ avatar: { asset: <
|
|
251
|
+
media={{ avatar: { asset: <Suitcase /> } }}
|
|
252
252
|
action={{ label: 'Primary action', onClick: fn() }}
|
|
253
253
|
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
254
254
|
onDismiss={fn()}
|
|
@@ -257,7 +257,7 @@ export const Responsiveness: Story = {
|
|
|
257
257
|
sentiment="proposition"
|
|
258
258
|
title="Side-by-side (480px)"
|
|
259
259
|
description={lorem10}
|
|
260
|
-
media={{ avatar: { asset: <
|
|
260
|
+
media={{ avatar: { asset: <Suitcase /> } }}
|
|
261
261
|
action={{ label: 'Primary action', onClick: fn() }}
|
|
262
262
|
actionSecondary={{ label: 'Secondary', onClick: fn() }}
|
|
263
263
|
onDismiss={fn()}
|
|
@@ -64,7 +64,7 @@ Custom media icons should include their own accessibility attributes. Use the `t
|
|
|
64
64
|
<InfoPrompt
|
|
65
65
|
sentiment="success"
|
|
66
66
|
description="Your travel account is ready!"
|
|
67
|
-
media={{ asset: <
|
|
67
|
+
media={{ asset: <Suitcase title="Travel feature" /> }}
|
|
68
68
|
/>
|
|
69
69
|
|
|
70
70
|
<InfoPrompt
|
|
@@ -2,7 +2,7 @@ import type { ReactElement } from 'react';
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
4
4
|
import { action } from 'storybook/actions';
|
|
5
|
-
import { Confetti, GiftBox, Star,
|
|
5
|
+
import { Confetti, GiftBox, Star, Suitcase, Briefcase, Plane } from '@transferwise/icons';
|
|
6
6
|
import { lorem10, lorem20 } from '../../test-utils';
|
|
7
7
|
import Button from '../../button';
|
|
8
8
|
import Title from '../../title';
|
|
@@ -90,7 +90,7 @@ const MEDIA_OPTIONS: Record<string, { asset: ReactElement } | undefined> = {
|
|
|
90
90
|
star: { asset: <Star title="Starred" /> },
|
|
91
91
|
confetti: { asset: <Confetti size={24} title="Celebration" /> },
|
|
92
92
|
giftbox: { asset: <GiftBox title="Gift" /> },
|
|
93
|
-
travel: { asset: <
|
|
93
|
+
travel: { asset: <Suitcase title="Travel" /> },
|
|
94
94
|
briefcase: { asset: <Briefcase title="Business" /> },
|
|
95
95
|
plane: { asset: <Plane title="Travel" /> },
|
|
96
96
|
};
|
|
@@ -297,7 +297,7 @@ export const MediaTypes: StoryObj<InfoPromptProps> = {
|
|
|
297
297
|
<InfoPrompt
|
|
298
298
|
sentiment="success"
|
|
299
299
|
description="Your travel account is ready!"
|
|
300
|
-
media={{ asset: <
|
|
300
|
+
media={{ asset: <Suitcase title="Travel" /> }}
|
|
301
301
|
/>
|
|
302
302
|
<InfoPrompt
|
|
303
303
|
sentiment="warning"
|