@transferwise/components 0.0.0-experimental-c81666a → 0.0.0-experimental-bb07bc5

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 (79) hide show
  1. package/build/i18n/ja.json +1 -1
  2. package/build/i18n/ja.json.js +1 -1
  3. package/build/i18n/ja.json.mjs +1 -1
  4. package/build/image/Image.js +10 -9
  5. package/build/image/Image.js.map +1 -1
  6. package/build/image/Image.mjs +11 -11
  7. package/build/image/Image.mjs.map +1 -1
  8. package/build/index.js +1 -0
  9. package/build/index.js.map +1 -1
  10. package/build/index.mjs +1 -1
  11. package/build/main.css +212 -0
  12. package/build/styles/listItem/ListItem.css +212 -0
  13. package/build/styles/main.css +212 -0
  14. package/build/test-utils/assets/apple-pay-logo.svg +84 -0
  15. package/build/types/image/Image.d.ts +1 -0
  16. package/build/types/image/Image.d.ts.map +1 -1
  17. package/build/types/index.d.ts +2 -0
  18. package/build/types/index.d.ts.map +1 -1
  19. package/build/types/listItem/ListItem.d.ts +50 -0
  20. package/build/types/listItem/ListItem.d.ts.map +1 -0
  21. package/build/types/listItem/ListItemAdditionalInfo.d.ts +9 -0
  22. package/build/types/listItem/ListItemAdditionalInfo.d.ts.map +1 -0
  23. package/build/types/listItem/ListItemButton.d.ts +6 -0
  24. package/build/types/listItem/ListItemButton.d.ts.map +1 -0
  25. package/build/types/listItem/ListItemCheckbox.d.ts +4 -0
  26. package/build/types/listItem/ListItemCheckbox.d.ts.map +1 -0
  27. package/build/types/listItem/ListItemIconButton.d.ts +8 -0
  28. package/build/types/listItem/ListItemIconButton.d.ts.map +1 -0
  29. package/build/types/listItem/ListItemMedia.d.ts +19 -0
  30. package/build/types/listItem/ListItemMedia.d.ts.map +1 -0
  31. package/build/types/listItem/ListItemNavigation.d.ts +4 -0
  32. package/build/types/listItem/ListItemNavigation.d.ts.map +1 -0
  33. package/build/types/listItem/ListItemSwitch.d.ts +3 -0
  34. package/build/types/listItem/ListItemSwitch.d.ts.map +1 -0
  35. package/build/types/listItem/index.d.ts +6 -0
  36. package/build/types/listItem/index.d.ts.map +1 -0
  37. package/build/types/listItem/prompt/Prompt.d.ts +12 -0
  38. package/build/types/listItem/prompt/Prompt.d.ts.map +1 -0
  39. package/build/types/listItem/useItemControl.d.ts +5 -0
  40. package/build/types/listItem/useItemControl.d.ts.map +1 -0
  41. package/build/types/test-utils/fake-data.d.ts +2 -0
  42. package/build/types/test-utils/fake-data.d.ts.map +1 -1
  43. package/build/types/uploadInput/UploadInput.d.ts.map +1 -1
  44. package/build/uploadInput/UploadInput.js +0 -1
  45. package/build/uploadInput/UploadInput.js.map +1 -1
  46. package/build/uploadInput/UploadInput.mjs +0 -1
  47. package/build/uploadInput/UploadInput.mjs.map +1 -1
  48. package/package.json +3 -3
  49. package/src/i18n/ja.json +1 -1
  50. package/src/image/Image.spec.tsx +3 -3
  51. package/src/image/Image.tsx +12 -10
  52. package/src/index.ts +2 -0
  53. package/src/legacylistItem/LegacyListItem.story.tsx +5 -5
  54. package/src/legacylistItem/LegacyListItem.tests.story.tsx +6 -6
  55. package/src/listItem/ListItem.css +212 -0
  56. package/src/listItem/ListItem.less +210 -0
  57. package/src/listItem/ListItem.spec.tsx +35 -0
  58. package/src/listItem/ListItem.story.tsx +461 -0
  59. package/src/listItem/ListItem.tsx +282 -0
  60. package/src/listItem/ListItemAdditionalInfo.tsx +31 -0
  61. package/src/listItem/ListItemButton.spec.tsx +92 -0
  62. package/src/listItem/ListItemButton.tsx +24 -0
  63. package/src/listItem/ListItemCheckbox.tsx +14 -0
  64. package/src/listItem/ListItemIconButton.tsx +16 -0
  65. package/src/listItem/ListItemMedia.tsx +52 -0
  66. package/src/listItem/ListItemNavigation.tsx +23 -0
  67. package/src/listItem/ListItemSwitch.tsx +8 -0
  68. package/src/listItem/index.ts +10 -0
  69. package/src/listItem/prompt/Prompt.spec.tsx +77 -0
  70. package/src/listItem/prompt/Prompt.story.tsx +170 -0
  71. package/src/listItem/prompt/Prompt.tsx +44 -0
  72. package/src/listItem/useItemControl.tsx +23 -0
  73. package/src/main.css +212 -0
  74. package/src/main.less +1 -0
  75. package/src/promoCard/__snapshots__/PromoCard.spec.tsx.snap +0 -1
  76. package/src/promoCard/__snapshots__/PromoCardGroup.spec.tsx.snap +0 -2
  77. package/src/test-utils/assets/apple-pay-logo.svg +84 -0
  78. package/src/test-utils/fake-data.ts +5 -0
  79. package/src/uploadInput/UploadInput.tsx +4 -13
