@transferwise/components 0.0.0-experimental-e06e456 → 0.0.0-experimental-e3978a5

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 (201) hide show
  1. package/build/index.js +2 -0
  2. package/build/index.js.map +1 -1
  3. package/build/index.mjs +1 -0
  4. package/build/index.mjs.map +1 -1
  5. package/build/listItem/AdditionalInfo/ListItemAdditionalInfo.js +56 -0
  6. package/build/listItem/AdditionalInfo/ListItemAdditionalInfo.js.map +1 -0
  7. package/build/listItem/AdditionalInfo/ListItemAdditionalInfo.mjs +54 -0
  8. package/build/listItem/AdditionalInfo/ListItemAdditionalInfo.mjs.map +1 -0
  9. package/build/listItem/AvatarLayout/ListItemAvatarLayout.js +23 -0
  10. package/build/listItem/AvatarLayout/ListItemAvatarLayout.js.map +1 -0
  11. package/build/listItem/AvatarLayout/ListItemAvatarLayout.mjs +21 -0
  12. package/build/listItem/AvatarLayout/ListItemAvatarLayout.mjs.map +1 -0
  13. package/build/listItem/AvatarView/ListItemAvatarView.js +23 -0
  14. package/build/listItem/AvatarView/ListItemAvatarView.js.map +1 -0
  15. package/build/listItem/AvatarView/ListItemAvatarView.mjs +21 -0
  16. package/build/listItem/AvatarView/ListItemAvatarView.mjs.map +1 -0
  17. package/build/listItem/Button/ListItemButton.js +43 -0
  18. package/build/listItem/Button/ListItemButton.js.map +1 -0
  19. package/build/listItem/Button/ListItemButton.mjs +41 -0
  20. package/build/listItem/Button/ListItemButton.mjs.map +1 -0
  21. package/build/listItem/Checkbox/ListItemCheckbox.js +30 -0
  22. package/build/listItem/Checkbox/ListItemCheckbox.js.map +1 -0
  23. package/build/listItem/Checkbox/ListItemCheckbox.mjs +28 -0
  24. package/build/listItem/Checkbox/ListItemCheckbox.mjs.map +1 -0
  25. package/build/listItem/IconButton/ListItemIconButton.js +56 -0
  26. package/build/listItem/IconButton/ListItemIconButton.js.map +1 -0
  27. package/build/listItem/IconButton/ListItemIconButton.mjs +54 -0
  28. package/build/listItem/IconButton/ListItemIconButton.mjs.map +1 -0
  29. package/build/listItem/Image/ListItemImage.js +31 -0
  30. package/build/listItem/Image/ListItemImage.js.map +1 -0
  31. package/build/listItem/Image/ListItemImage.mjs +29 -0
  32. package/build/listItem/Image/ListItemImage.mjs.map +1 -0
  33. package/build/listItem/ListItem.js +313 -0
  34. package/build/listItem/ListItem.js.map +1 -0
  35. package/build/listItem/ListItem.mjs +308 -0
  36. package/build/listItem/ListItem.mjs.map +1 -0
  37. package/build/listItem/ListItemContext.js +8 -0
  38. package/build/listItem/ListItemContext.js.map +1 -0
  39. package/build/listItem/ListItemContext.mjs +6 -0
  40. package/build/listItem/ListItemContext.mjs.map +1 -0
  41. package/build/listItem/Navigation/ListItemNavigation.js +44 -0
  42. package/build/listItem/Navigation/ListItemNavigation.js.map +1 -0
  43. package/build/listItem/Navigation/ListItemNavigation.mjs +42 -0
  44. package/build/listItem/Navigation/ListItemNavigation.mjs.map +1 -0
  45. package/build/listItem/Prompt/ListItemPrompt.js +59 -0
  46. package/build/listItem/Prompt/ListItemPrompt.js.map +1 -0
  47. package/build/listItem/Prompt/ListItemPrompt.mjs +54 -0
  48. package/build/listItem/Prompt/ListItemPrompt.mjs.map +1 -0
  49. package/build/listItem/Radio/ListItemRadio.js +30 -0
  50. package/build/listItem/Radio/ListItemRadio.js.map +1 -0
  51. package/build/listItem/Radio/ListItemRadio.mjs +28 -0
  52. package/build/listItem/Radio/ListItemRadio.mjs.map +1 -0
  53. package/build/listItem/Switch/ListItemSwitch.js +30 -0
  54. package/build/listItem/Switch/ListItemSwitch.js.map +1 -0
  55. package/build/listItem/Switch/ListItemSwitch.mjs +28 -0
  56. package/build/listItem/Switch/ListItemSwitch.mjs.map +1 -0
  57. package/build/listItem/useListItemControl.js +22 -0
  58. package/build/listItem/useListItemControl.js.map +1 -0
  59. package/build/listItem/useListItemControl.mjs +20 -0
  60. package/build/listItem/useListItemControl.mjs.map +1 -0
  61. package/build/listItem/useListItemMedia.js +21 -0
  62. package/build/listItem/useListItemMedia.js.map +1 -0
  63. package/build/listItem/useListItemMedia.mjs +19 -0
  64. package/build/listItem/useListItemMedia.mjs.map +1 -0
  65. package/build/main.css +771 -1
  66. package/build/styles/button/Button.css +1 -1
  67. package/build/styles/listItem/ListItem.css +770 -0
  68. package/build/styles/listItem/ListItem.grid.css +370 -0
  69. package/build/styles/main.css +771 -1
  70. package/build/types/index.d.ts +2 -0
  71. package/build/types/index.d.ts.map +1 -1
  72. package/build/types/listItem/AdditionalInfo/ListItemAdditionalInfo.d.ts +15 -0
  73. package/build/types/listItem/AdditionalInfo/ListItemAdditionalInfo.d.ts.map +1 -0
  74. package/build/types/listItem/AdditionalInfo/index.d.ts +3 -0
  75. package/build/types/listItem/AdditionalInfo/index.d.ts.map +1 -0
  76. package/build/types/listItem/AvatarLayout/ListItemAvatarLayout.d.ts +18 -0
  77. package/build/types/listItem/AvatarLayout/ListItemAvatarLayout.d.ts.map +1 -0
  78. package/build/types/listItem/AvatarLayout/index.d.ts +3 -0
  79. package/build/types/listItem/AvatarLayout/index.d.ts.map +1 -0
  80. package/build/types/listItem/AvatarView/ListItemAvatarView.d.ts +16 -0
  81. package/build/types/listItem/AvatarView/ListItemAvatarView.d.ts.map +1 -0
  82. package/build/types/listItem/AvatarView/index.d.ts +3 -0
  83. package/build/types/listItem/AvatarView/index.d.ts.map +1 -0
  84. package/build/types/listItem/Button/ListItemButton.d.ts +20 -0
  85. package/build/types/listItem/Button/ListItemButton.d.ts.map +1 -0
  86. package/build/types/listItem/Button/index.d.ts +3 -0
  87. package/build/types/listItem/Button/index.d.ts.map +1 -0
  88. package/build/types/listItem/Checkbox/ListItemCheckbox.d.ts +14 -0
  89. package/build/types/listItem/Checkbox/ListItemCheckbox.d.ts.map +1 -0
  90. package/build/types/listItem/Checkbox/index.d.ts +3 -0
  91. package/build/types/listItem/Checkbox/index.d.ts.map +1 -0
  92. package/build/types/listItem/IconButton/ListItemIconButton.d.ts +18 -0
  93. package/build/types/listItem/IconButton/ListItemIconButton.d.ts.map +1 -0
  94. package/build/types/listItem/IconButton/index.d.ts +3 -0
  95. package/build/types/listItem/IconButton/index.d.ts.map +1 -0
  96. package/build/types/listItem/Image/ListItemImage.d.ts +25 -0
  97. package/build/types/listItem/Image/ListItemImage.d.ts.map +1 -0
  98. package/build/types/listItem/Image/index.d.ts +3 -0
  99. package/build/types/listItem/Image/index.d.ts.map +1 -0
  100. package/build/types/listItem/ListItem.d.ts +113 -0
  101. package/build/types/listItem/ListItem.d.ts.map +1 -0
  102. package/build/types/listItem/ListItemContext.d.ts +21 -0
  103. package/build/types/listItem/ListItemContext.d.ts.map +1 -0
  104. package/build/types/listItem/Navigation/ListItemNavigation.d.ts +15 -0
  105. package/build/types/listItem/Navigation/ListItemNavigation.d.ts.map +1 -0
  106. package/build/types/listItem/Navigation/index.d.ts +3 -0
  107. package/build/types/listItem/Navigation/index.d.ts.map +1 -0
  108. package/build/types/listItem/Prompt/ListItemPrompt.d.ts +16 -0
  109. package/build/types/listItem/Prompt/ListItemPrompt.d.ts.map +1 -0
  110. package/build/types/listItem/Prompt/index.d.ts +3 -0
  111. package/build/types/listItem/Prompt/index.d.ts.map +1 -0
  112. package/build/types/listItem/Radio/ListItemRadio.d.ts +14 -0
  113. package/build/types/listItem/Radio/ListItemRadio.d.ts.map +1 -0
  114. package/build/types/listItem/Radio/index.d.ts +3 -0
  115. package/build/types/listItem/Radio/index.d.ts.map +1 -0
  116. package/build/types/listItem/Switch/ListItemSwitch.d.ts +14 -0
  117. package/build/types/listItem/Switch/ListItemSwitch.d.ts.map +1 -0
  118. package/build/types/listItem/Switch/index.d.ts +3 -0
  119. package/build/types/listItem/Switch/index.d.ts.map +1 -0
  120. package/build/types/listItem/_stories/helpers.d.ts +27 -0
  121. package/build/types/listItem/_stories/helpers.d.ts.map +1 -0
  122. package/build/types/listItem/_stories/subcomponents.d.ts +18 -0
  123. package/build/types/listItem/_stories/subcomponents.d.ts.map +1 -0
  124. package/build/types/listItem/index.d.ts +14 -0
  125. package/build/types/listItem/index.d.ts.map +1 -0
  126. package/build/types/listItem/test-utils.d.ts +7 -0
  127. package/build/types/listItem/test-utils.d.ts.map +1 -0
  128. package/build/types/listItem/useListItemControl.d.ts +5 -0
  129. package/build/types/listItem/useListItemControl.d.ts.map +1 -0
  130. package/build/types/listItem/useListItemMedia.d.ts +6 -0
  131. package/build/types/listItem/useListItemMedia.d.ts.map +1 -0
  132. package/package.json +1 -1
  133. package/src/button/Button.css +1 -1
  134. package/src/button/Button.less +1 -1
  135. package/src/button/Button.story.tsx +4 -9
  136. package/src/index.ts +15 -0
  137. package/src/list/List.story.tsx +13 -3
  138. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.spec.tsx +56 -0
  139. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.story.tsx +198 -0
  140. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.tsx +36 -0
  141. package/src/listItem/AdditionalInfo/index.ts +2 -0
  142. package/src/listItem/AvatarLayout/ListItemAvatarLayout.spec.tsx +59 -0
  143. package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +124 -0
  144. package/src/listItem/AvatarLayout/ListItemAvatarLayout.tsx +27 -0
  145. package/src/listItem/AvatarLayout/index.ts +2 -0
  146. package/src/listItem/AvatarView/ListItemAvatarView.spec.tsx +75 -0
  147. package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +339 -0
  148. package/src/listItem/AvatarView/ListItemAvatarView.tsx +27 -0
  149. package/src/listItem/AvatarView/index.ts +2 -0
  150. package/src/listItem/Button/ListItemButton.spec.tsx +68 -0
  151. package/src/listItem/Button/ListItemButton.story.tsx +473 -0
  152. package/src/listItem/Button/ListItemButton.tsx +56 -0
  153. package/src/listItem/Button/index.ts +2 -0
  154. package/src/listItem/Checkbox/ListItemCheckbox.spec.tsx +82 -0
  155. package/src/listItem/Checkbox/ListItemCheckbox.story.tsx +128 -0
  156. package/src/listItem/Checkbox/ListItemCheckbox.tsx +33 -0
  157. package/src/listItem/Checkbox/index.ts +2 -0
  158. package/src/listItem/IconButton/ListItemIconButton.spec.tsx +119 -0
  159. package/src/listItem/IconButton/ListItemIconButton.story.tsx +284 -0
  160. package/src/listItem/IconButton/ListItemIconButton.tsx +73 -0
  161. package/src/listItem/IconButton/index.ts +2 -0
  162. package/src/listItem/Image/ListItemImage.spec.tsx +30 -0
  163. package/src/listItem/Image/ListItemImage.story.tsx +80 -0
  164. package/src/listItem/Image/ListItemImage.tsx +46 -0
  165. package/src/listItem/Image/index.ts +2 -0
  166. package/src/listItem/ListItem.css +770 -0
  167. package/src/listItem/ListItem.grid.css +370 -0
  168. package/src/listItem/ListItem.grid.less +622 -0
  169. package/src/listItem/ListItem.less +421 -0
  170. package/src/listItem/ListItem.spec.tsx +1521 -0
  171. package/src/listItem/ListItem.tsx +444 -0
  172. package/src/listItem/ListItemContext.tsx +26 -0
  173. package/src/listItem/Navigation/ListItemNavigation.spec.tsx +59 -0
  174. package/src/listItem/Navigation/ListItemNavigation.story.tsx +112 -0
  175. package/src/listItem/Navigation/ListItemNavigation.tsx +39 -0
  176. package/src/listItem/Navigation/index.ts +2 -0
  177. package/src/listItem/Prompt/ListItemPrompt.spec.tsx +36 -0
  178. package/src/listItem/Prompt/ListItemPrompt.story.tsx +204 -0
  179. package/src/listItem/Prompt/ListItemPrompt.tsx +32 -0
  180. package/src/listItem/Prompt/index.ts +2 -0
  181. package/src/listItem/Radio/ListItemRadio.spec.tsx +66 -0
  182. package/src/listItem/Radio/ListItemRadio.story.tsx +111 -0
  183. package/src/listItem/Radio/ListItemRadio.tsx +33 -0
  184. package/src/listItem/Radio/index.ts +2 -0
  185. package/src/listItem/Switch/ListItemSwitch.spec.tsx +47 -0
  186. package/src/listItem/Switch/ListItemSwitch.story.tsx +79 -0
  187. package/src/listItem/Switch/ListItemSwitch.tsx +33 -0
  188. package/src/listItem/Switch/index.ts +2 -0
  189. package/src/listItem/_stories/ListItem.focus.test.story.tsx +265 -0
  190. package/src/listItem/_stories/ListItem.layout.test.story.tsx +354 -0
  191. package/src/listItem/_stories/ListItem.scenarios.story.tsx +228 -0
  192. package/src/listItem/_stories/ListItem.story.tsx +775 -0
  193. package/src/listItem/_stories/ListItem.variants.test.story.tsx +271 -0
  194. package/src/listItem/_stories/helpers.tsx +53 -0
  195. package/src/listItem/_stories/subcomponents.tsx +139 -0
  196. package/src/listItem/index.ts +14 -0
  197. package/src/listItem/test-utils.tsx +33 -0
  198. package/src/listItem/useListItemControl.tsx +18 -0
  199. package/src/listItem/useListItemMedia.tsx +16 -0
  200. package/src/main.css +771 -1
  201. package/src/main.less +1 -0
