@transferwise/components 0.0.0-experimental-6f38fd8 → 0.0.0-experimental-37df6fe

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 (110) hide show
  1. package/build/flowNavigation/animatedLabel/AnimatedLabel.js +1 -1
  2. package/build/flowNavigation/animatedLabel/AnimatedLabel.js.map +1 -1
  3. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs +1 -1
  4. package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs.map +1 -1
  5. package/build/index.js +4 -3
  6. package/build/index.js.map +1 -1
  7. package/build/index.mjs +3 -3
  8. package/build/listItem/List.js.map +1 -0
  9. package/build/listItem/List.mjs.map +1 -0
  10. package/build/{legacylistItem/LegacyListItem.js → listItem/ListItem.js} +3 -3
  11. package/build/listItem/ListItem.js.map +1 -0
  12. package/build/{legacylistItem/LegacyListItem.mjs → listItem/ListItem.mjs} +3 -3
  13. package/build/listItem/ListItem.mjs.map +1 -0
  14. package/build/main.css +185 -3
  15. package/build/phoneNumberInput/PhoneNumberInput.js +1 -1
  16. package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
  17. package/build/phoneNumberInput/PhoneNumberInput.mjs +1 -1
  18. package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
  19. package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js +2 -2
  20. package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js.map +1 -1
  21. package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.mjs +2 -2
  22. package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.mjs.map +1 -1
  23. package/build/styles/item/Item.css +185 -0
  24. package/build/styles/main.css +185 -3
  25. package/build/styles/switch/Switch.css +0 -3
  26. package/build/test-utils/assets/apple-pay-logo.svg +84 -0
  27. package/build/types/index.d.ts +4 -2
  28. package/build/types/index.d.ts.map +1 -1
  29. package/build/types/item/Item.d.ts +43 -0
  30. package/build/types/item/Item.d.ts.map +1 -0
  31. package/build/types/item/ItemAdditionalInfo.d.ts +9 -0
  32. package/build/types/item/ItemAdditionalInfo.d.ts.map +1 -0
  33. package/build/types/item/ItemButton.d.ts +4 -0
  34. package/build/types/item/ItemButton.d.ts.map +1 -0
  35. package/build/types/item/ItemCheckbox.d.ts +4 -0
  36. package/build/types/item/ItemCheckbox.d.ts.map +1 -0
  37. package/build/types/item/ItemIconButton.d.ts +7 -0
  38. package/build/types/item/ItemIconButton.d.ts.map +1 -0
  39. package/build/types/item/ItemMedia.d.ts +19 -0
  40. package/build/types/item/ItemMedia.d.ts.map +1 -0
  41. package/build/types/item/ItemNavigation.d.ts +4 -0
  42. package/build/types/item/ItemNavigation.d.ts.map +1 -0
  43. package/build/types/item/ItemSwitch.d.ts +3 -0
  44. package/build/types/item/ItemSwitch.d.ts.map +1 -0
  45. package/build/types/item/index.d.ts +6 -0
  46. package/build/types/item/index.d.ts.map +1 -0
  47. package/build/types/item/prompt/Prompt.d.ts +12 -0
  48. package/build/types/item/prompt/Prompt.d.ts.map +1 -0
  49. package/build/types/item/useItemControl.d.ts +5 -0
  50. package/build/types/item/useItemControl.d.ts.map +1 -0
  51. package/build/types/listItem/List.d.ts.map +1 -0
  52. package/build/types/{legacylistItem/LegacyListItem.d.ts → listItem/ListItem.d.ts} +4 -4
  53. package/build/types/listItem/ListItem.d.ts.map +1 -0
  54. package/build/types/listItem/index.d.ts +5 -0
  55. package/build/types/listItem/index.d.ts.map +1 -0
  56. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts +2 -2
  57. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts.map +1 -1
  58. package/build/types/test-utils/fake-data.d.ts +2 -0
  59. package/build/types/test-utils/fake-data.d.ts.map +1 -1
  60. package/package.json +3 -3
  61. package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +1 -1
  62. package/src/index.ts +4 -2
  63. package/src/item/Item.css +185 -0
  64. package/src/item/Item.less +178 -0
  65. package/src/item/Item.story.tsx +188 -0
  66. package/src/item/Item.tsx +174 -0
  67. package/src/item/ItemAdditionalInfo.tsx +31 -0
  68. package/src/item/ItemButton.spec.tsx +90 -0
  69. package/src/item/ItemButton.tsx +18 -0
  70. package/src/item/ItemCheckbox.tsx +11 -0
  71. package/src/item/ItemIconButton.tsx +13 -0
  72. package/src/item/ItemMedia.tsx +52 -0
  73. package/src/item/ItemNavigation.tsx +11 -0
  74. package/src/item/ItemSwitch.tsx +8 -0
  75. package/src/item/index.ts +6 -0
  76. package/src/item/prompt/Prompt.spec.tsx +77 -0
  77. package/src/item/prompt/Prompt.story.tsx +170 -0
  78. package/src/item/prompt/Prompt.tsx +44 -0
  79. package/src/item/useItemControl.tsx +12 -0
  80. package/src/{legacylistItem/LegacyListItem.tests.story.tsx → listItem/ListItem.tests.story.tsx} +2 -2
  81. package/src/{legacylistItem/LegacyListItem.tsx → listItem/ListItem.tsx} +3 -3
  82. package/src/listItem/index.ts +4 -0
  83. package/src/main.css +185 -3
  84. package/src/main.less +2 -1
  85. package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +4 -4
  86. package/src/phoneNumberInput/PhoneNumberInput.story.tsx +0 -22
  87. package/src/phoneNumberInput/PhoneNumberInput.tsx +1 -1
  88. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.ts +3 -4
  89. package/src/switch/Switch.css +0 -3
  90. package/src/switch/Switch.less +0 -1
  91. package/src/test-utils/assets/apple-pay-logo.svg +84 -0
  92. package/src/test-utils/fake-data.ts +5 -0
  93. package/build/legacylistItem/LegacyListItem.js.map +0 -1
  94. package/build/legacylistItem/LegacyListItem.mjs.map +0 -1
  95. package/build/legacylistItem/List.js.map +0 -1
  96. package/build/legacylistItem/List.mjs.map +0 -1
  97. package/build/types/legacylistItem/LegacyListItem.d.ts.map +0 -1
  98. package/build/types/legacylistItem/List.d.ts.map +0 -1
  99. package/build/types/legacylistItem/index.d.ts +0 -5
  100. package/build/types/legacylistItem/index.d.ts.map +0 -1
  101. package/src/legacylistItem/index.ts +0 -4
  102. /package/build/{legacylistItem → listItem}/List.js +0 -0
  103. /package/build/{legacylistItem → listItem}/List.mjs +0 -0
  104. /package/build/styles/{legacylistItem/LegacyListItem.css → listItem/ListItem.css} +0 -0
  105. /package/build/types/{legacylistItem → listItem}/List.d.ts +0 -0
  106. /package/src/{legacylistItem → listItem}/List.tsx +0 -0
  107. /package/src/{legacylistItem/LegacyListItem.css → listItem/ListItem.css} +0 -0
  108. /package/src/{legacylistItem/LegacyListItem.less → listItem/ListItem.less} +0 -0
  109. /package/src/{legacylistItem/LegacyListItem.spec.tsx → listItem/ListItem.spec.tsx} +0 -0
  110. /package/src/{legacylistItem/LegacyListItem.story.tsx → listItem/ListItem.story.tsx} +0 -0
