@transferwise/components 46.135.2 → 46.136.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/avatarWrapper/AvatarWrapper.js.map +1 -1
- package/build/avatarWrapper/AvatarWrapper.mjs.map +1 -1
- package/build/container/Container.js +24 -0
- package/build/container/Container.js.map +1 -0
- package/build/container/Container.mjs +20 -0
- package/build/container/Container.mjs.map +1 -0
- 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/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/main.css +46 -0
- package/build/styles/container/Container.css +38 -0
- package/build/styles/main.css +46 -0
- package/build/types/body/Body.d.ts +2 -2
- package/build/types/container/Container.d.ts +16 -0
- package/build/types/container/Container.d.ts.map +1 -0
- package/build/types/container/index.d.ts +3 -0
- package/build/types/container/index.d.ts.map +1 -0
- package/build/types/iconButton/IconButton.d.ts +1 -1
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/listItem/Button/ListItemButton.d.ts +1 -1
- package/build/types/listItem/ListItem.d.ts +1 -1
- package/build/types/select/searchBox/SearchBox.d.ts +1 -1
- package/build/types/title/Title.d.ts +2 -2
- package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +1 -1
- package/package.json +3 -3
- 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/container/Container.css +38 -0
- package/src/container/Container.less +39 -0
- package/src/container/Container.story.tsx +130 -0
- package/src/container/Container.test.tsx +37 -0
- package/src/container/Container.tsx +37 -0
- package/src/container/index.ts +2 -0
- package/src/flowNavigation/FlowNavigation.story.tsx +16 -8
- 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/index.ts +2 -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/main.css +46 -0
- package/src/main.less +1 -0
- 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
|
@@ -11,6 +11,7 @@ import Display from '../display';
|
|
|
11
11
|
import Sticky from '../sticky';
|
|
12
12
|
|
|
13
13
|
import FlowNavigation, { FlowNavigationProps } from './FlowNavigation';
|
|
14
|
+
import Container from '../container';
|
|
14
15
|
|
|
15
16
|
interface CustomControls {
|
|
16
17
|
showCloseButton: boolean;
|
|
@@ -228,6 +229,9 @@ export const SendFlow: Story = {
|
|
|
228
229
|
done: false,
|
|
229
230
|
avatarURL: '../tapestry-01.png',
|
|
230
231
|
},
|
|
232
|
+
parameters: {
|
|
233
|
+
padding: '0',
|
|
234
|
+
},
|
|
231
235
|
render: (args) => {
|
|
232
236
|
const [activeStep, setActiveStep] = useState(2);
|
|
233
237
|
const steps = [
|
|
@@ -269,15 +273,19 @@ export const SendFlow: Story = {
|
|
|
269
273
|
onClose={() => alert('close & move away')}
|
|
270
274
|
onGoBack={() => setActiveStep(activeStep > 0 ? activeStep - 1 : 0)}
|
|
271
275
|
/>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
<Container size="narrow">
|
|
277
|
+
<Body className="m-a-3">
|
|
278
|
+
<Display type={Typography.DISPLAY_SMALL}>{steps[activeStep].label} Step</Display>
|
|
279
|
+
<br />
|
|
280
|
+
{lorem10}
|
|
281
|
+
</Body>
|
|
282
|
+
</Container>
|
|
278
283
|
|
|
279
284
|
<Sticky>
|
|
280
|
-
<
|
|
285
|
+
<Container
|
|
286
|
+
size="narrow"
|
|
287
|
+
className="d-flex justify-content-center align-items-center p-y-3"
|
|
288
|
+
>
|
|
281
289
|
<Button
|
|
282
290
|
v2
|
|
283
291
|
disabled={activeStep === 3}
|
|
@@ -286,7 +294,7 @@ export const SendFlow: Story = {
|
|
|
286
294
|
>
|
|
287
295
|
Continue
|
|
288
296
|
</Button>
|
|
289
|
-
</
|
|
297
|
+
</Container>
|
|
290
298
|
</Sticky>
|
|
291
299
|
</>
|
|
292
300
|
);
|
|
@@ -2,13 +2,12 @@ import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
|
2
2
|
import {
|
|
3
3
|
ArrowLeft,
|
|
4
4
|
Cross,
|
|
5
|
-
Defrost,
|
|
6
5
|
Edit,
|
|
7
6
|
Menu,
|
|
8
7
|
Plus,
|
|
9
|
-
|
|
8
|
+
Cog,
|
|
10
9
|
Star,
|
|
11
|
-
|
|
10
|
+
Suitcase,
|
|
12
11
|
Briefcase,
|
|
13
12
|
Bank,
|
|
14
13
|
Freeze,
|
|
@@ -96,7 +95,7 @@ export const Priority: Story = {
|
|
|
96
95
|
<Plus />
|
|
97
96
|
</IconButton>
|
|
98
97
|
<IconButton priority="secondary" type={args.type} aria-label="Secondary" onClick={fn()}>
|
|
99
|
-
<
|
|
98
|
+
<Cog />
|
|
100
99
|
</IconButton>
|
|
101
100
|
<IconButton priority="tertiary" type={args.type} aria-label="Tertiary" onClick={fn()}>
|
|
102
101
|
<Star />
|
|
@@ -363,7 +362,7 @@ export const SentimentAwareness: Story = {
|
|
|
363
362
|
type="default"
|
|
364
363
|
onClick={fn()}
|
|
365
364
|
>
|
|
366
|
-
<
|
|
365
|
+
<Cog />
|
|
367
366
|
</IconButton>
|
|
368
367
|
<IconButton
|
|
369
368
|
size={args.size}
|
|
@@ -381,7 +380,7 @@ export const SentimentAwareness: Story = {
|
|
|
381
380
|
type="default"
|
|
382
381
|
onClick={fn()}
|
|
383
382
|
>
|
|
384
|
-
<
|
|
383
|
+
<Suitcase />
|
|
385
384
|
</IconButton>
|
|
386
385
|
<IconButton
|
|
387
386
|
size={args.size}
|
|
@@ -2,9 +2,9 @@ import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
|
2
2
|
import {
|
|
3
3
|
Menu,
|
|
4
4
|
Plus,
|
|
5
|
-
|
|
5
|
+
Cog,
|
|
6
6
|
Star,
|
|
7
|
-
|
|
7
|
+
Suitcase,
|
|
8
8
|
Cross,
|
|
9
9
|
Edit,
|
|
10
10
|
Briefcase,
|
|
@@ -109,7 +109,7 @@ export const Variants: Story = {
|
|
|
109
109
|
type="default"
|
|
110
110
|
onClick={fn()}
|
|
111
111
|
>
|
|
112
|
-
<
|
|
112
|
+
<Cog />
|
|
113
113
|
</IconButton>
|
|
114
114
|
<IconButton
|
|
115
115
|
size={size}
|
|
@@ -127,7 +127,7 @@ export const Variants: Story = {
|
|
|
127
127
|
type="default"
|
|
128
128
|
onClick={fn()}
|
|
129
129
|
>
|
|
130
|
-
<
|
|
130
|
+
<Suitcase />
|
|
131
131
|
</IconButton>
|
|
132
132
|
<IconButton
|
|
133
133
|
size={size}
|
|
@@ -261,13 +261,13 @@ export const KeyboardInteraction: Story = {
|
|
|
261
261
|
type="default"
|
|
262
262
|
onClick={fn()}
|
|
263
263
|
>
|
|
264
|
-
<
|
|
264
|
+
<Cog />
|
|
265
265
|
</IconButton>
|
|
266
266
|
<IconButton size={48} aria-label="Tertiary" priority="tertiary" type="default" onClick={fn()}>
|
|
267
267
|
<Star />
|
|
268
268
|
</IconButton>
|
|
269
269
|
<IconButton size={48} aria-label="Minimal" priority="minimal" type="default" onClick={fn()}>
|
|
270
|
-
<
|
|
270
|
+
<Suitcase />
|
|
271
271
|
</IconButton>
|
|
272
272
|
<IconButton
|
|
273
273
|
size={48}
|
|
@@ -343,13 +343,13 @@ export const Zoom400: Story = {
|
|
|
343
343
|
type="default"
|
|
344
344
|
onClick={fn()}
|
|
345
345
|
>
|
|
346
|
-
<
|
|
346
|
+
<Cog />
|
|
347
347
|
</IconButton>
|
|
348
348
|
<IconButton size={40} aria-label="Tertiary" priority="tertiary" type="default" onClick={fn()}>
|
|
349
349
|
<Star />
|
|
350
350
|
</IconButton>
|
|
351
351
|
<IconButton size={40} aria-label="Minimal" priority="minimal" type="default" onClick={fn()}>
|
|
352
|
-
<
|
|
352
|
+
<Suitcase />
|
|
353
353
|
</IconButton>
|
|
354
354
|
<IconButton
|
|
355
355
|
size={40}
|
|
@@ -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
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Types
|
|
5
5
|
*/
|
|
6
|
+
export type { ContainerProps } from './container';
|
|
6
7
|
export type { AccordionItem, AccordionProps } from './accordion';
|
|
7
8
|
export type { ActionOptionProps } from './actionOption';
|
|
8
9
|
export type { AlertAction, AlertProps, AlertType } from './alert';
|
|
@@ -152,6 +153,7 @@ export { default as CheckboxOption } from './checkboxOption';
|
|
|
152
153
|
export { default as Chevron } from './chevron';
|
|
153
154
|
export { Chip, default as Chips } from './chips';
|
|
154
155
|
export { default as CircularButton } from './circularButton';
|
|
156
|
+
export { default as Container } from './container';
|
|
155
157
|
export { default as Option } from './common/Option';
|
|
156
158
|
export { default as BottomSheet } from './common/bottomSheet';
|
|
157
159
|
export { default as BaseCard } from './common/baseCard';
|
|
@@ -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={
|