@@ -0,0 +1,265 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import Link from '../../link';
3
+ import List from '../../list';
4
+ import { ListItem, type ListItemProps } from '../ListItem';
5
+ import { expect, userEvent, within, waitFor } from 'storybook/test';
6
+
7
+ const waitForFocus = async (assertion: () => Promise<void>, timeout = 3000) => {
8
+ await waitFor(assertion, { timeout });
9
+ };
10
+
11
+ const waitForListItem = async (canvas: ReturnType<typeof within>, timeout = 3000) => {
12
+ await waitFor(
13
+ async () => {
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
15
+ await expect(canvas.getByRole('listitem')).toBeInTheDocument();
16
+ },
17
+ { timeout },
18
+ );
19
+ };
20
+
21
+ export default {
22
+ component: ListItem,
23
+ title: 'Content/ListItem/tests/focus',
24
+ tags: ['!autodocs'],
25
+ parameters: {
26
+ controls: { disable: true },
27
+ actions: { disable: true },
28
+ a11y: { disable: true },
29
+ knobs: { disable: true },
30
+ },
31
+ } satisfies Meta<ListItemProps>;
32
+ type Story = StoryObj<ListItemProps>;
33
+
34
+ const title = {
35
+ full: 'Fully interactive',
36
+ partial: 'Partially interactive',
37
+ };
38
+
39
+ const subtitle = {
40
+ full: 'Whole item should be focusable, control should not.',
41
+ partial: 'Only control should be focusable, not whole item.',
42
+ };
43
+
44
+ const additionalInfo = {
45
+ static: (
46
+ <ListItem.AdditionalInfo>
47
+ Fully interactive ListItems don&apos;t allow any nested interactive elements like links or
48
+ buttons within AdditionalInfo.
49
+ </ListItem.AdditionalInfo>
50
+ ),
51
+ interactive: (
52
+ <ListItem.AdditionalInfo
53
+ action={{ label: 'appended to the end.', href: 'https://wise.com', target: '_blank' }}
54
+ >
55
+ This additional info has a focusable link
56
+ </ListItem.AdditionalInfo>
57
+ ),
58
+ };
59
+
60
+ const prompt = {
61
+ static: <ListItem.Prompt sentiment="positive">Non-interactive prompt.</ListItem.Prompt>,
62
+ interactive: (
63
+ <ListItem.Prompt sentiment="positive">
64
+ This prompt has a{' '}
65
+ <Link href="https://wise.com" target="_blank" rel="noreferrer">
66
+ single interactive element
67
+ </Link>{' '}
68
+ that spreads across the whole prompt area.
69
+ </ListItem.Prompt>
70
+ ),
71
+ };
72
+
73
+ const button = {
74
+ full: (
75
+ <ListItem.Button priority="secondary-neutral" onClick={() => {}}>
76
+ Click me
77
+ </ListItem.Button>
78
+ ),
79
+ partial: (
80
+ <ListItem.Button partiallyInteractive priority="secondary-neutral" onClick={() => {}}>
81
+ Click me
82
+ </ListItem.Button>
83
+ ),
84
+ };
85
+
86
+ export const FullyInteractive: Story = {
87
+ play: async ({ canvasElement }) => {
88
+ const canvas = within(canvasElement);
89
+ await waitForListItem(canvas);
90
+ await userEvent.tab();
91
+ await waitForFocus(async () => {
92
+ await expect(canvas.getByRole('button')).toHaveFocus();
93
+ });
94
+ },
95
+ render: () => (
96
+ <List>
97
+ <ListItem
98
+ title={title.full}
99
+ subtitle={subtitle.full}
100
+ additionalInfo={additionalInfo.static}
101
+ control={button.full}
102
+ prompt={prompt.static}
103
+ />
104
+ </List>
105
+ ),
106
+ };
107
+
108
+ export const FullyInteractiveFocusedOnPrompt: Story = {
109
+ play: async ({ canvasElement }) => {
110
+ const canvas = within(canvasElement);
111
+ await waitForListItem(canvas);
112
+
113
+ await userEvent.tab();
114
+ await waitForFocus(async () => {
115
+ await expect(canvas.getByRole('button')).toHaveFocus();
116
+ });
117
+
118
+ await userEvent.tab();
119
+ await waitForFocus(async () => {
120
+ await expect(canvas.getByRole('link', { name: /^single interactive element/ })).toHaveFocus();
121
+ });
122
+ },
123
+ render: () => (
124
+ <List>
125
+ <ListItem
126
+ title={title.full}
127
+ subtitle={subtitle.full}
128
+ additionalInfo={additionalInfo.static}
129
+ control={button.full}
130
+ prompt={prompt.interactive}
131
+ />
132
+ </List>
133
+ ),
134
+ };
135
+
136
+ export const PartiallyInteractiveFocusedOnControl: Story = {
137
+ play: async ({ canvasElement }) => {
138
+ const canvas = within(canvasElement);
139
+ await waitForListItem(canvas);
140
+ await userEvent.tab();
141
+ await waitForFocus(async () => {
142
+ await expect(canvas.getByRole('button')).toHaveFocus();
143
+ });
144
+ },
145
+ render: () => (
146
+ <List>
147
+ <ListItem
148
+ title={title.partial}
149
+ subtitle={subtitle.partial}
150
+ additionalInfo={additionalInfo.interactive}
151
+ control={button.partial}
152
+ prompt={prompt.interactive}
153
+ />
154
+ </List>
155
+ ),
156
+ };
157
+
158
+ export const PartiallyInteractiveFocusedOnAdditionInfo: Story = {
159
+ play: async ({ canvasElement }) => {
160
+ const canvas = within(canvasElement);
161
+ await waitForListItem(canvas);
162
+
163
+ await userEvent.tab();
164
+ await waitForFocus(async () => {
165
+ await expect(canvas.getByRole('button')).toHaveFocus();
166
+ });
167
+
168
+ await userEvent.tab();
169
+ await waitForFocus(async () => {
170
+ await expect(canvas.getByRole('link', { name: /^appended to the end/ })).toHaveFocus();
171
+ });
172
+ },
173
+ render: () => (
174
+ <List>
175
+ <ListItem
176
+ title={title.partial}
177
+ subtitle={subtitle.partial}
178
+ additionalInfo={additionalInfo.interactive}
179
+ control={button.partial}
180
+ prompt={prompt.interactive}
181
+ />
182
+ </List>
183
+ ),
184
+ };
185
+
186
+ export const PartiallyInteractiveFocusedOnPrompt: Story = {
187
+ play: async ({ canvasElement }) => {
188
+ const canvas = within(canvasElement);
189
+ await waitForListItem(canvas);
190
+
191
+ await userEvent.tab();
192
+ await waitForFocus(async () => {
193
+ await expect(canvas.getByRole('button')).toHaveFocus();
194
+ });
195
+
196
+ await userEvent.tab();
197
+ await waitForFocus(async () => {
198
+ await expect(canvas.getByRole('link', { name: /^appended to the end/ })).toHaveFocus();
199
+ });
200
+
201
+ await userEvent.tab();
202
+ await waitForFocus(async () => {
203
+ await expect(canvas.getByRole('link', { name: /^single interactive element/ })).toHaveFocus();
204
+ });
205
+ },
206
+ render: () => (
207
+ <List>
208
+ <ListItem
209
+ title={title.partial}
210
+ subtitle={subtitle.partial}
211
+ additionalInfo={additionalInfo.interactive}
212
+ control={button.partial}
213
+ prompt={prompt.interactive}
214
+ />
215
+ </List>
216
+ ),
217
+ };
218
+
219
+ export const FullyInteractiveDisabled: Story = {
220
+ play: async ({ canvasElement }) => {
221
+ const canvas = within(canvasElement);
222
+ await waitForListItem(canvas);
223
+
224
+ await userEvent.tab();
225
+ await waitForFocus(async () => {
226
+ await expect(canvas.getByRole('button')).not.toHaveFocus();
227
+ });
228
+ },
229
+ render: () => (
230
+ <List>
231
+ <ListItem
232
+ disabled
233
+ title={title.full}
234
+ subtitle={subtitle.full}
235
+ additionalInfo={additionalInfo.static}
236
+ control={button.full}
237
+ prompt={prompt.static}
238
+ />
239
+ </List>
240
+ ),
241
+ };
242
+
243
+ export const PartiallyInteractiveDisabled: Story = {
244
+ play: async ({ canvasElement }) => {
245
+ const canvas = within(canvasElement);
246
+ await waitForListItem(canvas);
247
+
248
+ await userEvent.tab();
249
+ await waitForFocus(async () => {
250
+ await expect(canvas.getByRole('button')).not.toHaveFocus();
251
+ });
252
+ },
253
+ render: () => (
254
+ <List>
255
+ <ListItem
256
+ disabled
257
+ title={title.partial}
258
+ subtitle={subtitle.partial}
259
+ additionalInfo={additionalInfo.interactive}
260
+ control={button.partial}
261
+ prompt={prompt.interactive}
262
+ />
263
+ </List>
264
+ ),
265
+ };
@@ -0,0 +1,354 @@
1
+ import { Fragment } from 'react';
2
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
3
+ import { Bank, FastFlag, MultiCurrency, Receipt, Savings } from '@transferwise/icons';
4
+ import Link from '../../link';
5
+ import List from '../../list';
6
+ import { ListItem, type ListItemProps } from '../ListItem';
7
+ import { lorem10, lorem20, lorem5 } from '../../test-utils';
8
+ import portraitImage from '../../test-utils/assets/placeholder-landscape.svg';
9
+ import landscapeImage from '../../test-utils/assets/placeholder-portrait.svg';
10
+ import { SB_LIST_ITEM_CONTROLS as CONTROLS, SB_LIST_ITEM_MEDIA as MEDIA } from './subcomponents';
11
+ import { ListItemMediaSize } from '../ListItemContext';
12
+
13
+ const withSizedContainer = (width: number) => (Story: any) => (
14
+ <List
15
+ className="list-unstyled"
16
+ style={{
17
+ width,
18
+ display: 'flex',
19
+ flexDirection: 'column',
20
+ gap: 16,
21
+ }}
22
+ >
23
+ <Story />
24
+ </List>
25
+ );
26
+
27
+ export default {
28
+ title: 'Content/ListItem/tests/layout',
29
+ tags: ['!autodocs'],
30
+ parameters: {
31
+ controls: { disable: true },
32
+ actions: { disable: true },
33
+ a11y: { disable: true },
34
+ knobs: { disable: true },
35
+ },
36
+ } satisfies Meta<ListItemProps>;
37
+ type Story = StoryObj<ListItemProps>;
38
+
39
+ const variants = [
40
+ <ListItem
41
+ key="button"
42
+ title="Button only"
43
+ control={
44
+ <ListItem.Button priority="secondary-neutral" onClick={() => {}}>
45
+ Click me
46
+ </ListItem.Button>
47
+ }
48
+ />,
49
+ <ListItem
50
+ key="media"
51
+ title="With media"
52
+ subtitle="Short subtitle"
53
+ additionalInfo={<ListItem.AdditionalInfo>{lorem5}</ListItem.AdditionalInfo>}
54
+ media={
55
+ <ListItem.AvatarView>
56
+ <FastFlag />
57
+ </ListItem.AvatarView>
58
+ }
59
+ />,
60
+ <ListItem key="value" title="With value" valueTitle="100 GBP" valueSubtitle="100 USD" />,
61
+ <ListItem
62
+ key="prompt"
63
+ title="With prompt"
64
+ prompt={
65
+ <ListItem.Prompt sentiment="positive">
66
+ This is a prompt with <Link href="https://wise.com">a link</Link>.
67
+ </ListItem.Prompt>
68
+ }
69
+ />,
70
+ <ListItem
71
+ key="button-media"
72
+ title="Button + media"
73
+ media={
74
+ <ListItem.AvatarView>
75
+ <MultiCurrency />
76
+ </ListItem.AvatarView>
77
+ }
78
+ control={
79
+ <ListItem.Button priority="primary" onClick={() => {}}>
80
+ Action
81
+ </ListItem.Button>
82
+ }
83
+ />,
84
+ <ListItem
85
+ key="button-value"
86
+ title="Button + value"
87
+ valueTitle="42 EUR"
88
+ control={
89
+ <ListItem.Button priority="secondary-neutral" onClick={() => {}}>
90
+ Pay
91
+ </ListItem.Button>
92
+ }
93
+ />,
94
+ <ListItem
95
+ key="media-value"
96
+ valueColumnWidth={80}
97
+ title="Media + value"
98
+ subtitle="this column is 20% wide"
99
+ media={
100
+ <ListItem.AvatarView>
101
+ <Bank />
102
+ </ListItem.AvatarView>
103
+ }
104
+ valueTitle="1,000 USD"
105
+ />,
106
+ <ListItem
107
+ key="prompt-value"
108
+ title="Prompt + value"
109
+ valueTitle="10 GBP"
110
+ prompt={<ListItem.Prompt sentiment="warning">Warning prompt!</ListItem.Prompt>}
111
+ />,
112
+ <ListItem
113
+ key="media-prompt"
114
+ title="Media + prompt"
115
+ media={
116
+ <ListItem.AvatarView>
117
+ <Receipt />
118
+ </ListItem.AvatarView>
119
+ }
120
+ prompt={<ListItem.Prompt sentiment="positive">Discount available!</ListItem.Prompt>}
121
+ />,
122
+ <ListItem
123
+ key="most"
124
+ title="Everything but button"
125
+ subtitle="No control"
126
+ media={
127
+ <ListItem.AvatarView>
128
+ <Savings />
129
+ </ListItem.AvatarView>
130
+ }
131
+ valueTitle="999 GBP"
132
+ prompt={
133
+ <ListItem.Prompt sentiment="positive">
134
+ <Link href="https://wise.com">See details</Link>
135
+ </ListItem.Prompt>
136
+ }
137
+ />,
138
+ <ListItem
139
+ key="all"
140
+ title="All together"
141
+ subtitle="Everything in one"
142
+ media={
143
+ <ListItem.AvatarView>
144
+ <Savings />
145
+ </ListItem.AvatarView>
146
+ }
147
+ valueTitle="999 GBP"
148
+ prompt={
149
+ <ListItem.Prompt sentiment="positive">
150
+ <Link href="https://wise.com">See details</Link>
151
+ </ListItem.Prompt>
152
+ }
153
+ control={
154
+ <ListItem.Button priority="secondary-neutral" onClick={() => {}}>
155
+ Go
156
+ </ListItem.Button>
157
+ }
158
+ />,
159
+ ];
160
+
161
+ export const Under320: Story = {
162
+ render: () => <>{variants}</>,
163
+ decorators: [withSizedContainer(320)],
164
+ };
165
+
166
+ export const Between321And399: Story = {
167
+ render: () => <>{variants}</>,
168
+ decorators: [withSizedContainer(360)],
169
+ };
170
+
171
+ export const Over400: Story = {
172
+ render: () => <>{variants}</>,
173
+ decorators: [withSizedContainer(400)],
174
+ };
175
+
176
+ export const GapsBetweenItems: Story = {
177
+ render: () => {
178
+ const props = {
179
+ title: lorem5,
180
+ subtitle: lorem10,
181
+ media: MEDIA.image,
182
+ control: CONTROLS.switch,
183
+ };
184
+ return (
185
+ <List>
186
+ <ListItem {...props} />
187
+ <ListItem {...props} spotlight="active" />
188
+ <ListItem {...props} spotlight="inactive" />
189
+ <ListItem {...props} />
190
+ </List>
191
+ );
192
+ },
193
+ };
194
+
195
+ type PreviewStoryArgs = ListItemProps & {
196
+ previewImageSize: ListItemMediaSize;
197
+ previewWithLineGuides: boolean;
198
+ previewPrompt: boolean | ListItemProps['prompt'];
199
+ };
200
+
201
+ const previewArgGroup = {
202
+ category: 'Preview options',
203
+ type: {
204
+ summary: undefined,
205
+ },
206
+ };
207
+
208
+ const previewArgTypes = {
209
+ previewImageSize: {
210
+ options: [32, 40, 48, 56, 72],
211
+ control: {
212
+ type: 'inline-radio',
213
+ },
214
+ name: 'Preview with image size',
215
+ table: previewArgGroup,
216
+ },
217
+ previewWithLineGuides: {
218
+ control: {
219
+ type: 'boolean',
220
+ },
221
+ name: 'Preview with line guides',
222
+ table: previewArgGroup,
223
+ },
224
+ previewPrompt: {
225
+ control: {
226
+ type: 'boolean',
227
+ },
228
+ mapping: {
229
+ true: <ListItem.Prompt sentiment="positive">This is a prompt</ListItem.Prompt>,
230
+ false: null,
231
+ },
232
+ name: 'Preview with `prompt`',
233
+ table: previewArgGroup,
234
+ },
235
+ } as const;
236
+
237
+ const getPropsForPreview = (args: PreviewStoryArgs) => {
238
+ const { previewImageSize, previewWithLineGuides, previewPrompt, ...props } = args as {
239
+ previewImageSize: ListItemMediaSize;
240
+ previewWithLineGuides: boolean;
241
+ previewPrompt: boolean | ListItemProps['prompt'];
242
+ title: ListItemProps['title'];
243
+ subtitle: ListItemProps['subtitle'];
244
+ [key: string]: any;
245
+ };
246
+
247
+ return [
248
+ {
249
+ ...props,
250
+ prompt: previewPrompt,
251
+ },
252
+ {
253
+ previewImageSize,
254
+ previewWithLineGuides,
255
+ },
256
+ ] as const;
257
+ };
258
+
259
+ // Create a completely untyped story object
260
+ const ImageAlignmentStory: StoryObj<PreviewStoryArgs> = {
261
+ parameters: {
262
+ controls: { disable: false },
263
+ },
264
+ args: {
265
+ title: lorem5,
266
+ subtitle: lorem10,
267
+ previewImageSize: 48,
268
+ previewWithLineGuides: true,
269
+ previewPrompt: false,
270
+ },
271
+ argTypes: previewArgTypes,
272
+ decorators: [
273
+ (Story: any, { args }: { args: PreviewStoryArgs }) => (
274
+ <>
275
+ {args.previewWithLineGuides && (
276
+ <style
277
+ dangerouslySetInnerHTML={{
278
+ __html: `
279
+ .wds-list-item-media-image-wrapper::before{
280
+ content: '';
281
+ border: 1px dashed blue;
282
+ position: absolute;
283
+ width: ${args.previewImageSize}px;
284
+ height: ${args.previewImageSize}px;
285
+ }
286
+ .wds-list-item-media-image-wrapper::after {
287
+ content: '';
288
+ border: 1px dashed red;
289
+ position: absolute;
290
+ left: 0;
291
+ width: 100%;
292
+ height: ${args.previewImageSize}px;
293
+ }
294
+ `,
295
+ }}
296
+ />
297
+ )}
298
+ <List
299
+ className="list-unstyled"
300
+ style={{ display: 'grid', gridTemplateColumns: '340px 400px', gap: '16px' }}
301
+ >
302
+ <Story />
303
+ </List>
304
+ </>
305
+ ),
306
+ ],
307
+ render: (args: PreviewStoryArgs) => {
308
+ const [{ title, subtitle, ...props }, previewProps] = getPropsForPreview(args);
309
+
310
+ const additionalInfo = <ListItem.AdditionalInfo>{lorem20}</ListItem.AdditionalInfo>;
311
+ const control = CONTROLS.button;
312
+ const size = previewProps.previewImageSize;
313
+
314
+ const instances = [
315
+ { title, ...props },
316
+ { title, subtitle, ...props },
317
+ { title, subtitle, additionalInfo, ...props },
318
+ { title, control, ...props },
319
+ { title, subtitle, control, ...props },
320
+ { title, subtitle, additionalInfo, control, ...props },
321
+ ] as const;
322
+
323
+ return (
324
+ <>
325
+ {instances.map((itemProps, index) => (
326
+ <Fragment key={`landscape-${index}`}>
327
+ <ListItem
328
+ {...itemProps}
329
+ media={<ListItem.Image size={size} src={landscapeImage} alt="landscape image" />}
330
+ />
331
+ <ListItem
332
+ {...itemProps}
333
+ media={<ListItem.Image size={size} src={landscapeImage} alt="landscape image" />}
334
+ />
335
+ </Fragment>
336
+ ))}
337
+ {instances.map((itemProps, index) => (
338
+ <Fragment key={`portrait-${index}`}>
339
+ <ListItem
340
+ {...itemProps}
341
+ media={<ListItem.Image size={size} src={portraitImage} alt="portrait image" />}
342
+ />
343
+ <ListItem
344
+ {...itemProps}
345
+ media={<ListItem.Image size={size} src={portraitImage} alt="portrait image" />}
346
+ />
347
+ </Fragment>
348
+ ))}
349
+ </>
350
+ );
351
+ },
352
+ };
353
+
354
+ export const ImageAlignment = ImageAlignmentStory;