@transferwise/components 0.0.0-experimental-15914a5 → 0.0.0-experimental-e1870f3

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 (99) hide show
  1. package/build/flowNavigation/animatedLabel/AnimatedLabel.js +2 -2
  2. package/build/flowNavigation/animatedLabel/AnimatedLabel.js.map +1 -1
  3. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs +2 -2
  4. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs.map +1 -1
  5. package/build/index.js +2 -2
  6. package/build/index.mjs +1 -1
  7. package/build/listItem/AvatarLayout/ListItemAvatarLayout.js +0 -2
  8. package/build/listItem/AvatarLayout/ListItemAvatarLayout.js.map +1 -1
  9. package/build/listItem/AvatarLayout/ListItemAvatarLayout.mjs +0 -2
  10. package/build/listItem/AvatarLayout/ListItemAvatarLayout.mjs.map +1 -1
  11. package/build/listItem/AvatarView/ListItemAvatarView.js +0 -2
  12. package/build/listItem/AvatarView/ListItemAvatarView.js.map +1 -1
  13. package/build/listItem/AvatarView/ListItemAvatarView.mjs +0 -2
  14. package/build/listItem/AvatarView/ListItemAvatarView.mjs.map +1 -1
  15. package/build/listItem/Image/ListItemImage.js +1 -3
  16. package/build/listItem/Image/ListItemImage.js.map +1 -1
  17. package/build/listItem/Image/ListItemImage.mjs +1 -3
  18. package/build/listItem/Image/ListItemImage.mjs.map +1 -1
  19. package/build/{legacylistItem → listItem}/List.js +3 -2
  20. package/build/listItem/List.js.map +1 -0
  21. package/build/{legacylistItem → listItem}/List.mjs +3 -2
  22. package/build/listItem/List.mjs.map +1 -0
  23. package/build/listItem/ListItem.js +14 -16
  24. package/build/listItem/ListItem.js.map +1 -1
  25. package/build/listItem/ListItem.mjs +14 -16
  26. package/build/listItem/ListItem.mjs.map +1 -1
  27. package/build/listItem/ListItemContext.js.map +1 -1
  28. package/build/listItem/ListItemContext.mjs.map +1 -1
  29. package/build/listItem/Navigation/ListItemNavigation.js +1 -1
  30. package/build/listItem/Navigation/ListItemNavigation.js.map +1 -1
  31. package/build/listItem/Navigation/ListItemNavigation.mjs +1 -1
  32. package/build/listItem/Navigation/ListItemNavigation.mjs.map +1 -1
  33. package/build/main.css +99 -51
  34. package/build/styles/avatarView/AvatarView.css +3 -1
  35. package/build/styles/listItem/ListItem.css +96 -50
  36. package/build/styles/listItem/ListItem.grid.css +9 -15
  37. package/build/styles/main.css +99 -51
  38. package/build/types/index.d.ts +4 -4
  39. package/build/types/index.d.ts.map +1 -1
  40. package/build/types/legacylistItem/index.d.ts +0 -2
  41. package/build/types/legacylistItem/index.d.ts.map +1 -1
  42. package/build/types/listItem/AvatarLayout/ListItemAvatarLayout.d.ts.map +1 -1
  43. package/build/types/listItem/AvatarView/ListItemAvatarView.d.ts.map +1 -1
  44. package/build/types/listItem/Image/ListItemImage.d.ts.map +1 -1
  45. package/build/types/listItem/List.d.ts +7 -0
  46. package/build/types/listItem/List.d.ts.map +1 -0
  47. package/build/types/listItem/ListItem.d.ts.map +1 -1
  48. package/build/types/listItem/ListItemContext.d.ts +0 -4
  49. package/build/types/listItem/ListItemContext.d.ts.map +1 -1
  50. package/build/types/listItem/index.d.ts +2 -0
  51. package/build/types/listItem/index.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/avatarView/AvatarView.css +3 -1
  54. package/src/avatarView/AvatarView.less +2 -1
  55. package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +1 -1
  56. package/src/index.ts +9 -4
  57. package/src/legacylistItem/LegacyListItem.story.tsx +2 -2
  58. package/src/legacylistItem/LegacyListItem.tests.story.tsx +2 -1
  59. package/src/legacylistItem/index.ts +0 -2
  60. package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +5 -4
  61. package/src/listItem/AvatarLayout/ListItemAvatarLayout.tsx +0 -3
  62. package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +5 -4
  63. package/src/listItem/AvatarView/ListItemAvatarView.tsx +0 -3
  64. package/src/listItem/Button/ListItemButton.spec.tsx +35 -10
  65. package/src/listItem/Button/ListItemButton.story.tsx +9 -8
  66. package/src/listItem/IconButton/ListItemIconButton.story.tsx +8 -8
  67. package/src/listItem/Image/ListItemImage.spec.tsx +5 -8
  68. package/src/listItem/Image/ListItemImage.story.tsx +6 -23
  69. package/src/listItem/Image/ListItemImage.tsx +1 -4
  70. package/src/listItem/List.tsx +11 -0
  71. package/src/listItem/ListItem.css +96 -50
  72. package/src/listItem/ListItem.grid.css +9 -15
  73. package/src/listItem/ListItem.grid.less +14 -22
  74. package/src/listItem/ListItem.less +111 -46
  75. package/src/listItem/ListItem.tsx +18 -24
  76. package/src/listItem/ListItemContext.tsx +0 -5
  77. package/src/listItem/Navigation/ListItemNavigation.tsx +1 -1
  78. package/src/listItem/Prompt/ListItemPrompt.story.tsx +7 -7
  79. package/src/listItem/Switch/ListItemSwitch.story.tsx +3 -2
  80. package/src/listItem/_stories/ListItem.scenarios.story.tsx +226 -0
  81. package/src/listItem/_stories/ListItem.story.tsx +45 -7
  82. package/src/listItem/_stories/ListItem.variants.test.story.tsx +7 -11
  83. package/src/listItem/index.ts +3 -0
  84. package/src/main.css +99 -51
  85. package/build/legacylistItem/List.js.map +0 -1
  86. package/build/legacylistItem/List.mjs.map +0 -1
  87. package/build/listItem/useListItemMedia.js +0 -21
  88. package/build/listItem/useListItemMedia.js.map +0 -1
  89. package/build/listItem/useListItemMedia.mjs +0 -19
  90. package/build/listItem/useListItemMedia.mjs.map +0 -1
  91. package/build/types/legacylistItem/List.d.ts +0 -6
  92. package/build/types/legacylistItem/List.d.ts.map +0 -1
  93. package/build/types/listItem/test-utils.d.ts +0 -7
  94. package/build/types/listItem/test-utils.d.ts.map +0 -1
  95. package/build/types/listItem/useListItemMedia.d.ts +0 -6
  96. package/build/types/listItem/useListItemMedia.d.ts.map +0 -1
  97. package/src/legacylistItem/List.tsx +0 -10
  98. package/src/listItem/test-utils.tsx +0 -33
  99. package/src/listItem/useListItemMedia.tsx +0 -13