@@ -0,0 +1,188 @@
1
+ import Item, { Props as ItemProps } from './Item';
2
+ import { Meta, StoryObj } from '@storybook/react';
3
+ import React, { useState } from 'react';
4
+ import { Plus } from '@transferwise/icons';
5
+
6
+ import { lorem10, lorem20, lorem40, lorem5 } from '../test-utils';
7
+ import { Flag } from '@wise/art';
8
+
9
+ export default {
10
+ component: Item,
11
+ title: 'Content/Item',
12
+ } satisfies Meta<ItemProps>;
13
+
14
+ type Story = StoryObj<ItemProps>;
15
+
16
+ // TODO: delete this story later when we have more peace of the List Item puzzle
17
+ export const TempPlayground: Story = {
18
+ render: () => {
19
+ const [checked, setChecked] = useState(false);
20
+
21
+ return (
22
+ <div>
23
+ <Item
24
+ media={
25
+ <Item.AvatarView badge={{ type: 'action' }}>
26
+ <Flag code="BBD" />
27
+ </Item.AvatarView>
28
+ }
29
+ title="Test title"
30
+ subtitle="Test subtitle"
31
+ additionalInfo={<Item.AdditionalInfo>{lorem10}</Item.AdditionalInfo>}
32
+ />
33
+ <Item
34
+ media={
35
+ <Item.AvatarLayout
36
+ avatars={[{ asset: <Flag code="BBD" /> }, { asset: <Flag code="MXN" /> }]}
37
+ />
38
+ }
39
+ title="Test title"
40
+ subtitle={lorem10}
41
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
42
+ />
43
+ <Item
44
+ media={
45
+ <Item.AvatarLayout
46
+ size={72}
47
+ orientation="diagonal"
48
+ avatars={[{ asset: <Flag code="BBD" /> }, { asset: <Flag code="MXN" /> }]}
49
+ />
50
+ }
51
+ title="Test title"
52
+ subtitle={lorem10}
53
+ valueTitle="100 GBP"
54
+ valueSubtitle="100 USD"
55
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
56
+ />
57
+ <Item
58
+ media={<Item.AvatarView profileType="BUSINESS" notification />}
59
+ title="Test title"
60
+ subtitle="Test subtitle"
61
+ additionalInfo={
62
+ <Item.AdditionalInfo
63
+ action={{ label: 'Learn more', href: 'https://wise.com', target: '_blank' }}
64
+ >
65
+ {lorem10}
66
+ </Item.AdditionalInfo>
67
+ }
68
+ prompt={<Item.Prompt type="negative">You have done something wrong</Item.Prompt>}
69
+ />
70
+ <Item
71
+ media={<Item.AvatarView imgSrc="../avatar-square-dude.webp" selected />}
72
+ title="Test title"
73
+ additionalInfo={<Item.AdditionalInfo>{lorem10}</Item.AdditionalInfo>}
74
+ prompt={
75
+ <Item.Prompt
76
+ type="discount"
77
+ action={{
78
+ href: 'https://wise.com',
79
+ target: '_blank',
80
+ 'aria-label': 'clickable prompt',
81
+ }}
82
+ >
83
+ The whole prompt is secretly clickable
84
+ </Item.Prompt>
85
+ }
86
+ />
87
+
88
+ <Item
89
+ media={<Item.Image src="../apple-pay-logo.svg" />}
90
+ title="Accepting Apple Pay"
91
+ subtitle={lorem10}
92
+ />
93
+
94
+ <Item
95
+ media={<Item.Image src="../wise-card.svg" />}
96
+ title="Wise Business Card"
97
+ subtitle={lorem5}
98
+ />
99
+
100
+ <Item
101
+ title="Test title"
102
+ disabled
103
+ subtitle={lorem10}
104
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
105
+ control={<Item.Button priority="secondary-neutral">as Button</Item.Button>}
106
+ />
107
+ <Item
108
+ title="Test title"
109
+ disabled
110
+ subtitle={lorem10}
111
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
112
+ control={
113
+ <Item.IconButton>
114
+ <Plus />
115
+ </Item.IconButton>
116
+ }
117
+ />
118
+
119
+ <Item
120
+ title="Test title"
121
+ subtitle={lorem10}
122
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
123
+ control={
124
+ <Item.Button
125
+ priority="secondary-neutral"
126
+ as="a"
127
+ target="dfdf"
128
+ aria-label="adsfasfd"
129
+ href="wise.com"
130
+ >
131
+ as Link
132
+ </Item.Button>
133
+ }
134
+ />
135
+
136
+ <Item
137
+ title="Test title"
138
+ subtitle={lorem10}
139
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
140
+ control={<Item.Checkbox indeterminate />}
141
+ />
142
+
143
+ <Item
144
+ title="Test title"
145
+ subtitle={lorem10}
146
+ additionalInfo={<Item.AdditionalInfo>{lorem20}</Item.AdditionalInfo>}
147
+ control={
148
+ <Item.Switch
149
+ checked={checked}
150
+ onClick={() => {
151
+ setChecked(!checked);
152
+ }}
153
+ />
154
+ }
155
+ />
156
+ </div>
157
+ );
158
+ },
159
+ };
160
+
161
+ export const ItemRadio: Story = {
162
+ render: () => (
163
+ <div>
164
+ {/* Basic */}
165
+ {/* with additional info */}
166
+ {/* with prompt */}
167
+ {/* Disabled */}
168
+ {/* Spotlight */}
169
+ {/* Active */}
170
+ </div>
171
+ ),
172
+ };
173
+
174
+ export const ItemButton: Story = {
175
+ render: () => (
176
+ <div>
177
+ {/* Basic */}
178
+ {/* with additional info */}
179
+ {/* with prompt */}
180
+ {/* partially interactive */}
181
+ {/* Disabled */}
182
+ {/* Spotlight */}
183
+ {/* Active */}
184
+ </div>
185
+ ),
186
+ };
187
+
188
+ // add intaces with dark mode
@@ -0,0 +1,174 @@
1
+ import { createContext, ReactNode, useId, useMemo, useState } from 'react';
2
+ import { Typography } from '../common';
3
+ import Body from '../body';
4
+ import { AdditionalInfo } from './ItemAdditionalInfo';
5
+ import { IconButton } from './ItemIconButton';
6
+ import { Checkbox } from './ItemCheckbox';
7
+ import { Navigation } from './ItemNavigation';
8
+ import { clsx } from 'clsx';
9
+ import { Button } from './ItemButton';
10
+ import { Switch } from './ItemSwitch';
11
+ import { AvatarLayout, AvatarView, Image } from './ItemMedia';
12
+ import Prompt from './prompt/Prompt';
13
+
14
+ export type ItemTypes =
15
+ | 'non-interactive'
16
+ | 'navigation'
17
+ | 'radio'
18
+ | 'checkbox'
19
+ | 'switch'
20
+ | 'button'
21
+ | 'icon-button';
22
+
23
+ export type Props = {
24
+ as?: 'li' | 'div' | 'span';
25
+ inverted?: boolean;
26
+ disabled?: boolean;
27
+ spotlight?: 'active' | 'inactive';
28
+ title: ReactNode;
29
+ subtitle?: ReactNode;
30
+ additionalInfo?: ReactNode;
31
+ valueTitle?: ReactNode;
32
+ valueSubtitle?: ReactNode;
33
+ media?: ReactNode;
34
+ control?: ReactNode;
35
+ prompt?: ReactNode;
36
+ };
37
+
38
+ export type ItemContextData = {
39
+ setControlType: (type: ItemTypes) => void;
40
+ ids: {
41
+ label: string;
42
+ additionalInfo: string;
43
+ value: string;
44
+ control: string;
45
+ prompt: string;
46
+ };
47
+ props: Pick<Props, 'as' | 'disabled' | 'inverted'>;
48
+ };
49
+
50
+ // @ts-expect-error for now let's mock it with `null` value
51
+ // but actually by default we should specify `setControlType('none')`
52
+ export const ItemContext = createContext<ItemContextData>(null);
53
+
54
+ export const Item = ({
55
+ as: View = 'li',
56
+ title,
57
+ subtitle,
58
+ additionalInfo,
59
+ prompt,
60
+ inverted,
61
+ media,
62
+ spotlight = undefined,
63
+ valueTitle,
64
+ valueSubtitle,
65
+ control = null,
66
+ disabled,
67
+ }: Props) => {
68
+ /*
69
+ const returnType = (): ReactNode => {
70
+ switch (type) {
71
+ case 'Navigation':
72
+ return <Chevron orientation={Position.RIGHT} disabled />;
73
+ case 'Radio':
74
+ return <RadioButton name="Hello" checked />;
75
+ case 'Checkbox':
76
+ return <CheckboxButton name="Hello" checked />;
77
+ case 'Switch':
78
+ return <Switch onClick={() => console.log('clicked')} />;
79
+ case 'Button':
80
+ return <Button v2>Hello</Button>;
81
+ case 'IconButton':
82
+ return (
83
+ <IconButton size={40} priority="minimal">
84
+ <InfoCircle />
85
+ </IconButton>
86
+ );
87
+ }
88
+ };
89
+ */
90
+ const idPrefix = useId();
91
+
92
+ const [controlType, setControlType] = useState<ItemTypes>('non-interactive');
93
+ const ids = {
94
+ label: `${idPrefix}_label`,
95
+ value: `${idPrefix}_value`,
96
+ control: `${idPrefix}_control`,
97
+ prompt: `${idPrefix}_prompt`,
98
+ additionalInfo: `${idPrefix}_additional-info`,
99
+ };
100
+
101
+ const itemCtx = useMemo(
102
+ () => ({ setControlType, ids, props: { as: View, disabled, inverted } }),
103
+ [],
104
+ );
105
+
106
+ return (
107
+ <ItemContext.Provider value={itemCtx}>
108
+ <View
109
+ className={clsx(
110
+ 'np-item',
111
+ { 'np-item-interactive': controlType !== 'non-interactive' },
112
+ `np-item-${controlType}`,
113
+ 'd-flex flex-row',
114
+ { 'align-items-center': !subtitle },
115
+ {
116
+ [`np-item-spotlight-${spotlight}`]: !!spotlight,
117
+ },
118
+ )}
119
+ aria-describedby={[ids.additionalInfo].join(' ')}
120
+ >
121
+ {media && <div className="np-item-media">{media}</div>}
122
+
123
+ {/* Title + Subtitle + Values + Additional Info - Group */}
124
+ <div>
125
+ {/* Title + Subtitle + Values - Group */}
126
+ <div className="d-flex justify-content-between">
127
+ <span>
128
+ {/* @ts-expect-error div can have role and aria-lavel props */}
129
+ <Body
130
+ type={Typography.BODY_LARGE_BOLD}
131
+ className="np-item-title"
132
+ // for a11y this needs to be a header but for SEO it shouldn't be `h*` tag
133
+ // so we enable header semantics via `role` and `aria-level` attrs
134
+ role="heading"
135
+ aria-level="4"
136
+ >
137
+ {title}
138
+ </Body>
139
+ <Body className="np-item-subtitle">{subtitle}</Body>
140
+ </span>
141
+ {(valueTitle || valueSubtitle) && (
142
+ <span id={ids.value} className="np-item-value">
143
+ <Body type={Typography.BODY_LARGE_BOLD} className="np-item-title-value">
144
+ {valueTitle}
145
+ </Body>
146
+ <Body className="np-item-subtitle-value">{valueSubtitle}</Body>
147
+ </span>
148
+ )}
149
+ </div>
150
+
151
+ {/* Additional Info and Prompt here */}
152
+ {Boolean(subtitle) && additionalInfo}
153
+ </div>
154
+ {control === null ? null : <Body className="np-item-control">{control}</Body>}
155
+ {prompt}
156
+ </View>
157
+ </ItemContext.Provider>
158
+ );
159
+ };
160
+
161
+ /* eslint-disable functional/immutable-data */
162
+ Item.Image = Image;
163
+ Item.AvatarView = AvatarView;
164
+ Item.AvatarLayout = AvatarLayout;
165
+ Item.AdditionalInfo = AdditionalInfo;
166
+ Item.Checkbox = Checkbox;
167
+ Item.IconButton = IconButton;
168
+ Item.Navigation = Navigation;
169
+ Item.Button = Button;
170
+ Item.Switch = Switch;
171
+ Item.Prompt = Prompt;
172
+ /* eslint-enable functional/immutable-data */
173
+
174
+ export default Item;
@@ -0,0 +1,31 @@
1
+ import { PropsWithChildren, useContext } from 'react';
2
+ import { ItemContext, ItemContextData } from './Item';
3
+ import Body from '../body';
4
+ import Link, { LinkProps } from '../link';
5
+ import { Typography } from '../common';
6
+
7
+ export type ItemAdditionalInfoProps = PropsWithChildren<{
8
+ action?: Pick<LinkProps, 'href' | 'onClick' | 'target'> & { label?: string };
9
+ }>;
10
+
11
+ export const AdditionalInfo = function ({ children, action }: ItemAdditionalInfoProps) {
12
+ const { ids } = useContext<ItemContextData>(ItemContext);
13
+
14
+ return (
15
+ <Body
16
+ type={Typography.BODY_DEFAULT}
17
+ id={ids.additionalInfo}
18
+ className="np-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,90 @@
1
+ import { render, screen, mockMatchMedia } from '../test-utils';
2
+ import { Button as ItemButton } from './ItemButton';
3
+ import { ButtonPriority } from '../button/Button.types';
4
+ import { ItemContext } from './Item';
5
+
6
+ mockMatchMedia();
7
+
8
+ describe('ItemButton', () => {
9
+ const mockSetControlType = jest.fn();
10
+
11
+ const renderWithItemContext = (ui: React.ReactNode) => {
12
+ return render(
13
+ <ItemContext.Provider
14
+ value={{
15
+ setControlType: mockSetControlType,
16
+ ids: {
17
+ label: 'label',
18
+ additionalInfo: 'additional Info',
19
+ value: 'value',
20
+ control: 'control',
21
+ prompt: 'prompt',
22
+ },
23
+ props: {},
24
+ }}
25
+ >
26
+ {ui}
27
+ </ItemContext.Provider>,
28
+ );
29
+ };
30
+
31
+ beforeEach(() => {
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+ it('always sets v2 to true', () => {
36
+ renderWithItemContext(<ItemButton priority="primary">Test Button</ItemButton>);
37
+ const button = screen.getByRole('button');
38
+ expect(button).toBeInTheDocument();
39
+ expect(mockSetControlType).toHaveBeenCalledWith('button');
40
+ });
41
+
42
+ it('always sets size to "md"', () => {
43
+ renderWithItemContext(<ItemButton priority="primary">Test Button</ItemButton>);
44
+ const button = screen.getByRole('button');
45
+ expect(button).toHaveClass('wds-Button--medium');
46
+ });
47
+
48
+ it('supports all priorities', () => {
49
+ const priorities: ButtonPriority[] = ['primary', 'secondary', 'tertiary'];
50
+ priorities.forEach((priority) => {
51
+ renderWithItemContext(<ItemButton priority={priority}>Test {priority}</ItemButton>);
52
+ const button = screen.getByRole('button', { name: `Test ${priority}` });
53
+ expect(button).toBeInTheDocument();
54
+ });
55
+ });
56
+
57
+ it('renders as a button by default', () => {
58
+ renderWithItemContext(<ItemButton>Click me</ItemButton>);
59
+ const button = screen.getByRole('button');
60
+ expect(button).toBeInTheDocument();
61
+ expect(button.tagName).toBe('BUTTON');
62
+ });
63
+
64
+ it('renders as an anchor when href is provided', () => {
65
+ renderWithItemContext(<ItemButton href="https://example.com">Go to Example</ItemButton>);
66
+ const link = screen.getByRole('link', { name: 'Go to Example' });
67
+ expect(link).toBeInTheDocument();
68
+ expect(link.tagName).toBe('A');
69
+ expect(link).toHaveAttribute('href', 'https://example.com');
70
+ });
71
+
72
+ it('spreads additional props to the button', () => {
73
+ renderWithItemContext(<ItemButton aria-label="Custom Button">Custom</ItemButton>);
74
+ const button = screen.getByRole('button', { name: 'Custom Button' });
75
+ expect(button).toBeInTheDocument();
76
+ expect(button).toHaveAttribute('aria-label', 'Custom Button');
77
+ });
78
+
79
+ it('spreads additional props to the anchor', () => {
80
+ renderWithItemContext(
81
+ <ItemButton href="https://example.com" target="_blank" aria-label="Custom Link">
82
+ Custom Link
83
+ </ItemButton>,
84
+ );
85
+ const link = screen.getByRole('link', { name: 'Custom Link' });
86
+ expect(link).toBeInTheDocument();
87
+ expect(link).toHaveAttribute('href', 'https://example.com');
88
+ expect(link).toHaveAttribute('target', '_blank');
89
+ });
90
+ });
@@ -0,0 +1,18 @@
1
+ import { default as ButtonComp, NewButtonProps } from '../button';
2
+ import { useItemControl } from './useItemControl';
3
+
4
+ export type ItemButtonProps = Omit<NewButtonProps, 'v2' | 'size' | 'disabled'>;
5
+
6
+ export const Button = ({ priority = 'secondary', ...props }: ItemButtonProps) => {
7
+ const { baseItemProps } = useItemControl('button');
8
+
9
+ const commonProps = {
10
+ ...props,
11
+ priority,
12
+ v2: true,
13
+ size: 'md',
14
+ disabled: baseItemProps.disabled,
15
+ };
16
+
17
+ return <ButtonComp {...(commonProps as NewButtonProps)} />;
18
+ };
@@ -0,0 +1,11 @@
1
+ import CheckboxButton from '../checkboxButton';
2
+ import { CheckboxButtonProps } from '../checkboxButton/CheckboxButton';
3
+ import { useItemControl } from './useItemControl';
4
+
5
+ export type ItemCheckboxProps = Pick<CheckboxButtonProps, 'checked' | 'indeterminate' | 'onChange'>;
6
+
7
+ export const Checkbox = function (props: ItemCheckboxProps) {
8
+ const { baseItemProps } = useItemControl('checkbox');
9
+
10
+ return <CheckboxButton disabled={baseItemProps.disabled} {...props} />;
11
+ };
@@ -0,0 +1,13 @@
1
+ import { default as IconButtonComp, IconButtonProps } from '../iconButton';
2
+ import { useItemControl } from './useItemControl';
3
+ import { ReactNode } from 'react';
4
+
5
+ export type ItemIconButtonProps = Pick<IconButtonProps, 'onClick' | 'href' | 'target'> & {
6
+ children: ReactNode;
7
+ };
8
+
9
+ export const IconButton = function (props: ItemIconButtonProps) {
10
+ const { baseItemProps } = useItemControl('icon-button');
11
+
12
+ return <IconButtonComp {...props} size={32} disabled={baseItemProps.disabled} />;
13
+ };
@@ -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 ItemAvatarViewProps = Omit<AvatarViewProps, 'size' | 'interactive'> & SizeProp;
9
+
10
+ export const AvatarView = ({ className, size = 48, ...props }: ItemAvatarViewProps) => {
11
+ return (
12
+ <AvatarViewComp
13
+ {...props}
14
+ size={size}
15
+ className={clsx('np-item-media-avatar-view', className)}
16
+ />
17
+ );
18
+ };
19
+
20
+ export type ItemAvatarLayoutProps = Omit<AvatarLayoutProps, 'size' | 'interactive'> & SizeProp;
21
+
22
+ export const AvatarLayout = ({ className, size = 48, ...props }: ItemAvatarLayoutProps) => {
23
+ return (
24
+ <AvatarLayoutComp
25
+ {...props}
26
+ size={size}
27
+ className={clsx('np-item-media-avatar-layout', className)}
28
+ />
29
+ );
30
+ };
31
+
32
+ export type ItemImageProps = 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 }: ItemImageProps) => {
41
+ return (
42
+ <div
43
+ className={clsx('np-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,11 @@
1
+ import { ChevronRight, BillSplit as Disabled } from '@transferwise/icons';
2
+ import { ButtonProps } from '../button/Button.types';
3
+ import { useItemControl } from './useItemControl';
4
+
5
+ export type ItemNavigationProps = Pick<ButtonProps, 'onClick' | 'href'>;
6
+
7
+ export const Navigation = function Navigation({ onClick }: ItemNavigationProps) {
8
+ const { baseItemProps } = useItemControl('navigation');
9
+
10
+ return baseItemProps.disabled ? <Disabled size={24} /> : <ChevronRight size={24} />;
11
+ };
@@ -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,6 @@
1
+ export type { Props as ItemProps } from './Item';
2
+ export type { ItemAdditionalInfoProps } from './ItemAdditionalInfo';
3
+ export type { ItemCheckboxProps } from './ItemCheckbox';
4
+ export type { ItemImageProps, ItemAvatarViewProps, ItemAvatarLayoutProps } from './ItemMedia';
5
+
6
+ export { default } from './Item';
@@ -0,0 +1,77 @@
1
+ import { render, screen, mockMatchMedia } from '../../test-utils';
2
+ import { ItemPrompt } from './Prompt';
3
+ import { Sentiment } from '../../common';
4
+
5
+ mockMatchMedia();
6
+
7
+ describe('ItemPrompt', () => {
8
+ it('renders the aria-label when provided', () => {
9
+ const ariaLabel = 'Test aria-label';
10
+ render(
11
+ <ItemPrompt
12
+ type={Sentiment.POSITIVE}
13
+ action={{ 'aria-label': ariaLabel, href: 'https://example.com' }}
14
+ >
15
+ Positive prompt
16
+ </ItemPrompt>,
17
+ );
18
+
19
+ expect(screen.getByLabelText(ariaLabel)).toBeInTheDocument();
20
+ });
21
+
22
+ it('applies the interactive class when href is provided', () => {
23
+ render(
24
+ <ItemPrompt
25
+ type={Sentiment.POSITIVE}
26
+ action={{ href: 'https://example.com', 'aria-label': 'Interactive link' }}
27
+ >
28
+ Interactive link
29
+ </ItemPrompt>,
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
+ <ItemPrompt
38
+ type={Sentiment.POSITIVE}
39
+ action={{ onClick: jest.fn(), 'aria-label': 'Interactive button' }}
40
+ >
41
+ Interactive button
42
+ </ItemPrompt>,
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(<ItemPrompt type={Sentiment.POSITIVE}>Non-interactive prompt</ItemPrompt>);
50
+
51
+ expect(screen.getByText('Non-interactive prompt')).not.toHaveClass('np-prompt-interactive');
52
+ });
53
+
54
+ it('renders the children content', () => {
55
+ render(<ItemPrompt type={Sentiment.POSITIVE}>This is a child prompt</ItemPrompt>);
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
+ <ItemPrompt
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
+ </ItemPrompt>,
72
+ );
73
+
74
+ const link = screen.getByRole('link');
75
+ expect(link).toHaveAttribute('target', '_blank');
76
+ });
77
+ });