@@ -0,0 +1,282 @@
1
+ import {
2
+ createContext,
3
+ ElementType,
4
+ PropsWithChildren,
5
+ ReactNode,
6
+ useId,
7
+ useMemo,
8
+ useState,
9
+ } from 'react';
10
+ import { Typography } from '../common';
11
+ import Body from '../body';
12
+ import { AdditionalInfo } from './ListItemAdditionalInfo';
13
+ import { IconButton, ListItemIconButtonProps } from './ListItemIconButton';
14
+ import { Checkbox, ListItemCheckboxProps } from './ListItemCheckbox';
15
+ import { ListItemNavigationProps, Navigation } from './ListItemNavigation';
16
+ import { clsx } from 'clsx';
17
+ import { Button, ListItemButtonProps } from './ListItemButton';
18
+ import { Switch } from './ListItemSwitch';
19
+ import { AvatarLayout, AvatarView, Image } from './ListItemMedia';
20
+ import Prompt from './prompt/Prompt';
21
+ import { PrimitiveAnchor, PrimitiveButton } from '../primitives';
22
+ import { ButtonProps } from '../button/Button.types';
23
+
24
+ export type ListItemTypes =
25
+ | 'non-interactive'
26
+ | 'navigation'
27
+ | 'radio'
28
+ | 'checkbox'
29
+ | 'switch'
30
+ | 'button'
31
+ | 'icon-button';
32
+
33
+ export type ListItemControlProps =
34
+ | ListItemNavigationProps
35
+ // TODO: add more types for control props here;
36
+ | ListItemCheckboxProps
37
+ | ListItemButtonProps
38
+ | ListItemIconButtonProps;
39
+
40
+ export type Props = {
41
+ as?: 'li' | 'div' | 'span';
42
+ inverted?: boolean;
43
+ disabled?: boolean;
44
+ spotlight?: 'active' | 'inactive';
45
+ title: ReactNode;
46
+ subtitle?: ReactNode;
47
+ additionalInfo?: ReactNode;
48
+ valueTitle?: ReactNode;
49
+ valueSubtitle?: ReactNode;
50
+ media?: ReactNode;
51
+ control?: ReactNode;
52
+ prompt?: ReactNode;
53
+ className?: string;
54
+ };
55
+
56
+ export type ListItemContextData = {
57
+ setControlType: (type: ListItemTypes) => void;
58
+ setControlProps: (props: ListItemControlProps) => void;
59
+ ids: {
60
+ label: string;
61
+ additionalInfo: string;
62
+ value: string;
63
+ control: string;
64
+ prompt: string;
65
+ };
66
+ props: Pick<Props, 'disabled' | 'inverted'>;
67
+ };
68
+
69
+ // @ts-expect-error for now let's mock it with `null` value
70
+ // but actually by default we should specify `setControlType('none')`
71
+ export const ListItemContext = createContext<ListItemContextData>(null);
72
+
73
+ export const ListItem = ({
74
+ as: ListItemElement = 'li',
75
+ title,
76
+ subtitle,
77
+ additionalInfo,
78
+ prompt,
79
+ inverted,
80
+ media,
81
+ spotlight = undefined,
82
+ valueTitle,
83
+ valueSubtitle,
84
+ control = null,
85
+ disabled,
86
+ className,
87
+ }: Props) => {
88
+ /*
89
+ const returnType = (): ReactNode => {
90
+ switch (type) {
91
+ case 'Navigation':
92
+ return <Chevron orientation={Position.RIGHT} disabled />;
93
+ case 'Radio':
94
+ return <RadioButton name="Hello" checked />;
95
+ case 'Checkbox':
96
+ return <CheckboxButton name="Hello" checked />;
97
+ case 'Switch':
98
+ return <Switch onClick={() => console.log('clicked')} />;
99
+ case 'Button':
100
+ return <Button v2>Hello</Button>;
101
+ case 'IconButton':
102
+ return (
103
+ <IconButton size={40} priority="minimal">
104
+ <InfoCircle />
105
+ </IconButton>
106
+ );
107
+ }
108
+ };
109
+ */
110
+ const idPrefix = useId();
111
+ const [controlProps, setControlProps] = useState<ListItemControlProps>({});
112
+ const [controlType, setControlType] = useState<ListItemTypes>('non-interactive');
113
+ const ids = {
114
+ label: `${idPrefix}_label`,
115
+ value: `${idPrefix}_value`,
116
+ control: `${idPrefix}_control`,
117
+ prompt: `${idPrefix}_prompt`,
118
+ additionalInfo: `${idPrefix}_additional-info`,
119
+ };
120
+
121
+ // TODO: add partially interactive check from Button and IconButtons
122
+ // (https://transferwise.atlassian.net/browse/DS-7562)
123
+ const isFullyInteractive = controlType !== 'non-interactive';
124
+
125
+ const listItemContext = useMemo(
126
+ () => ({ setControlType, setControlProps, ids, props: { disabled, inverted } }),
127
+ [],
128
+ );
129
+ return (
130
+ <ListItemContext.Provider value={listItemContext}>
131
+ <ListItemElement
132
+ className={clsx(
133
+ 'wds-list-item',
134
+ {
135
+ 'wds-list-item-interactive':
136
+ controlType !== 'non-interactive' &&
137
+ !(controlProps as ListItemButtonProps | ListItemIconButtonProps)
138
+ ?.partiallyInteractive,
139
+ },
140
+ `wds-list-item-${controlType}`,
141
+ {
142
+ [`wds-list-item-spotlight-${spotlight}`]: isFullyInteractive && !!spotlight,
143
+ },
144
+ { disabled },
145
+ className,
146
+ )}
147
+ aria-describedby={[ids.additionalInfo].join(' ')}
148
+ >
149
+ <View {...{ prompt, controlType, controlProps }}>
150
+ {media && <div className="wds-list-item-media">{media}</div>}
151
+
152
+ {/* Title + Subtitle + Values + Additional Info - Group */}
153
+ <div className="wds-list-item-body">
154
+ {/* Title + Subtitle + Values - Group */}
155
+ <div className="d-flex justify-content-between">
156
+ <span>
157
+ {/* @ts-expect-error div can have role and aria-lavel props */}
158
+ <Body
159
+ type={Typography.BODY_LARGE_BOLD}
160
+ className="wds-list-item-title"
161
+ // for a11y this needs to be a header but for SEO it shouldn't be `h*` tag
162
+ // so we enable header semantics via `role` and `aria-level` attrs
163
+ role="heading"
164
+ aria-level="4"
165
+ >
166
+ {title}
167
+ </Body>
168
+ <Body className="wds-list-item-subtitle">{subtitle}</Body>
169
+ </span>
170
+ {(valueTitle || valueSubtitle) && (
171
+ <span
172
+ id={ids.value}
173
+ className={clsx('wds-list-item-value', 'd-flex align-items-center', {
174
+ 'flex-column': valueTitle !== undefined && valueSubtitle !== undefined,
175
+ })}
176
+ >
177
+ {valueTitle && (
178
+ <Body type={Typography.BODY_LARGE_BOLD} className="wds-list-item-title-value">
179
+ {valueTitle}
180
+ </Body>
181
+ )}
182
+ {valueSubtitle && (
183
+ <Body className="wds-list-item-subtitle-value">{valueSubtitle}</Body>
184
+ )}
185
+ </span>
186
+ )}
187
+ </div>
188
+
189
+ {/* Additional Info and Prompt here */}
190
+ {Boolean(subtitle) && additionalInfo}
191
+ </div>
192
+ {control === null ? null : <Body className="wds-list-item-control">{control}</Body>}
193
+ </View>
194
+ </ListItemElement>
195
+ </ListItemContext.Provider>
196
+ );
197
+ };
198
+
199
+ type ViewProps = PropsWithChildren<{
200
+ prompt?: ReactNode;
201
+ controlType?: ListItemTypes;
202
+ controlProps?: ListItemControlProps;
203
+ }>;
204
+
205
+ function View({ children, prompt, controlType = 'non-interactive', controlProps }: ViewProps) {
206
+ const isLinkControl = ['navigation' /* TODO: add other link controls */].includes(controlType);
207
+
208
+ const isPartiallyInteractive =
209
+ (controlType === 'button' || controlType === 'icon-button') &&
210
+ !!(
211
+ controlProps &&
212
+ (controlProps as ListItemButtonProps | ListItemIconButtonProps).partiallyInteractive
213
+ );
214
+ const isHrefProvided = isLinkControl
215
+ ? (controlProps as ListItemNavigationProps)?.href !== '#'
216
+ : false;
217
+
218
+ if (isLinkControl && isHrefProvided) {
219
+ return (
220
+ // for link instances of .Navigation, .IconButton, .Button
221
+ <>
222
+ <PrimitiveAnchor
223
+ href={(controlProps as ListItemNavigationProps)?.href}
224
+ target={(controlProps as ListItemNavigationProps)?.target}
225
+ className={clsx(
226
+ 'd-flex flex-row',
227
+ // { 'align-items-center': !subtitle }
228
+ )}
229
+ >
230
+ {children}
231
+ </PrimitiveAnchor>
232
+ {prompt}
233
+ </>
234
+ );
235
+ }
236
+
237
+ if (isPartiallyInteractive) {
238
+ return (
239
+ <>
240
+ <div
241
+ className={clsx(
242
+ 'd-flex flex-row partially-interactive',
243
+ // { 'align-items-center': !subtitle }
244
+ )}
245
+ >
246
+ {children}
247
+ </div>
248
+ {prompt}
249
+ </>
250
+ );
251
+ }
252
+
253
+ return (
254
+ // for form control instances of .Radio, .Checkbox, .Switch, .Button, .Navigation etc
255
+ <fieldset>
256
+ <label
257
+ className={clsx(
258
+ 'd-flex flex-row',
259
+ // { 'align-items-center': !subtitle }
260
+ )}
261
+ >
262
+ {children}
263
+ </label>
264
+ {prompt}
265
+ </fieldset>
266
+ );
267
+ }
268
+
269
+ /* eslint-disable functional/immutable-data */
270
+ ListItem.Image = Image;
271
+ ListItem.AvatarView = AvatarView;
272
+ ListItem.AvatarLayout = AvatarLayout;
273
+ ListItem.AdditionalInfo = AdditionalInfo;
274
+ ListItem.Checkbox = Checkbox;
275
+ ListItem.IconButton = IconButton;
276
+ ListItem.Navigation = Navigation;
277
+ ListItem.Button = Button;
278
+ ListItem.Switch = Switch;
279
+ ListItem.Prompt = Prompt;
280
+ /* eslint-enable functional/immutable-data */
281
+
282
+ export default ListItem;
@@ -0,0 +1,31 @@
1
+ import { PropsWithChildren, useContext } from 'react';
2
+ import { ListItemContext, ListItemContextData } from './ListItem';
3
+ import Body from '../body';
4
+ import Link, { LinkProps } from '../link';
5
+ import { Typography } from '../common';
6
+
7
+ export type ListItemAdditionalInfoProps = PropsWithChildren<{
8
+ action?: Pick<LinkProps, 'href' | 'onClick' | 'target'> & { label?: string };
9
+ }>;
10
+
11
+ export const AdditionalInfo = function ({ children, action }: ListItemAdditionalInfoProps) {
12
+ const { ids } = useContext<ListItemContextData>(ListItemContext);
13
+
14
+ return (
15
+ <Body
16
+ type={Typography.BODY_DEFAULT}
17
+ id={ids.additionalInfo}
18
+ className="wds-list-item-additional-info"
19
+ >
20
+ {children}
21
+ {action ? (
22
+ <>
23
+ {' '}
24
+ <Link href={action.href} target={action.target} onClick={action.onClick}>
25
+ {action.label}
26
+ </Link>
27
+ </>
28
+ ) : null}
29
+ </Body>
30
+ );
31
+ };
@@ -0,0 +1,92 @@
1
+ import { render, screen, mockMatchMedia } from '../test-utils';
2
+ import { Button as ItemButton } from './ListItemButton';
3
+ import { ButtonPriority } from '../button/Button.types';
4
+ import { ListItemContext } from './ListItem';
5
+
6
+ mockMatchMedia();
7
+
8
+ describe('ItemButton', () => {
9
+ const mockSetControlType = jest.fn();
10
+ const mockSetControlProps = jest.fn();
11
+
12
+ const renderWithItemContext = (ui: React.ReactNode) => {
13
+ return render(
14
+ <ListItemContext.Provider
15
+ value={{
16
+ setControlType: mockSetControlType,
17
+ setControlProps: mockSetControlProps,
18
+ ids: {
19
+ label: 'label',
20
+ additionalInfo: 'additional Info',
21
+ value: 'value',
22
+ control: 'control',
23
+ prompt: 'prompt',
24
+ },
25
+ props: {},
26
+ }}
27
+ >
28
+ {ui}
29
+ </ListItemContext.Provider>,
30
+ );
31
+ };
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ });
36
+
37
+ it('always sets v2 to true', () => {
38
+ renderWithItemContext(<ItemButton priority="primary">Test Button</ItemButton>);
39
+ const button = screen.getByRole('button');
40
+ expect(button).toBeInTheDocument();
41
+ expect(mockSetControlType).toHaveBeenCalledWith('button');
42
+ });
43
+
44
+ it('always sets size to "md"', () => {
45
+ renderWithItemContext(<ItemButton priority="primary">Test Button</ItemButton>);
46
+ const button = screen.getByRole('button');
47
+ expect(button).toHaveClass('wds-Button--medium');
48
+ });
49
+
50
+ it('supports all priorities', () => {
51
+ const priorities: ButtonPriority[] = ['primary', 'secondary', 'tertiary'];
52
+ priorities.forEach((priority) => {
53
+ renderWithItemContext(<ItemButton priority={priority}>Test {priority}</ItemButton>);
54
+ const button = screen.getByRole('button', { name: `Test ${priority}` });
55
+ expect(button).toBeInTheDocument();
56
+ });
57
+ });
58
+
59
+ it('renders as a button by default', () => {
60
+ renderWithItemContext(<ItemButton>Click me</ItemButton>);
61
+ const button = screen.getByRole('button');
62
+ expect(button).toBeInTheDocument();
63
+ expect(button.tagName).toBe('BUTTON');
64
+ });
65
+
66
+ it('renders as an anchor when href is provided', () => {
67
+ renderWithItemContext(<ItemButton href="https://example.com">Go to Example</ItemButton>);
68
+ const link = screen.getByRole('link', { name: 'Go to Example' });
69
+ expect(link).toBeInTheDocument();
70
+ expect(link.tagName).toBe('A');
71
+ expect(link).toHaveAttribute('href', 'https://example.com');
72
+ });
73
+
74
+ it('spreads additional props to the button', () => {
75
+ renderWithItemContext(<ItemButton aria-label="Custom Button">Custom</ItemButton>);
76
+ const button = screen.getByRole('button', { name: 'Custom Button' });
77
+ expect(button).toBeInTheDocument();
78
+ expect(button).toHaveAttribute('aria-label', 'Custom Button');
79
+ });
80
+
81
+ it('spreads additional props to the anchor', () => {
82
+ renderWithItemContext(
83
+ <ItemButton href="https://example.com" target="_blank" aria-label="Custom Link">
84
+ Custom Link
85
+ </ItemButton>,
86
+ );
87
+ const link = screen.getByRole('link', { name: 'Custom Link' });
88
+ expect(link).toBeInTheDocument();
89
+ expect(link).toHaveAttribute('href', 'https://example.com');
90
+ expect(link).toHaveAttribute('target', '_blank');
91
+ });
92
+ });
@@ -0,0 +1,24 @@
1
+ import { default as ButtonComp, NewButtonProps } from '../button';
2
+ import { useItemControl } from './useItemControl';
3
+
4
+ export type ListItemButtonProps = Omit<NewButtonProps, 'v2' | 'size' | 'disabled'> & {
5
+ partiallyInteractive?: boolean;
6
+ };
7
+
8
+ export const Button = ({
9
+ priority = 'secondary',
10
+ partiallyInteractive,
11
+ ...props
12
+ }: ListItemButtonProps) => {
13
+ const { baseItemProps } = useItemControl('button', { partiallyInteractive, ...props });
14
+
15
+ const commonProps = {
16
+ ...props,
17
+ priority,
18
+ v2: true,
19
+ size: 'md',
20
+ disabled: baseItemProps.disabled,
21
+ };
22
+
23
+ return <ButtonComp {...(commonProps as NewButtonProps)} />;
24
+ };
@@ -0,0 +1,14 @@
1
+ import CheckboxButton from '../checkboxButton';
2
+ import { CheckboxButtonProps } from '../checkboxButton/CheckboxButton';
3
+ import { useItemControl } from './useItemControl';
4
+
5
+ export type ListItemCheckboxProps = Pick<
6
+ CheckboxButtonProps,
7
+ 'checked' | 'indeterminate' | 'onChange'
8
+ >;
9
+
10
+ export const Checkbox = function (props: ListItemCheckboxProps) {
11
+ const { baseItemProps } = useItemControl('checkbox');
12
+
13
+ return <CheckboxButton disabled={baseItemProps.disabled} {...props} />;
14
+ };
@@ -0,0 +1,16 @@
1
+ import { default as IconButtonComp, IconButtonProps } from '../iconButton';
2
+ import { useItemControl } from './useItemControl';
3
+ import { ReactNode } from 'react';
4
+
5
+ export type ListItemIconButtonProps = Pick<IconButtonProps, 'onClick' | 'href' | 'target'> & {
6
+ children: ReactNode;
7
+ partiallyInteractive?: boolean;
8
+ };
9
+
10
+ export const IconButton = function (props: ListItemIconButtonProps) {
11
+ const { partiallyInteractive, ...restProps } = props;
12
+
13
+ const { baseItemProps } = useItemControl('icon-button', { partiallyInteractive, ...restProps });
14
+
15
+ return <IconButtonComp {...restProps} size={32} disabled={baseItemProps.disabled} />;
16
+ };
@@ -0,0 +1,52 @@
1
+ import { default as AvatarViewComp, AvatarViewProps } from '../avatarView';
2
+ import { default as AvatarLayoutComp, AvatarLayoutProps } from '../avatarLayout';
3
+ import { default as ImageComp, ImageProps } from '../image/Image';
4
+ import { clsx } from 'clsx';
5
+
6
+ type SizeProp = { size?: 32 | 40 | 48 | 56 | 72 };
7
+
8
+ export type ListItemAvatarViewProps = Omit<AvatarViewProps, 'size' | 'interactive'> & SizeProp;
9
+
10
+ export const AvatarView = ({ className, size = 48, ...props }: ListItemAvatarViewProps) => {
11
+ return (
12
+ <AvatarViewComp
13
+ {...props}
14
+ size={size}
15
+ className={clsx('wds-list-item-media-avatar-view', className)}
16
+ />
17
+ );
18
+ };
19
+
20
+ export type ListItemAvatarLayoutProps = Omit<AvatarLayoutProps, 'size' | 'interactive'> & SizeProp;
21
+
22
+ export const AvatarLayout = ({ className, size = 48, ...props }: ListItemAvatarLayoutProps) => {
23
+ return (
24
+ <AvatarLayoutComp
25
+ {...props}
26
+ size={size}
27
+ className={clsx('wds-list-item-media-avatar-layout', className)}
28
+ />
29
+ );
30
+ };
31
+
32
+ export type ListItemImageProps = Omit<ImageProps, 'stretch' | 'shrink' | 'id' | 'alt'> &
33
+ SizeProp & {
34
+ alt?: string;
35
+ };
36
+
37
+ /**
38
+ * TODO: mention that image is for rare cases not for DS illustrations, they discouraged
39
+ */
40
+ export const Image = ({ alt = '', size = 48, ...props }: ListItemImageProps) => {
41
+ return (
42
+ <div
43
+ className={clsx('wds-list-item-media-image')}
44
+ style={{
45
+ // @ts-expect-error CSS custom props allowed
46
+ '--item-media-image-size': `${size}px`,
47
+ }}
48
+ >
49
+ <ImageComp {...props} alt={alt} />
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,23 @@
1
+ import { ChevronRight, BackslashCircle } from '@transferwise/icons';
2
+ import { ButtonProps } from '../button/Button.types';
3
+ import { useItemControl } from './useItemControl';
4
+ import { PrimitiveButton } from '../primitives';
5
+
6
+ export type ListItemNavigationProps = Pick<ButtonProps, 'onClick' | 'href' | 'target'>;
7
+
8
+ export const Navigation = function Navigation({ href = '#', ...props }: ListItemNavigationProps) {
9
+ const { baseItemProps } = useItemControl('navigation', { href, ...props });
10
+ const isLink = href !== '#';
11
+ const icon = baseItemProps.disabled ? <BackslashCircle size={24} /> : <ChevronRight size={24} />;
12
+ return isLink ? (
13
+ <>{icon}</>
14
+ ) : (
15
+ <PrimitiveButton
16
+ className="btn-unstyled"
17
+ // @ts-expect-error TODO: fix the type error later
18
+ onClick={props.onClick}
19
+ >
20
+ {icon}
21
+ </PrimitiveButton>
22
+ );
23
+ };
@@ -0,0 +1,8 @@
1
+ import { default as SwitchComp, SwitchProps } from '../switch';
2
+ import { useItemControl } from './useItemControl';
3
+
4
+ export const Switch = function (props: SwitchProps) {
5
+ const { baseItemProps } = useItemControl('switch');
6
+
7
+ return <SwitchComp disabled={baseItemProps.disabled} {...props} />;
8
+ };
@@ -0,0 +1,10 @@
1
+ export type { Props as ListItemProps } from './ListItem';
2
+ export type { ListItemAdditionalInfoProps } from './ListItemAdditionalInfo';
3
+ export type { ListItemCheckboxProps } from './ListItemCheckbox';
4
+ export type {
5
+ ListItemImageProps,
6
+ ListItemAvatarViewProps,
7
+ ListItemAvatarLayoutProps,
8
+ } from './ListItemMedia';
9
+
10
+ export { default } from './ListItem';
@@ -0,0 +1,77 @@
1
+ import { render, screen, mockMatchMedia } from '../../test-utils';
2
+ import { Prompt } from './Prompt';
3
+ import { Sentiment } from '../../common';
4
+
5
+ mockMatchMedia();
6
+
7
+ describe('ListItem.Prompt', () => {
8
+ it('renders the aria-label when provided', () => {
9
+ const ariaLabel = 'Test aria-label';
10
+ render(
11
+ <Prompt
12
+ type={Sentiment.POSITIVE}
13
+ action={{ 'aria-label': ariaLabel, href: 'https://example.com' }}
14
+ >
15
+ Positive prompt
16
+ </Prompt>,
17
+ );
18
+
19
+ expect(screen.getByLabelText(ariaLabel)).toBeInTheDocument();
20
+ });
21
+
22
+ it('applies the interactive class when href is provided', () => {
23
+ render(
24
+ <Prompt
25
+ type={Sentiment.POSITIVE}
26
+ action={{ href: 'https://example.com', 'aria-label': 'Interactive link' }}
27
+ >
28
+ Interactive link
29
+ </Prompt>,
30
+ );
31
+
32
+ expect(screen.getByRole('link')).toHaveClass('np-prompt-interactive');
33
+ });
34
+
35
+ it('applies the interactive class when onClick is provided', () => {
36
+ render(
37
+ <Prompt
38
+ type={Sentiment.POSITIVE}
39
+ action={{ onClick: jest.fn(), 'aria-label': 'Interactive button' }}
40
+ >
41
+ Interactive button
42
+ </Prompt>,
43
+ );
44
+
45
+ expect(screen.getByRole('button')).toHaveClass('np-prompt-interactive');
46
+ });
47
+
48
+ it('does not apply the interactive class when no action is provided', () => {
49
+ render(<Prompt type={Sentiment.POSITIVE}>Non-interactive prompt</Prompt>);
50
+
51
+ expect(screen.getByText('Non-interactive prompt')).not.toHaveClass('np-prompt-interactive');
52
+ });
53
+
54
+ it('renders the children content', () => {
55
+ render(<Prompt type={Sentiment.POSITIVE}>This is a child prompt</Prompt>);
56
+
57
+ expect(screen.getByText('This is a child prompt')).toBeInTheDocument();
58
+ });
59
+
60
+ it('spreads additional props to the wrapper element', () => {
61
+ render(
62
+ <Prompt
63
+ type={Sentiment.POSITIVE}
64
+ action={{
65
+ href: 'https://example.com',
66
+ target: '_blank',
67
+ 'aria-label': 'Custom props prompt',
68
+ }}
69
+ >
70
+ Custom props prompt
71
+ </Prompt>,
72
+ );
73
+
74
+ const link = screen.getByRole('link');
75
+ expect(link).toHaveAttribute('target', '_blank');
76
+ });
77
+ });