@@ -7,7 +7,7 @@
7
7
  border-radius: var(--radius-medium);
8
8
  background-color: var(--color-background-screen);
9
9
  position: relative;
10
- padding: var(--size-12);
10
+ padding: var(--size-12) 0;
11
11
  container-type: inline-size;
12
12
 
13
13
  & + .wds-list-item-spotlight,
@@ -47,12 +47,6 @@
47
47
  outline: none;
48
48
  }
49
49
 
50
- &:has(.wds-list-item-control:focus-visible),
51
- &:has(input[type="checkbox"]:focus-visible) {
52
- .ring();
53
- outline-offset: -1px;
54
- }
55
-
56
50
  .wds-list-item-additional-info {
57
51
  .ring-offset-0
58
52
  }
@@ -76,32 +70,88 @@
76
70
  }
77
71
  }
78
72
 
79
- &:not(.disabled, :disabled):hover {
80
- background-color: var(--color-background-screen-hover);
81
-
82
- .wds-list-item-control-wrapper {
83
- .wds-Button {
84
- background-color: var(--Button-background-hover);
85
- color: var(--Button-color-hover);
86
- transition: none;
87
- }
73
+ // spotlighted list item has extra horizontal padding so the focus
74
+ // and hover styles can be rendered within the main container.
75
+ &.wds-list-item-spotlight {
76
+ &:has(.wds-list-item-control:focus-visible),
77
+ &:has(input[type="checkbox"]:focus-visible) {
78
+ .ring();
79
+ outline-offset: -1px;
80
+ }
81
+
82
+ &:not(.disabled, :disabled) {
83
+ &:hover {
84
+ background-color: var(--color-background-screen-hover);
88
85
  }
86
+
87
+ &:active {
88
+ background-color: var(--color-background-screen-active);
89
+ }
90
+ }
91
+
92
+ &:has(.wds-list-item-prompt:hover a, .wds-list-item-prompt:hover button) {
93
+ &:hover {
94
+ background-color: unset;
95
+ }
96
+ }
89
97
  }
90
98
 
91
- &:not(.disabled, :disabled):active {
92
- background-color: var(--color-background-screen-active);
99
+ // non-spotlighted list item has no horizontal padding so the focus
100
+ // and hover styles have to be rendered out of bounds.
101
+ &:not(.wds-list-item-spotlight) {
102
+ &:has(.wds-list-item-control:focus-visible),
103
+ &:has(input[type="checkbox"]:focus-visible) {
104
+ &:before {
105
+ .ring();
106
+ content: '';
107
+ position: absolute;
108
+ inset: 0 -8px;
109
+ border-radius: var(--radius-medium);
110
+ outline-offset: -1px;
111
+ }
112
+ }
113
+
114
+ &:not(.disabled, :disabled) {
115
+ &:hover,
116
+ &:active {
117
+ &:before {
118
+ content: '';
119
+ position: absolute;
120
+ inset: 0 -8px;
121
+ border-radius: var(--radius-medium);
122
+ }
123
+ }
124
+
125
+ &:hover:before {
126
+ background-color: var(--color-background-screen-hover);
127
+ }
128
+
129
+ &:active:before {
130
+ background-color: var(--color-background-screen-active);
131
+ }
132
+ }
133
+
134
+ &:has(.wds-list-item-prompt:hover a, .wds-list-item-prompt:hover button) {
135
+ &:hover:before {
136
+ background-color: unset;
137
+ }
138
+ }
93
139
  }
94
140
 
95
- .wds-list-item-button-control {
141
+ &:not(.disabled, :disabled):hover {
142
+ .wds-list-item-control-wrapper {
96
143
  .wds-Button {
144
+ background-color: var(--Button-background-hover);
145
+ color: var(--Button-color-hover);
97
146
  transition: none;
98
147
  }
99
148
  }
149
+ }
100
150
 
101
- &:has(.wds-list-item-prompt:hover a, .wds-list-item-prompt:hover button) {
102
- &:hover {
103
- background-color: unset;
104
- }
151
+ .wds-list-item-button-control {
152
+ .wds-Button {
153
+ transition: none;
154
+ }
105
155
  }
106
156
  }
107
157
 
@@ -109,8 +159,8 @@
109
159
  grid-area: media;
110
160
 
111
161
  &-image {
112
- width: var(--wds-list-item-control-wrapper-height);
113
- height: var(--wds-list-item-control-wrapper-height);
162
+ width: var(--item-media-image-size);
163
+ height: var(--item-media-image-size);
114
164
  }
115
165
  }
116
166
 
@@ -123,18 +173,8 @@
123
173
  word-break: break-word;
124
174
  }
125
175
 
126
- &-title {
127
- color: var(--color-content-primary);
128
- }
129
-
130
- &-value {
131
- text-align: right;
132
- }
133
-
134
- &-body-center {
135
- display: flex;
136
- flex-direction: row;
137
- align-items: center;
176
+ &-title, &-title-value {
177
+ color: var(--color-content-primary);
138
178
  }
139
179
 
140
180
  &-additional-info {
@@ -143,16 +183,13 @@
143
183
  margin-top: calc(var(--size-4) * -1);
144
184
  }
145
185
 
146
- &-control-wrapper {
147
- grid-area: control;
148
- align-content: center;
149
- max-height: var(--wds-list-item-control-wrapper-height);
186
+ &-value {
187
+ align-items: end;
188
+ text-align: right;
150
189
  }
151
190
 
152
- &-navigation {
153
- .tw-icon-chevron-right {
154
- color: var(--color-interactive-secondary);
155
- }
191
+ &-control-wrapper {
192
+ grid-area: control;
156
193
  }
157
194
 
158
195
  &-control {
@@ -175,6 +212,9 @@
175
212
 
176
213
 
177
214
  &-spotlight {
215
+ padding-left: var(--size-12);
216
+ padding-right: var(--size-12);
217
+
178
218
  &-active {
179
219
  background-color: var(--color-background-neutral);
180
220
  &:not(.disabled, :disabled):hover {
@@ -187,7 +227,7 @@
187
227
 
188
228
  &-inactive {
189
229
  background-color: color-mix(in srgb, var(--color-background-neutral) 25%, transparent);
190
- border: 1px dashed var(--color-border-neutral);
230
+
191
231
  &:not(.disabled, :disabled):hover {
192
232
  background-color: color-mix(in srgb, var(--color-background-neutral-hover) 25%, transparent);
193
233
  }
@@ -197,6 +237,31 @@
197
237
  }
198
238
  }
199
239
 
240
+ .wds-list-item-spotlight__border{
241
+ position: absolute;
242
+ inset:0;
243
+ width: 100%;
244
+ height: 100%;
245
+
246
+ rect {
247
+ --wds-list-item-spotlight-borderWidth: 1px;
248
+ --wds-list-item-spotlight-borderWidthOffset: .5px; // half of the border width to center the border
249
+ --wds-list-item-spotlight-strokeDashSize: calc(var(--size-12) * .5);
250
+
251
+ fill: none;
252
+ stroke: var(--color-border-neutral);
253
+ width: calc(100% - var(--wds-list-item-spotlight-borderWidth));
254
+ height: calc(100% - var(--wds-list-item-spotlight-borderWidth));
255
+ x: var(--wds-list-item-spotlight-borderWidthOffset);
256
+ y: var(--wds-list-item-spotlight-borderWidthOffset);
257
+ rx: calc(var(--radius-medium) - var(--wds-list-item-spotlight-borderWidthOffset));
258
+ ry: calc(var(--radius-medium) - var(--wds-list-item-spotlight-borderWidthOffset));
259
+ stroke-dasharray:
260
+ var(--wds-list-item-spotlight-strokeDashSize)
261
+ var(--wds-list-item-spotlight-strokeDashSize);
262
+ }
263
+ }
264
+
200
265
  &-prompt {
201
266
  grid-area: prompt;
202
267
 
@@ -4,7 +4,6 @@ import {
4
4
  useId,
5
5
  useMemo,
6
6
  useState,
7
- useEffect,
8
7
  type PropsWithChildren,
9
8
  type ReactNode,
10
9
  } from 'react';
@@ -23,11 +22,7 @@ import { AvatarView } from './AvatarView';
23
22
  import { Image } from './Image';
24
23
  import { Prompt } from './Prompt';
25
24
  import { PrimitiveAnchor } from '../primitives';
26
- import {
27
- ListItemContext,
28
- type ListItemContextData,
29
- type ListItemMediaSize,
30
- } from './ListItemContext';
25
+ import { ListItemContext, type ListItemContextData } from './ListItemContext';
31
26
 
32
27
  export type ListItemTypes =
33
28
  | 'non-interactive'
@@ -124,8 +119,6 @@ export const ListItem = ({
124
119
  const idPrefix = useId();
125
120
  const [controlProps, setControlProps] = useState<ListItemControlProps>({});
126
121
  const [controlType, setControlType] = useState<ListItemTypes>('non-interactive');
127
- const [mediaSize, setMediaSize] = useState<ListItemMediaSize | undefined>();
128
-
129
122
  const ids: ListItemContextData['ids'] = {
130
123
  title: `${idPrefix}_title`,
131
124
  ...(subtitle ? { subtitle: `${idPrefix}_subtitle` } : {}),
@@ -163,13 +156,11 @@ export const ListItem = ({
163
156
  () => ({
164
157
  setControlType,
165
158
  setControlProps,
166
- setMediaSize,
167
159
  ids,
168
160
  props: { disabled, inverted },
169
- mediaSize,
170
161
  describedByIds,
171
162
  }),
172
- [describedByIds, mediaSize],
163
+ [describedByIds],
173
164
  );
174
165
  const gridColumnsStyle = {
175
166
  '--wds-list-item-body-left': valueColumnWidth ? `${100 - valueColumnWidth}fr` : '50fr',
@@ -236,6 +227,12 @@ export const ListItem = ({
236
227
  aria-disabled={disabled}
237
228
  aria-labelledby={ariaLabelledby}
238
229
  >
230
+ {spotlight === 'inactive' && (
231
+ <svg aria-hidden="true" className="wds-list-item-spotlight__border">
232
+ <rect />
233
+ </svg>
234
+ )}
235
+
239
236
  <View
240
237
  {...{
241
238
  isPartiallyInteractive,
@@ -256,11 +253,7 @@ export const ListItem = ({
256
253
  style={valueColumnWidth ? gridColumnsStyle : undefined}
257
254
  >
258
255
  {/* Title + Subtitle + Values - Group */}
259
- <span
260
- className={clsx({
261
- 'wds-list-item-body-center': title && !subtitle,
262
- })}
263
- >
256
+ <span>
264
257
  {(() => {
265
258
  const titles = [
266
259
  <Body
@@ -287,8 +280,6 @@ export const ListItem = ({
287
280
  <span
288
281
  className={clsx('wds-list-item-value', {
289
282
  'flex-column': valueTitle !== undefined || valueSubtitle !== undefined,
290
- 'wds-list-item-body-center':
291
- (valueTitle && !valueSubtitle) || (!valueTitle && valueSubtitle),
292
283
  })}
293
284
  >
294
285
  {(() => {
@@ -327,11 +318,6 @@ export const ListItem = ({
327
318
  className={clsx('wds-list-item-control-wrapper', {
328
319
  'wds-list-item-button-control': controlType === 'button',
329
320
  })}
330
- style={
331
- {
332
- '--wds-list-item-control-wrapper-height': mediaSize ? `${mediaSize}px` : 'auto',
333
- } as React.CSSProperties
334
- }
335
321
  >
336
322
  {control}
337
323
  </Body>
@@ -384,6 +370,7 @@ function View({
384
370
  href={(controlProps as ListItemNavigationProps)?.href}
385
371
  target={(controlProps as ListItemNavigationProps)?.target}
386
372
  className={clsx('wds-list-item-view d-flex flex-row', {
373
+ 'align-items-center': !subtitle,
387
374
  'wds-list-item-control': controlType === 'navigation',
388
375
  })}
389
376
  disabled={disabled}
@@ -399,7 +386,13 @@ function View({
399
386
  if (isPartiallyInteractive || controlType === 'non-interactive') {
400
387
  return (
401
388
  <div className={clsx('wds-list-item-gridWrapper', className)}>
402
- <div className={clsx('wds-list-item-view d-flex flex-row')}>{children}</div>
389
+ <div
390
+ className={clsx('wds-list-item-view d-flex flex-row', {
391
+ 'align-items-center': !subtitle,
392
+ })}
393
+ >
394
+ {children}
395
+ </div>
403
396
 
404
397
  {renderExtras()}
405
398
  </div>
@@ -414,6 +407,7 @@ function View({
414
407
  <label
415
408
  htmlFor={ids.control}
416
409
  className={clsx('wds-list-item-view', {
410
+ 'align-items-center': !subtitle,
417
411
  clickable: !disabled,
418
412
  fullyInteractive: !isPartiallyInteractive,
419
413
  })}
@@ -1,13 +1,9 @@
1
1
  import { createContext } from 'react';
2
2
  import type { ListItemTypes, ListItemControlProps, Props as ListItemProps } from './ListItem';
3
- import type { ListItemAvatarViewProps } from './AvatarView';
4
-
5
- export type ListItemMediaSize = 32 | 40 | 48 | 56 | 72;
6
3
 
7
4
  export type ListItemContextData = {
8
5
  setControlType: (type: ListItemTypes) => void;
9
6
  setControlProps: (props: ListItemControlProps) => void;
10
- setMediaSize: (size: ListItemMediaSize | undefined) => void;
11
7
  ids: {
12
8
  title: string;
13
9
  subtitle?: string;
@@ -18,7 +14,6 @@ export type ListItemContextData = {
18
14
  prompt?: string;
19
15
  };
20
16
  props: Pick<ListItemProps, 'disabled' | 'inverted'>;
21
- mediaSize?: ListItemAvatarViewProps['size'];
22
17
  describedByIds: string;
23
18
  };
24
19
 
@@ -19,7 +19,7 @@ export const Navigation = function Navigation({ href = '#', ...props }: ListItem
19
19
  const { baseItemProps } = useListItemControl('navigation', { href, ...props });
20
20
  const { ids, describedByIds } = useContext(ListItemContext);
21
21
  const isLink = href !== '#';
22
- const icon = <ChevronRight size={16} />;
22
+ const icon = <ChevronRight size={24} />;
23
23
 
24
24
  if (baseItemProps.disabled) return <BackslashCircle size={24} />;
25
25
 
@@ -3,7 +3,7 @@ import { action } from 'storybook/actions';
3
3
  import { lorem10, lorem5 } from '../../test-utils';
4
4
  import Link from '../../link';
5
5
  import { Sentiment as Sentiments } from '../../common';
6
- import { ListItem } from '../..';
6
+ import { List, ListItem } from '../..';
7
7
  import { Prompt, ListItemPromptProps } from './ListItemPrompt';
8
8
  import {
9
9
  SB_LIST_ITEM_CONTROLS as CONTROLS,
@@ -74,7 +74,7 @@ export const Sentiment: Story = {
74
74
  knobs: { disable: true },
75
75
  },
76
76
  render: (args) => (
77
- <ol className="list-unstyled">
77
+ <List as="ol">
78
78
  <ListItem
79
79
  title={lorem5}
80
80
  subtitle={lorem10}
@@ -103,7 +103,7 @@ export const Sentiment: Story = {
103
103
  control={CONTROLS.switch}
104
104
  prompt={<Prompt sentiment={Sentiments.NEGATIVE}>This is a negative prompt.</Prompt>}
105
105
  />
106
- </ol>
106
+ </List>
107
107
  ),
108
108
  };
109
109
 
@@ -122,7 +122,7 @@ export const Interactivity: Story = {
122
122
  },
123
123
  },
124
124
  render: (args) => (
125
- <ol className="list-unstyled">
125
+ <List as="ol">
126
126
  <ListItem
127
127
  title={lorem5}
128
128
  subtitle={lorem10}
@@ -154,7 +154,7 @@ export const Interactivity: Story = {
154
154
  </Prompt>
155
155
  }
156
156
  />
157
- </ol>
157
+ </List>
158
158
  ),
159
159
  };
160
160
 
@@ -172,7 +172,7 @@ export const Sizing: Story = {
172
172
  },
173
173
  },
174
174
  render: (args) => (
175
- <ol className="list-unstyled">
175
+ <List as="ol">
176
176
  <ListItem
177
177
  title={lorem5}
178
178
  subtitle={lorem10}
@@ -196,6 +196,6 @@ export const Sizing: Story = {
196
196
  </ListItem.Prompt>
197
197
  }
198
198
  />
199
- </ol>
199
+ </List>
200
200
  ),
201
201
  };
@@ -8,6 +8,7 @@ import {
8
8
  import type { ListItemSwitchProps } from './ListItemSwitch';
9
9
  import { fn } from 'storybook/test';
10
10
  import { lorem10, lorem5 } from '../../test-utils';
11
+ import { List } from '../List';
11
12
 
12
13
  const meta: Meta<ListItemSwitchProps> = {
13
14
  component: ListItem.Switch,
@@ -50,7 +51,7 @@ export const Playground: Story = {
50
51
  export const States: Story = {
51
52
  render: (args) => {
52
53
  return (
53
- <ol className="list-unstyled">
54
+ <List as="ol">
54
55
  <ListItem
55
56
  title={lorem5}
56
57
  subtitle={lorem10}
@@ -63,7 +64,7 @@ export const States: Story = {
63
64
  media={MEDIA.avatarSingle}
64
65
  control={<ListItem.Switch {...args} />}
65
66
  />
66
- </ol>
67
+ </List>
67
68
  );
68
69
  },
69
70
  };
@@ -0,0 +1,226 @@
1
+ import { useState } from 'react';
2
+ import { Title, Subtitle, Description, Stories } from '@storybook/addon-docs/blocks';
3
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
4
+ import { action } from 'storybook/actions';
5
+ import { InfoCircle, Documents } from '@transferwise/icons';
6
+ import { lorem10, lorem100, lorem20, lorem5 } from '../../test-utils';
7
+ import Modal from '../../modal';
8
+ import Link from '../../link';
9
+ import Popover from '../../popover';
10
+ import { SnackbarConsumer, type SnackbarContextType } from '../../snackbar/SnackbarContext';
11
+ import SnackbarProvider from '../../snackbar/SnackbarProvider';
12
+ import Tooltip from '../../tooltip';
13
+ import { Position } from '../../common';
14
+ import { ListItem, type Props as ItemProps } from '../ListItem';
15
+ import {
16
+ SB_LIST_ITEM_ADDITIONAL_INFO as INFO,
17
+ SB_LIST_ITEM_CONTROLS as CONTROLS,
18
+ SB_LIST_ITEM_MEDIA as MEDIA,
19
+ SB_LIST_ITEM_PROMPTS as PROMPTS,
20
+ } from './subcomponents';
21
+
22
+ /**
23
+ * Recent storybook versions render the full source including all story parameters which
24
+ * creates noise, this simple workaround only shows the render function.
25
+ * This is a workaround for the issue.
26
+ * @see https://github.com/storybookjs/storybook/issues/22281
27
+ */
28
+ const storySourceWithoutNoise = (config: Story): Story => {
29
+ const newConfig: Story = {
30
+ ...config,
31
+ parameters: {
32
+ ...config.parameters,
33
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
34
+ docs: {
35
+ ...config.parameters?.docs,
36
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
37
+ source: {
38
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
39
+ ...config.parameters?.docs?.source,
40
+ type: 'code' as const,
41
+ transform: (code: string) =>
42
+ // eslint-disable-next-line regexp/no-super-linear-backtracking
43
+ code.replace(/storySourceWithoutNoise\(\{\s+render\s?:\s+(.*)\}\)$/gms, '$1'),
44
+ },
45
+ },
46
+ },
47
+ };
48
+
49
+ return newConfig;
50
+ };
51
+
52
+ export default {
53
+ component: ListItem,
54
+ title: 'Content/ListItem/common scenarios',
55
+ tags: ['autodocs'],
56
+ args: {
57
+ title: lorem5,
58
+ subtitle: lorem10,
59
+ media: MEDIA.image,
60
+ additionalInfo: INFO.nonInteractive,
61
+ prompt: PROMPTS.interactive,
62
+ control: CONTROLS.button,
63
+ },
64
+ parameters: {
65
+ docs: {
66
+ toc: true,
67
+ page: () => (
68
+ <>
69
+ <Title>Common scenarios</Title>
70
+ <Subtitle />
71
+ <Description />
72
+ <Stories />
73
+ </>
74
+ ),
75
+ },
76
+ },
77
+ } satisfies Meta<ItemProps>;
78
+ type Story = StoryObj<ItemProps>;
79
+
80
+ /**
81
+ * Aside from using the main control for triggering a [Modal](?path=/docs/dialogs-modal--docs),
82
+ * it's also possible to toggle it via inline-button as part of the `<ListItem.AdditionalInfo>`
83
+ * or `<ListItem.Prompt>`.
84
+ */
85
+ export const WithModals: Story = storySourceWithoutNoise({
86
+ render: function WithModals(listItemProps) {
87
+ const [isModalOpen, setIsModalOpen] = useState(false);
88
+
89
+ const handleOpening = () => {
90
+ setIsModalOpen(true);
91
+ action('Modal opening')();
92
+ };
93
+ const handleClosing = () => {
94
+ setIsModalOpen(false);
95
+ action('Modal closing')();
96
+ };
97
+
98
+ return (
99
+ <>
100
+ <ListItem
101
+ {...listItemProps}
102
+ control={
103
+ <ListItem.Button partiallyInteractive onClick={handleOpening}>
104
+ Open modal
105
+ </ListItem.Button>
106
+ }
107
+ additionalInfo={
108
+ <ListItem.AdditionalInfo
109
+ action={{ label: 'button opening a modal', onClick: handleOpening }}
110
+ >
111
+ This is additional information that contains a
112
+ </ListItem.AdditionalInfo>
113
+ }
114
+ prompt={
115
+ <ListItem.Prompt sentiment="positive">
116
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
117
+ This prompt also has a <Link onClick={handleOpening}>
118
+ button that opens a modal
119
+ </Link>{' '}
120
+ for your convenience.
121
+ </ListItem.Prompt>
122
+ }
123
+ />
124
+ <Modal body={lorem100} open={isModalOpen} onClose={handleClosing} />
125
+ </>
126
+ );
127
+ },
128
+ });
129
+
130
+ /**
131
+ * The control or inline buttons can trigger a [Popover](?path=/docs/dialogs-popover--docs).
132
+ */
133
+ export const WithPopover: Story = {
134
+ render: (listItemProps) => {
135
+ return (
136
+ <ListItem
137
+ {...listItemProps}
138
+ control={
139
+ <Popover preferredPlacement={Position.BOTTOM} title={lorem5} content={lorem20}>
140
+ <ListItem.IconButton partiallyInteractive>
141
+ <InfoCircle />
142
+ </ListItem.IconButton>
143
+ </Popover>
144
+ }
145
+ prompt={PROMPTS.nonInteractive}
146
+ />
147
+ );
148
+ },
149
+ };
150
+
151
+ /**
152
+ * The control or inline buttons can trigger a [Tooltip](?path=/docs/dialogs-tooltip--docs).
153
+ */
154
+ export const WithTooltip: Story = {
155
+ render: (listItemProps) => {
156
+ return (
157
+ <ListItem
158
+ {...listItemProps}
159
+ control={
160
+ <Tooltip position={Position.BOTTOM} label={lorem10}>
161
+ <ListItem.IconButton partiallyInteractive>
162
+ <InfoCircle />
163
+ </ListItem.IconButton>
164
+ </Tooltip>
165
+ }
166
+ prompt={PROMPTS.nonInteractive}
167
+ />
168
+ );
169
+ },
170
+ };
171
+
172
+ /**
173
+ * ListItem is often used as a simple copy and paste button augmented with a [Snackbar](?path=/docs/dialogs-snackbar--docs). <br />
174
+ *
175
+ * **NB** The snippet below uses a simplified example implementation of a clipboard handler,
176
+ * which may not be suitable or sufficient for your needs.
177
+ */
178
+ export const CopyAndPasteWithSnackbar: Story = storySourceWithoutNoise({
179
+ render: function CopyAndPaste() {
180
+ const title = 'Artie Choke';
181
+ const subtitle = 'Account holder';
182
+
183
+ const handleCopy = async (createSnackbar: SnackbarContextType['createSnackbar']) =>
184
+ navigator.clipboard
185
+ .writeText(`${subtitle}: ${title}`)
186
+ .catch((err) => {
187
+ console.error('Failed to copy text: ', err);
188
+ })
189
+ .then(() => {
190
+ createSnackbar({
191
+ text: `${subtitle} copied to clipboard.`,
192
+ });
193
+ });
194
+
195
+ return (
196
+ <ListItem
197
+ title={title}
198
+ subtitle={subtitle}
199
+ inverted
200
+ control={
201
+ <SnackbarProvider>
202
+ <SnackbarConsumer>
203
+ {({ createSnackbar }) => {
204
+ return (
205
+ <ListItem.IconButton
206
+ partiallyInteractive
207
+ onClick={async () => handleCopy(createSnackbar)}
208
+ >
209
+ <Documents />
210
+ </ListItem.IconButton>
211
+ );
212
+ }}
213
+ </SnackbarConsumer>
214
+ </SnackbarProvider>
215
+ }
216
+ media={
217
+ <ListItem.AvatarView
218
+ imgSrc="../avatar-square-dude.webp"
219
+ profileName="John Smith"
220
+ size={48}
221
+ />
222
+ }
223
+ />
224
+ );
225
+ },
226
+ });