@transferwise/components 46.103.1 → 46.104.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 (209) 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 +309 -0
  34. package/build/listItem/ListItem.js.map +1 -0
  35. package/build/listItem/ListItem.mjs +304 -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 +770 -0
  66. package/build/styles/listItem/ListItem.css +770 -0
  67. package/build/styles/listItem/ListItem.grid.css +370 -0
  68. package/build/styles/listItem/Prompt/ListItemPrompt.css +157 -0
  69. package/build/styles/main.css +770 -0
  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 +111 -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 +3 -3
  133. package/src/actionButton/ActionButton.story.tsx +1 -1
  134. package/src/avatar/Avatar.story.tsx +1 -1
  135. package/src/avatarWrapper/AvatarWrapper.story.tsx +1 -1
  136. package/src/badge/Badge.story.tsx +1 -1
  137. package/src/button/Button.story.tsx +1 -0
  138. package/src/button/LegacyButton.story.tsx +1 -1
  139. package/src/index.ts +15 -0
  140. package/src/legacylistItem/LegacyListItem.story.tsx +1 -1
  141. package/src/legacylistItem/LegacyListItem.tests.story.tsx +2 -1
  142. package/src/list/List.story.tsx +13 -3
  143. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.spec.tsx +56 -0
  144. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.story.tsx +198 -0
  145. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.tsx +36 -0
  146. package/src/listItem/AdditionalInfo/index.ts +2 -0
  147. package/src/listItem/AvatarLayout/ListItemAvatarLayout.spec.tsx +59 -0
  148. package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +124 -0
  149. package/src/listItem/AvatarLayout/ListItemAvatarLayout.tsx +27 -0
  150. package/src/listItem/AvatarLayout/index.ts +2 -0
  151. package/src/listItem/AvatarView/ListItemAvatarView.spec.tsx +75 -0
  152. package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +339 -0
  153. package/src/listItem/AvatarView/ListItemAvatarView.tsx +27 -0
  154. package/src/listItem/AvatarView/index.ts +2 -0
  155. package/src/listItem/Button/ListItemButton.spec.tsx +68 -0
  156. package/src/listItem/Button/ListItemButton.story.tsx +473 -0
  157. package/src/listItem/Button/ListItemButton.tsx +56 -0
  158. package/src/listItem/Button/index.ts +2 -0
  159. package/src/listItem/Checkbox/ListItemCheckbox.spec.tsx +82 -0
  160. package/src/listItem/Checkbox/ListItemCheckbox.story.tsx +128 -0
  161. package/src/listItem/Checkbox/ListItemCheckbox.tsx +33 -0
  162. package/src/listItem/Checkbox/index.ts +2 -0
  163. package/src/listItem/IconButton/ListItemIconButton.spec.tsx +119 -0
  164. package/src/listItem/IconButton/ListItemIconButton.story.tsx +284 -0
  165. package/src/listItem/IconButton/ListItemIconButton.tsx +73 -0
  166. package/src/listItem/IconButton/index.ts +2 -0
  167. package/src/listItem/Image/ListItemImage.spec.tsx +30 -0
  168. package/src/listItem/Image/ListItemImage.story.tsx +80 -0
  169. package/src/listItem/Image/ListItemImage.tsx +46 -0
  170. package/src/listItem/Image/index.ts +2 -0
  171. package/src/listItem/ListItem.css +770 -0
  172. package/src/listItem/ListItem.grid.css +370 -0
  173. package/src/listItem/ListItem.grid.less +622 -0
  174. package/src/listItem/ListItem.less +287 -0
  175. package/src/listItem/ListItem.spec.tsx +1511 -0
  176. package/src/listItem/ListItem.tsx +438 -0
  177. package/src/listItem/ListItemContext.tsx +26 -0
  178. package/src/listItem/Navigation/ListItemNavigation.spec.tsx +59 -0
  179. package/src/listItem/Navigation/ListItemNavigation.story.tsx +112 -0
  180. package/src/listItem/Navigation/ListItemNavigation.tsx +39 -0
  181. package/src/listItem/Navigation/index.ts +2 -0
  182. package/src/listItem/Prompt/ListItemPrompt.css +157 -0
  183. package/src/listItem/Prompt/ListItemPrompt.less +134 -0
  184. package/src/listItem/Prompt/ListItemPrompt.spec.tsx +36 -0
  185. package/src/listItem/Prompt/ListItemPrompt.story.tsx +204 -0
  186. package/src/listItem/Prompt/ListItemPrompt.tsx +32 -0
  187. package/src/listItem/Prompt/index.ts +2 -0
  188. package/src/listItem/Radio/ListItemRadio.spec.tsx +66 -0
  189. package/src/listItem/Radio/ListItemRadio.story.tsx +111 -0
  190. package/src/listItem/Radio/ListItemRadio.tsx +33 -0
  191. package/src/listItem/Radio/index.ts +2 -0
  192. package/src/listItem/Switch/ListItemSwitch.spec.tsx +47 -0
  193. package/src/listItem/Switch/ListItemSwitch.story.tsx +79 -0
  194. package/src/listItem/Switch/ListItemSwitch.tsx +33 -0
  195. package/src/listItem/Switch/index.ts +2 -0
  196. package/src/listItem/_stories/ListItem.focus.test.story.tsx +265 -0
  197. package/src/listItem/_stories/ListItem.layout.test.story.tsx +354 -0
  198. package/src/listItem/_stories/ListItem.scenarios.story.tsx +228 -0
  199. package/src/listItem/_stories/ListItem.story.tsx +774 -0
  200. package/src/listItem/_stories/ListItem.variants.test.story.tsx +271 -0
  201. package/src/listItem/_stories/helpers.tsx +53 -0
  202. package/src/listItem/_stories/subcomponents.tsx +139 -0
  203. package/src/listItem/index.ts +14 -0
  204. package/src/listItem/test-utils.tsx +33 -0
  205. package/src/listItem/useListItemControl.tsx +18 -0
  206. package/src/listItem/useListItemMedia.tsx +16 -0
  207. package/src/main.css +770 -0
  208. package/src/main.less +1 -0
  209. package/src/select/Select.story.tsx +1 -1
