@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.
Files changed (66) hide show
  1. package/build/avatarWrapper/AvatarWrapper.js.map +1 -1
  2. package/build/avatarWrapper/AvatarWrapper.mjs.map +1 -1
  3. package/build/container/Container.js +24 -0
  4. package/build/container/Container.js.map +1 -0
  5. package/build/container/Container.mjs +20 -0
  6. package/build/container/Container.mjs.map +1 -0
  7. package/build/index.js +2 -0
  8. package/build/index.js.map +1 -1
  9. package/build/index.mjs +1 -0
  10. package/build/index.mjs.map +1 -1
  11. package/build/listItem/ListItem.js +2 -2
  12. package/build/listItem/ListItem.js.map +1 -1
  13. package/build/listItem/ListItem.mjs +2 -2
  14. package/build/listItem/ListItem.mjs.map +1 -1
  15. package/build/main.css +46 -0
  16. package/build/styles/container/Container.css +38 -0
  17. package/build/styles/main.css +46 -0
  18. package/build/types/body/Body.d.ts +2 -2
  19. package/build/types/container/Container.d.ts +16 -0
  20. package/build/types/container/Container.d.ts.map +1 -0
  21. package/build/types/container/index.d.ts +3 -0
  22. package/build/types/container/index.d.ts.map +1 -0
  23. package/build/types/iconButton/IconButton.d.ts +1 -1
  24. package/build/types/index.d.ts +2 -0
  25. package/build/types/index.d.ts.map +1 -1
  26. package/build/types/listItem/Button/ListItemButton.d.ts +1 -1
  27. package/build/types/listItem/ListItem.d.ts +1 -1
  28. package/build/types/select/searchBox/SearchBox.d.ts +1 -1
  29. package/build/types/title/Title.d.ts +2 -2
  30. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +1 -1
  31. package/package.json +3 -3
  32. package/src/actionButton/ActionButton.story.tsx +4 -4
  33. package/src/actionButton/ActionButton.test.story.tsx +4 -4
  34. package/src/avatarWrapper/AvatarWrapper.tsx +3 -3
  35. package/src/common/circle/Circle.story.tsx +3 -3
  36. package/src/container/Container.css +38 -0
  37. package/src/container/Container.less +39 -0
  38. package/src/container/Container.story.tsx +130 -0
  39. package/src/container/Container.test.tsx +37 -0
  40. package/src/container/Container.tsx +37 -0
  41. package/src/container/index.ts +2 -0
  42. package/src/flowNavigation/FlowNavigation.story.tsx +16 -8
  43. package/src/iconButton/IconButton.story.tsx +5 -6
  44. package/src/iconButton/IconButton.test.story.tsx +8 -8
  45. package/src/icons/Icons.story.tsx +381 -0
  46. package/src/index.ts +2 -0
  47. package/src/listItem/ListItem.test.tsx +24 -0
  48. package/src/listItem/ListItem.tsx +2 -2
  49. package/src/listItem/_stories/ListItem.context.test.story.tsx +63 -0
  50. package/src/listItem/_stories/ListItem.scenarios.story.tsx +3 -3
  51. package/src/main.css +46 -0
  52. package/src/main.less +1 -0
  53. package/src/moneyInput/MoneyInput.story.tsx +2 -2
  54. package/src/navigationOption/NavigationOption.story.tsx +3 -3
  55. package/src/overlayHeader/OverlayHeader.story.tsx +2 -2
  56. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +3 -3
  57. package/src/prompt/ActionPrompt/ActionPrompt.test.story.tsx +3 -3
  58. package/src/prompt/InfoPrompt/InfoPrompt.accessibility.docs.mdx +1 -1
  59. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +3 -3
  60. package/src/prompt/InlinePrompt/InlinePrompt.accessibility.docs.mdx +1 -1
  61. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +5 -5
  62. package/src/prompt/InlinePrompt/InlinePrompt.test.story.tsx +2 -2
  63. package/src/select/Select.story.tsx +3 -3
  64. package/src/select/option/Option.test.tsx +3 -3
  65. package/src/summary/Summary.story.tsx +5 -5
  66. 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
- <Body className="m-a-3">
274
- <Display type={Typography.DISPLAY_SMALL}>{steps[activeStep].label} Step</Display>
275
- <br />
276
- {lorem10}
277
- </Body>
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
- <div className="d-flex justify-content-center align-items-center p-a-3">
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
- </div>
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
- Settings,
8
+ Cog,
10
9
  Star,
11
- Travel,
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
- <Settings />
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
- <Settings />
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
- <Travel />
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
- Settings,
5
+ Cog,
6
6
  Star,
7
- Travel,
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
- <Settings />
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
- <Travel />
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
- <Settings />
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
- <Travel />
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
- <Settings />
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
- <Travel />
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
- Home,
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
- <Home />
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
- <Home />
290
+ <House />
291
291
  </ListItem.AvatarView>
292
292
  }
293
293
  additionalInfo={