@@ -0,0 +1,128 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { useState } from 'react';
3
+ import List from '../../list';
4
+ import { ListItem } from '../ListItem';
5
+ import {
6
+ SB_LIST_ITEM_ADDITIONAL_INFO as INFO,
7
+ SB_LIST_ITEM_MEDIA as MEDIA,
8
+ } from '../_stories/subcomponents';
9
+ import type { ListItemCheckboxProps } from './ListItemCheckbox';
10
+ import { fn } from 'storybook/test';
11
+
12
+ const meta: Meta<ListItemCheckboxProps> = {
13
+ component: ListItem.Checkbox,
14
+ title: 'Content/ListItem/ListItem.Checkbox',
15
+ parameters: {
16
+ docs: {
17
+ toc: true,
18
+ },
19
+ },
20
+ args: {
21
+ checked: false,
22
+ indeterminate: false,
23
+ value: 'checkbox',
24
+ name: 'checkbox',
25
+ onChange: fn(),
26
+ onFocus: fn(),
27
+ onBlur: fn(),
28
+ },
29
+ argTypes: {
30
+ checked: {
31
+ control: 'boolean',
32
+ },
33
+ indeterminate: {
34
+ control: 'boolean',
35
+ },
36
+ onChange: {
37
+ table: {
38
+ type: { summary: '(event: ChangeEvent<HTMLInputElement>) => void' },
39
+ },
40
+ },
41
+ onFocus: {
42
+ table: {
43
+ type: { summary: '(event: FocusEvent<HTMLInputElement>) => void' },
44
+ },
45
+ },
46
+ onBlur: {
47
+ table: {
48
+ type: { summary: '(event: FocusEvent<HTMLInputElement>) => void' },
49
+ },
50
+ },
51
+ },
52
+ } satisfies Meta<ListItemCheckboxProps>;
53
+
54
+ export default meta;
55
+
56
+ type Story = StoryObj<ListItemCheckboxProps>;
57
+
58
+ export const Playground: Story = {
59
+ tags: ['!autodocs'],
60
+ render: (args: ListItemCheckboxProps) => {
61
+ return (
62
+ <List>
63
+ <ListItem
64
+ title="List item with checkbox"
65
+ subtitle="Select this option"
66
+ media={MEDIA.avatarSingle}
67
+ control={<ListItem.Checkbox {...args} />}
68
+ additionalInfo={INFO.nonInteractive}
69
+ />
70
+ </List>
71
+ );
72
+ },
73
+ };
74
+
75
+ /**
76
+ * Checkbox controls demonstrate different states including checked, unchecked, and indeterminate.
77
+ */
78
+ export const States: Story = {
79
+ parameters: {
80
+ controls: { disable: true },
81
+ },
82
+ render: function Render() {
83
+ const [isChecked0, setisChecked0] = useState(false);
84
+ const [isChecked1, setisChecked1] = useState(true);
85
+ const [isChecked2, setisChecked2] = useState(false);
86
+
87
+ return (
88
+ <List>
89
+ <ListItem
90
+ control={
91
+ <ListItem.Checkbox
92
+ checked={isChecked0}
93
+ onChange={() => setisChecked0((current) => !current)}
94
+ />
95
+ }
96
+ title="Unchecked option"
97
+ subtitle="This option is not selected"
98
+ media={MEDIA.avatarSingle}
99
+ />
100
+
101
+ <ListItem
102
+ control={
103
+ <ListItem.Checkbox
104
+ checked={isChecked1}
105
+ onChange={() => setisChecked1((current) => !current)}
106
+ />
107
+ }
108
+ title="Checked option"
109
+ subtitle="This option is selected"
110
+ media={MEDIA.avatarSingle}
111
+ />
112
+
113
+ <ListItem
114
+ control={
115
+ <ListItem.Checkbox
116
+ checked={isChecked2}
117
+ indeterminate
118
+ onChange={() => setisChecked2((current) => !current)}
119
+ />
120
+ }
121
+ title="Indeterminate option"
122
+ subtitle="This option is partially selected"
123
+ media={MEDIA.avatarSingle}
124
+ />
125
+ </List>
126
+ );
127
+ },
128
+ };
@@ -0,0 +1,33 @@
1
+ import { useContext } from 'react';
2
+ import CheckboxButton, { type CheckboxButtonProps } from '../../checkboxButton/CheckboxButton';
3
+ import { useListItemControl } from '../useListItemControl';
4
+ import { ListItemContext } from '../ListItemContext';
5
+
6
+ export type ListItemCheckboxProps = Pick<
7
+ CheckboxButtonProps,
8
+ 'checked' | 'indeterminate' | 'onChange' | 'onBlur' | 'onFocus' | 'value' | 'name'
9
+ >;
10
+
11
+ /**
12
+ * This component allows for rendering a checkbox control within a fully interactive ListItem. <br />It's a thin wrapper around the
13
+ * [CheckboxButton component](https://storybook.wise.design/?path=/docs/actions-checkboxbutton--docs),
14
+ * but offers only a subset of its features in line with the ListItem's constraints. <br />
15
+ *
16
+ * Please refer to the [Design documentation](https://wise.design/components/list-item---checkbox) for details.
17
+ */
18
+ export const Checkbox = function (props: ListItemCheckboxProps) {
19
+ const { baseItemProps } = useListItemControl('checkbox', { ...props });
20
+ const { ids, describedByIds } = useContext(ListItemContext);
21
+
22
+ return (
23
+ <CheckboxButton
24
+ {...props}
25
+ className="wds-list-item-control"
26
+ disabled={baseItemProps.disabled}
27
+ id={ids.control}
28
+ aria-describedby={describedByIds}
29
+ />
30
+ );
31
+ };
32
+
33
+ Checkbox.displayName = 'ListItem.Checkbox';
@@ -0,0 +1,2 @@
1
+ export type { ListItemCheckboxProps } from './ListItemCheckbox';
2
+ export { Checkbox } from './ListItemCheckbox';
@@ -0,0 +1,119 @@
1
+ import { Edit } from '@transferwise/icons';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { mockMatchMedia, render, screen } from '../../test-utils';
4
+ import { ListItem, type ListItemProps } from '../ListItem';
5
+
6
+ mockMatchMedia();
7
+
8
+ describe('ListItem.IconButton', () => {
9
+ const renderWith = (overrides: Partial<ListItemProps> = {}) =>
10
+ render(<ListItem title="Test title" {...overrides} />);
11
+
12
+ describe('as button', () => {
13
+ it('renders as button by default', () => {
14
+ renderWith({
15
+ control: (
16
+ <ListItem.IconButton>
17
+ <Edit />
18
+ </ListItem.IconButton>
19
+ ),
20
+ });
21
+ expect(screen.getByRole('button')).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders as button if onClick is provided', () => {
25
+ renderWith({
26
+ control: (
27
+ <ListItem.IconButton onClick={() => {}}>
28
+ <Edit />
29
+ </ListItem.IconButton>
30
+ ),
31
+ });
32
+
33
+ expect(screen.getByRole('button')).toBeInTheDocument();
34
+ });
35
+
36
+ it('handles onClick events', async () => {
37
+ const handleClick = jest.fn();
38
+ renderWith({
39
+ control: (
40
+ <ListItem.IconButton onClick={handleClick}>
41
+ <Edit />
42
+ </ListItem.IconButton>
43
+ ),
44
+ });
45
+
46
+ await userEvent.click(screen.getByRole('button'));
47
+ expect(handleClick).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ it('is disabled when ListItem is disabled', async () => {
51
+ const handleClick = jest.fn();
52
+ renderWith({
53
+ disabled: true,
54
+ control: (
55
+ <ListItem.IconButton onClick={handleClick}>
56
+ <Edit />
57
+ </ListItem.IconButton>
58
+ ),
59
+ });
60
+
61
+ const button = screen.getByRole('button');
62
+ expect(button).toBeDisabled();
63
+ await userEvent.click(button);
64
+ expect(handleClick).not.toHaveBeenCalled();
65
+ });
66
+ });
67
+
68
+ describe('as link', () => {
69
+ it('renders as link when href is provided', () => {
70
+ renderWith({
71
+ control: (
72
+ <ListItem.IconButton href="/test">
73
+ <Edit />
74
+ </ListItem.IconButton>
75
+ ),
76
+ });
77
+
78
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/test');
79
+ });
80
+
81
+ it('supports target attribute for links', () => {
82
+ renderWith({
83
+ control: (
84
+ <ListItem.IconButton href="/test" target="_blank">
85
+ <Edit />
86
+ </ListItem.IconButton>
87
+ ),
88
+ });
89
+
90
+ const link = screen.getByRole('link');
91
+ expect(link).toHaveAttribute('target', '_blank');
92
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
93
+ });
94
+
95
+ it('is disabled when ListItem is disabled', async () => {
96
+ renderWith({
97
+ disabled: true,
98
+ control: (
99
+ <ListItem.IconButton href="/test">
100
+ <Edit />
101
+ </ListItem.IconButton>
102
+ ),
103
+ });
104
+ expect(screen.getByRole('link')).toHaveAttribute('aria-disabled', 'true');
105
+ });
106
+ });
107
+
108
+ it('renders children content', () => {
109
+ renderWith({
110
+ control: (
111
+ <ListItem.IconButton>
112
+ <Edit />
113
+ </ListItem.IconButton>
114
+ ),
115
+ });
116
+
117
+ expect(screen.getByTestId('edit-icon')).toBeInTheDocument();
118
+ });
119
+ });
@@ -0,0 +1,284 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import ListItem from '..';
3
+ import { Edit } from '@transferwise/icons';
4
+ import { fn } from 'storybook/test';
5
+ import List from '../../list';
6
+ import {
7
+ SB_LIST_ITEM_ADDITIONAL_INFO as INFO,
8
+ SB_LIST_ITEM_MEDIA as MEDIA,
9
+ SB_LIST_ITEM_PROMPTS as PROMPTS,
10
+ } from '../_stories/subcomponents';
11
+ import { disableControls } from '../_stories/helpers';
12
+ import type { ListItemIconButtonProps } from './ListItemIconButton';
13
+
14
+ const hideControls = disableControls([
15
+ 'onClick',
16
+ 'children',
17
+ 'className',
18
+ 'addonStart',
19
+ 'addonEnd',
20
+ ]);
21
+
22
+ /**
23
+ * Use IconButton to provide compact action controls within a ListItem context using iconography.
24
+ *
25
+ * Refer to the [design documentation](https://wise.design/components/list-item) for more details.
26
+ */
27
+ const meta: Meta<ListItemIconButtonProps> = {
28
+ component: ListItem.IconButton,
29
+ title: 'Content/ListItem/ListItem.IconButton',
30
+ parameters: {
31
+ docs: {
32
+ toc: true,
33
+ },
34
+ },
35
+ args: {
36
+ partiallyInteractive: false,
37
+ priority: undefined,
38
+ type: undefined,
39
+ 'aria-label': 'Edit details',
40
+ children: <Edit />,
41
+ href: undefined,
42
+ target: undefined,
43
+ onClick: fn(),
44
+ },
45
+ argTypes: {
46
+ children: {
47
+ description: 'Once of the [Icon components](https://transferwise.github.io/icons/).',
48
+ table: {
49
+ type: { summary: 'ReactNode' },
50
+ },
51
+ control: false,
52
+ },
53
+ priority: {
54
+ control: 'radio',
55
+ options: ['unset (undefined)', 'primary', 'secondary', 'tertiary', 'minimal'],
56
+ mapping: {
57
+ 'unset (undefined)': undefined,
58
+ },
59
+ description: 'IconButton priority/style variant',
60
+ },
61
+ type: {
62
+ control: 'radio',
63
+ options: ['unset (undefined)', 'default', 'negative'],
64
+ mapping: {
65
+ 'unset (undefined)': undefined,
66
+ },
67
+ description:
68
+ 'Sets a visual hierarchy amongst the buttons displayed on the screen. <br /> **NB:** `negative` sentiment only affects the `primary` and `secondary` priorities.',
69
+ },
70
+ partiallyInteractive: {
71
+ control: 'boolean',
72
+ description: 'Whether the icon button allows partial interactivity in the ListItem',
73
+ },
74
+ href: {
75
+ control: 'text',
76
+ description: 'URL for icon button rendered as link',
77
+ },
78
+ target: {
79
+ control: 'select',
80
+ options: [undefined, '_blank', '_self', '_parent', '_top'],
81
+ description:
82
+ 'The `target` attribute for HTML anchor. If set to `_blank`, `rel="noopener noreferrer"` is automatically added to the rendered node.',
83
+ },
84
+ },
85
+ } satisfies Meta<ListItemIconButtonProps>;
86
+
87
+ export default meta;
88
+
89
+ type Story = StoryObj<ListItemIconButtonProps>;
90
+
91
+ export const Playground: Story = {
92
+ tags: ['!autodocs'],
93
+ render: (args: ListItemIconButtonProps) => {
94
+ return (
95
+ <List>
96
+ <ListItem
97
+ title="List item title"
98
+ subtitle="Subtitle goes here"
99
+ media={MEDIA.avatarSingle}
100
+ control={<ListItem.IconButton {...args} />}
101
+ additionalInfo={INFO.nonInteractive}
102
+ />
103
+ </List>
104
+ );
105
+ },
106
+ };
107
+
108
+ /**
109
+ * It's strongly encouraged to provide `aria-label` prop for the `IconButton` control to ensure it
110
+ * follows accessibility best practices.
111
+ * If not set, the component will provide accessible name via the `title`, `subtitle`, `valueTitle`
112
+ * and `valueSubtitle` props and accessible description via `additionalInfo` and `prompt`, which
113
+ * may not be descriptive enough for the screen readers users.
114
+ */
115
+ export const AccessibleName: Story = {
116
+ args: {
117
+ children: <Edit />,
118
+ href: 'https://wise.com',
119
+ target: '_blank',
120
+ onClick: undefined,
121
+ },
122
+ argTypes: hideControls(),
123
+ render: (args: ListItemIconButtonProps) => {
124
+ return (
125
+ <List>
126
+ <ListItem
127
+ control={<ListItem.IconButton {...args} />}
128
+ title="List item with icon button link"
129
+ subtitle="IconButton rendered as anchor element"
130
+ media={MEDIA.avatarSingle}
131
+ />
132
+ </List>
133
+ );
134
+ },
135
+ };
136
+
137
+ /**
138
+ * By default, ListItem is fully interactive, which means its whole surface is clickable
139
+ * and triggers the control. To remain WCAG-compliant there can be no nested interactive
140
+ * elements inside the item.<br />
141
+ *
142
+ * To work around this limitation, this control also allows for a partially interactive mode,
143
+ * which can be enabled via `partiallyInteractive` prop. Once set, only the control will
144
+ * remain interactive, which allows for interactive element to be attached to the
145
+ * `ListItem.AdditionalInfo`. This allows for more complex layouts while still providing a
146
+ * clear, accessible action point for users. <br />
147
+ *
148
+ * If you still require a fully interactive ListItem, you can alternatively use `ListItem.Prompt`
149
+ * which allows for a single instance of a link or an inline button.
150
+ *
151
+ * Refer to the [design documentation](https://wise.design/components/list-item#interaction) for details.
152
+ */
153
+ export const Interactivity: Story = {
154
+ args: {
155
+ children: <Edit />,
156
+ },
157
+ argTypes: hideControls(['partiallyInteractive']),
158
+ render: (args: ListItemIconButtonProps) => {
159
+ return (
160
+ <List>
161
+ <ListItem
162
+ control={<ListItem.IconButton {...args} />}
163
+ title="Fully interactive ListItem"
164
+ subtitle="IconButton in fully interactive context"
165
+ media={MEDIA.avatarSingle}
166
+ additionalInfo={INFO.nonInteractive}
167
+ prompt={PROMPTS.interactive}
168
+ />
169
+
170
+ <ListItem
171
+ control={<ListItem.IconButton {...args} partiallyInteractive />}
172
+ title="Partially interactive ListItem"
173
+ subtitle="IconButton in partially interactive context"
174
+ media={MEDIA.avatarSingle}
175
+ additionalInfo={INFO.interactive}
176
+ prompt={PROMPTS.interactive}
177
+ />
178
+ </List>
179
+ );
180
+ },
181
+ };
182
+
183
+ /**
184
+ * There are two different types of icon button – default and negative – designed to emphasise the nature of the action. <br />
185
+ * **NB:** Sentiment only applies to `primary` and `secondary` priorities. <br />
186
+ * [Design documentation](https://wise.design/components/icon-button#types)
187
+ */
188
+ export const Sentiment: Story = {
189
+ argTypes: hideControls(['sentiment', 'priority']),
190
+ render: (args) => {
191
+ return (
192
+ <List>
193
+ <ListItem
194
+ control={<ListItem.IconButton {...args} priority="primary" type="default" />}
195
+ title="Default Sentiment - Primary"
196
+ subtitle="Default sentiment with primary priority"
197
+ media={MEDIA.avatarSingle}
198
+ />
199
+ <ListItem
200
+ control={<ListItem.IconButton {...args} priority="secondary" type="default" />}
201
+ title="Default Sentiment - Secondary"
202
+ subtitle="Default sentiment with secondary priority"
203
+ media={MEDIA.avatarSingle}
204
+ />
205
+ <ListItem
206
+ control={<ListItem.IconButton {...args} priority="primary" type="negative" />}
207
+ title="Negative Sentiment - Primary"
208
+ subtitle="Negative sentiment with primary priority"
209
+ media={MEDIA.avatarSingle}
210
+ />
211
+ <ListItem
212
+ control={<ListItem.IconButton {...args} priority="secondary" type="negative" />}
213
+ title="Negative Sentiment - Secondary"
214
+ subtitle="Negative sentiment with secondary priority"
215
+ media={MEDIA.avatarSingle}
216
+ />
217
+ </List>
218
+ );
219
+ },
220
+ };
221
+
222
+ /**
223
+ * Priorities set a visual hierarchy amongst the buttons displayed on the
224
+ * screen to help more important buttons to take precedence over others. <br />
225
+ * [Design documentation](https://wise.design/components/icon-button#priorities)
226
+ */
227
+ export const Priority: Story = {
228
+ argTypes: hideControls(['sentiment', 'priority']),
229
+ render: (args) => {
230
+ return (
231
+ <List>
232
+ <ListItem
233
+ title="Primary Priority"
234
+ subtitle="Primary priority (default)"
235
+ media={MEDIA.avatarSingle}
236
+ control={<ListItem.IconButton {...args} priority="primary" />}
237
+ />
238
+ <ListItem
239
+ title="Secondary Priority"
240
+ subtitle="Secondary priority"
241
+ media={MEDIA.avatarSingle}
242
+ control={<ListItem.IconButton {...args} priority="secondary" />}
243
+ />
244
+ <ListItem
245
+ title="Secondary Neutral Priority"
246
+ subtitle="Secondary neutral priority"
247
+ media={MEDIA.avatarSingle}
248
+ control={<ListItem.IconButton {...args} priority="tertiary" />}
249
+ />
250
+ <ListItem
251
+ title="Tertiary Priority"
252
+ subtitle="Tertiary priority"
253
+ media={MEDIA.avatarSingle}
254
+ control={<ListItem.IconButton {...args} priority="minimal" />}
255
+ />
256
+ </List>
257
+ );
258
+ },
259
+ };
260
+
261
+ /**
262
+ * If `href` prop is set, the component will be automatically rendered as an HTML anchor element.
263
+ */
264
+ export const AsAnchor: Story = {
265
+ args: {
266
+ children: <Edit />,
267
+ href: 'https://wise.com',
268
+ target: '_blank',
269
+ onClick: undefined,
270
+ },
271
+ argTypes: hideControls(),
272
+ render: (args: ListItemIconButtonProps) => {
273
+ return (
274
+ <List>
275
+ <ListItem
276
+ control={<ListItem.IconButton {...args} />}
277
+ title="List item with icon button link"
278
+ subtitle="IconButton rendered as anchor element"
279
+ media={MEDIA.avatarSingle}
280
+ />
281
+ </List>
282
+ );
283
+ },
284
+ };
@@ -0,0 +1,73 @@
1
+ import { clsx } from 'clsx';
2
+ import { useContext, type ReactNode } from 'react';
3
+ import IconButtonComp, { type IconButtonProps } from '../../iconButton';
4
+ import { useListItemControl } from '../useListItemControl';
5
+ import { ListItemContext } from '../ListItemContext';
6
+
7
+ export type ListItemIconButtonProps = Pick<
8
+ IconButtonProps,
9
+ 'priority' | 'type' | 'onClick' | 'href' | 'target' | 'aria-label'
10
+ > & {
11
+ children: ReactNode;
12
+ partiallyInteractive?: boolean;
13
+ };
14
+
15
+ /**
16
+ * This component allows for rendering a IconButton control. It's a thin wrapper around the
17
+ * [IconButton component](https://storybook.wise.design/?path=/docs/actions-iconbutton--docs), but offers only
18
+ * a subset of its features in line with the ListItem's constraints. <br />
19
+ * <br />
20
+ * Please refer to the [Design documentation](https://wise.design/components/list-item---icon-button) for details.
21
+ */
22
+ export const IconButton = function ({
23
+ priority = 'minimal',
24
+ 'aria-label': ariaLabel,
25
+ ...props
26
+ }: ListItemIconButtonProps) {
27
+ const { partiallyInteractive, ...restProps } = props;
28
+
29
+ const { ids, props: itemProps } = useContext(ListItemContext);
30
+ const { baseItemProps } = useListItemControl('icon-button', {
31
+ partiallyInteractive,
32
+ ...restProps,
33
+ });
34
+
35
+ const getAriaProps = () => {
36
+ const labelIds = [
37
+ itemProps.inverted ? ids.subtitle : ids.title,
38
+ itemProps.inverted ? ids.title : ids.subtitle,
39
+ itemProps.inverted ? ids.valueSubtitle : ids.valueTitle,
40
+ itemProps.inverted ? ids.valueTitle : ids.valueSubtitle,
41
+ ].join(' ');
42
+ const descriptorIds = [ids.additionalInfo, ids.prompt].join(' ');
43
+
44
+ if (ariaLabel) {
45
+ return {
46
+ 'aria-label': ariaLabel,
47
+ 'aria-describedby': labelIds.concat(descriptorIds),
48
+ };
49
+ }
50
+
51
+ return {
52
+ 'aria-labelledby': labelIds,
53
+ 'aria-describedby': descriptorIds,
54
+ };
55
+ };
56
+
57
+ return (
58
+ <IconButtonComp
59
+ {...restProps}
60
+ {...getAriaProps()}
61
+ className={clsx(
62
+ 'wds-list-item-control',
63
+ !partiallyInteractive && props.href && 'wds-list-item-control_pseudo-element',
64
+ )}
65
+ id={ids.control}
66
+ size={40}
67
+ priority={priority}
68
+ disabled={baseItemProps.disabled}
69
+ />
70
+ );
71
+ };
72
+
73
+ IconButton.displayName = 'ListItem.IconButton';
@@ -0,0 +1,2 @@
1
+ export type { ListItemIconButtonProps } from './ListItemIconButton';
2
+ export { IconButton } from './ListItemIconButton';
@@ -0,0 +1,30 @@
1
+ import { render, screen } from '../../test-utils';
2
+ import { ListItem, type ListItemProps } from '../ListItem';
3
+
4
+ const renderWithMedia = (media: ListItemProps['media']) =>
5
+ render(<ListItem title="Test Title" media={media} />);
6
+
7
+ describe('ListItem.Image', () => {
8
+ it('renders image with presentation role when no alt text', () => {
9
+ renderWithMedia(<ListItem.Image src="test-image.jpg" />);
10
+ expect(screen.getByRole('presentation')).toHaveAttribute('src', 'test-image.jpg');
11
+ });
12
+
13
+ it('renders image with img role when alt text provided', () => {
14
+ renderWithMedia(<ListItem.Image src="test-image.jpg" alt="Test image description" />);
15
+
16
+ const image = screen.getByRole('img');
17
+ expect(image).toHaveAttribute('alt', 'Test image description');
18
+ expect(image).toHaveAttribute('src', 'test-image.jpg');
19
+ });
20
+
21
+ it('renders image with loading prop', () => {
22
+ renderWithMedia(<ListItem.Image src="test-image.jpg" loading="lazy" />);
23
+ expect(screen.getByRole('presentation')).toHaveAttribute('loading', 'lazy');
24
+ });
25
+
26
+ it('renders image with className prop', () => {
27
+ renderWithMedia(<ListItem.Image src="test-image.jpg" className="custom-image-class" />);
28
+ expect(screen.getByRole('presentation')).toHaveClass('custom-image-class');
29
+ });
30
+ });