@transferwise/components 0.0.0-experimental-7fb2742 → 0.0.0-experimental-e240700
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.
- package/build/flowNavigation/animatedLabel/AnimatedLabel.js +1 -1
- package/build/flowNavigation/animatedLabel/AnimatedLabel.js.map +1 -1
- package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs +1 -1
- package/build/flowNavigation/animatedLabel/AnimatedLabel.mjs.map +1 -1
- package/build/index.js +4 -3
- package/build/index.js.map +1 -1
- package/build/index.mjs +3 -3
- package/build/listItem/List.js.map +1 -0
- package/build/listItem/List.mjs.map +1 -0
- package/build/{legacylistItem/LegacyListItem.js → listItem/ListItem.js} +3 -3
- package/build/listItem/ListItem.js.map +1 -0
- package/build/{legacylistItem/LegacyListItem.mjs → listItem/ListItem.mjs} +3 -3
- package/build/listItem/ListItem.mjs.map +1 -0
- package/build/main.css +185 -3
- package/build/phoneNumberInput/PhoneNumberInput.js +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.js.map +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.mjs +1 -1
- package/build/phoneNumberInput/PhoneNumberInput.mjs.map +1 -1
- package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js +2 -2
- package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js.map +1 -1
- package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.mjs +2 -2
- package/build/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.mjs.map +1 -1
- package/build/styles/item/Item.css +185 -0
- package/build/styles/main.css +185 -3
- package/build/styles/switch/Switch.css +0 -3
- package/build/test-utils/assets/apple-pay-logo.svg +84 -0
- package/build/types/index.d.ts +4 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/item/Item.d.ts +43 -0
- package/build/types/item/Item.d.ts.map +1 -0
- package/build/types/item/ItemAdditionalInfo.d.ts +9 -0
- package/build/types/item/ItemAdditionalInfo.d.ts.map +1 -0
- package/build/types/item/ItemButton.d.ts +4 -0
- package/build/types/item/ItemButton.d.ts.map +1 -0
- package/build/types/item/ItemCheckbox.d.ts +4 -0
- package/build/types/item/ItemCheckbox.d.ts.map +1 -0
- package/build/types/item/ItemIconButton.d.ts +7 -0
- package/build/types/item/ItemIconButton.d.ts.map +1 -0
- package/build/types/item/ItemMedia.d.ts +19 -0
- package/build/types/item/ItemMedia.d.ts.map +1 -0
- package/build/types/item/ItemNavigation.d.ts +4 -0
- package/build/types/item/ItemNavigation.d.ts.map +1 -0
- package/build/types/item/ItemSwitch.d.ts +3 -0
- package/build/types/item/ItemSwitch.d.ts.map +1 -0
- package/build/types/item/index.d.ts +6 -0
- package/build/types/item/index.d.ts.map +1 -0
- package/build/types/item/prompt/Prompt.d.ts +12 -0
- package/build/types/item/prompt/Prompt.d.ts.map +1 -0
- package/build/types/item/useItemControl.d.ts +5 -0
- package/build/types/item/useItemControl.d.ts.map +1 -0
- package/build/types/listItem/List.d.ts.map +1 -0
- package/build/types/{legacylistItem/LegacyListItem.d.ts → listItem/ListItem.d.ts} +4 -4
- package/build/types/listItem/ListItem.d.ts.map +1 -0
- package/build/types/listItem/index.d.ts +5 -0
- package/build/types/listItem/index.d.ts.map +1 -0
- package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts +2 -2
- package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts.map +1 -1
- package/build/types/test-utils/fake-data.d.ts +2 -0
- package/build/types/test-utils/fake-data.d.ts.map +1 -1
- package/build/upload/Upload.js +3 -14
- package/build/upload/Upload.js.map +1 -1
- package/build/upload/Upload.mjs +3 -14
- package/build/upload/Upload.mjs.map +1 -1
- package/package.json +3 -3
- package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +1 -1
- package/src/index.ts +4 -2
- package/src/item/Item.css +185 -0
- package/src/item/Item.less +180 -0
- package/src/item/Item.story.tsx +273 -0
- package/src/item/Item.tsx +181 -0
- package/src/item/ItemAdditionalInfo.tsx +31 -0
- package/src/item/ItemButton.spec.tsx +90 -0
- package/src/item/ItemButton.tsx +18 -0
- package/src/item/ItemCheckbox.tsx +11 -0
- package/src/item/ItemIconButton.tsx +13 -0
- package/src/item/ItemMedia.tsx +52 -0
- package/src/item/ItemNavigation.tsx +11 -0
- package/src/item/ItemSwitch.tsx +8 -0
- package/src/item/index.ts +6 -0
- package/src/item/prompt/Prompt.spec.tsx +77 -0
- package/src/item/prompt/Prompt.story.tsx +170 -0
- package/src/item/prompt/Prompt.tsx +44 -0
- package/src/item/useItemControl.tsx +12 -0
- package/src/{legacylistItem/LegacyListItem.tests.story.tsx → listItem/ListItem.tests.story.tsx} +2 -2
- package/src/{legacylistItem/LegacyListItem.tsx → listItem/ListItem.tsx} +3 -3
- package/src/listItem/index.ts +4 -0
- package/src/main.css +185 -3
- package/src/main.less +2 -1
- package/src/phoneNumberInput/PhoneNumberInput.spec.tsx +4 -4
- package/src/phoneNumberInput/PhoneNumberInput.story.tsx +0 -22
- package/src/phoneNumberInput/PhoneNumberInput.tsx +1 -1
- package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.ts +3 -4
- package/src/switch/Switch.css +0 -3
- package/src/switch/Switch.less +0 -1
- package/src/test-utils/assets/apple-pay-logo.svg +84 -0
- package/src/test-utils/fake-data.ts +5 -0
- package/src/upload/Upload.story.tsx +0 -52
- package/src/upload/Upload.tsx +3 -15
- package/build/legacylistItem/LegacyListItem.js.map +0 -1
- package/build/legacylistItem/LegacyListItem.mjs.map +0 -1
- package/build/legacylistItem/List.js.map +0 -1
- package/build/legacylistItem/List.mjs.map +0 -1
- package/build/types/legacylistItem/LegacyListItem.d.ts.map +0 -1
- package/build/types/legacylistItem/List.d.ts.map +0 -1
- package/build/types/legacylistItem/index.d.ts +0 -5
- package/build/types/legacylistItem/index.d.ts.map +0 -1
- package/src/legacylistItem/index.ts +0 -4
- /package/build/{legacylistItem → listItem}/List.js +0 -0
- /package/build/{legacylistItem → listItem}/List.mjs +0 -0
- /package/build/styles/{legacylistItem/LegacyListItem.css → listItem/ListItem.css} +0 -0
- /package/build/types/{legacylistItem → listItem}/List.d.ts +0 -0
- /package/src/{legacylistItem → listItem}/List.tsx +0 -0
- /package/src/{legacylistItem/LegacyListItem.css → listItem/ListItem.css} +0 -0
- /package/src/{legacylistItem/LegacyListItem.less → listItem/ListItem.less} +0 -0
- /package/src/{legacylistItem/LegacyListItem.spec.tsx → listItem/ListItem.spec.tsx} +0 -0
- /package/src/{legacylistItem/LegacyListItem.story.tsx → listItem/ListItem.story.tsx} +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
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 className="np-item-body">
|
|
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
|
|
143
|
+
id={ids.value}
|
|
144
|
+
className={clsx('np-item-value', 'd-flex align-items-center', {
|
|
145
|
+
'flex-column': valueTitle !== undefined && valueSubtitle !== undefined,
|
|
146
|
+
})}
|
|
147
|
+
>
|
|
148
|
+
{valueTitle && (
|
|
149
|
+
<Body type={Typography.BODY_LARGE_BOLD} className="np-item-title-value">
|
|
150
|
+
{valueTitle}
|
|
151
|
+
</Body>
|
|
152
|
+
)}
|
|
153
|
+
{valueSubtitle && <Body className="np-item-subtitle-value">{valueSubtitle}</Body>}
|
|
154
|
+
</span>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Additional Info and Prompt here */}
|
|
159
|
+
{Boolean(subtitle) && additionalInfo}
|
|
160
|
+
</div>
|
|
161
|
+
{control === null ? null : <Body className="np-item-control">{control}</Body>}
|
|
162
|
+
{prompt}
|
|
163
|
+
</View>
|
|
164
|
+
</ItemContext.Provider>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/* eslint-disable functional/immutable-data */
|
|
169
|
+
Item.Image = Image;
|
|
170
|
+
Item.AvatarView = AvatarView;
|
|
171
|
+
Item.AvatarLayout = AvatarLayout;
|
|
172
|
+
Item.AdditionalInfo = AdditionalInfo;
|
|
173
|
+
Item.Checkbox = Checkbox;
|
|
174
|
+
Item.IconButton = IconButton;
|
|
175
|
+
Item.Navigation = Navigation;
|
|
176
|
+
Item.Button = Button;
|
|
177
|
+
Item.Switch = Switch;
|
|
178
|
+
Item.Prompt = Prompt;
|
|
179
|
+
/* eslint-enable functional/immutable-data */
|
|
180
|
+
|
|
181
|
+
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
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import Link from '../../link';
|
|
2
|
+
import { Sentiment } from '../../common';
|
|
3
|
+
import { ItemPrompt, ItemPromptProps } from './Prompt';
|
|
4
|
+
import { lorem40 } from '../../test-utils';
|
|
5
|
+
import { StoryObj } from '@storybook/react';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
component: ItemPrompt,
|
|
9
|
+
title: 'Content/Item/Prompt',
|
|
10
|
+
args: {
|
|
11
|
+
type: Sentiment.NEGATIVE,
|
|
12
|
+
children: 'You have done a terrible thing',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
type: {
|
|
16
|
+
options: [
|
|
17
|
+
Sentiment.POSITIVE,
|
|
18
|
+
Sentiment.NEGATIVE,
|
|
19
|
+
Sentiment.NEUTRAL,
|
|
20
|
+
Sentiment.WARNING,
|
|
21
|
+
'discount',
|
|
22
|
+
'savings',
|
|
23
|
+
],
|
|
24
|
+
control: { type: 'radio' },
|
|
25
|
+
description: 'The type of prompt to display',
|
|
26
|
+
},
|
|
27
|
+
action: {
|
|
28
|
+
table: {
|
|
29
|
+
disable: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
children: {
|
|
33
|
+
type: {
|
|
34
|
+
name: 'string',
|
|
35
|
+
required: true,
|
|
36
|
+
description: 'The content of the prompt',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type CustomStory = StoryObj<
|
|
43
|
+
ItemPromptProps & {
|
|
44
|
+
interactivity?: 'none' | 'full-anchor' | 'full-button' | 'link';
|
|
45
|
+
}
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
export const Basic: CustomStory = {
|
|
49
|
+
args: {
|
|
50
|
+
interactivity: 'none',
|
|
51
|
+
},
|
|
52
|
+
argTypes: {
|
|
53
|
+
interactivity: {
|
|
54
|
+
options: ['none', 'full-button', 'full-anchor', 'link'],
|
|
55
|
+
control: { type: 'radio' },
|
|
56
|
+
description: 'The type of interactivity to display',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
render: ({ interactivity, ...args }) => {
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<ItemPrompt
|
|
63
|
+
type={args.type}
|
|
64
|
+
action={
|
|
65
|
+
interactivity === 'full-anchor'
|
|
66
|
+
? { href: 'wise.com' }
|
|
67
|
+
: interactivity === 'full-button'
|
|
68
|
+
? {
|
|
69
|
+
onClick: () => {
|
|
70
|
+
console.log('clicked');
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
: undefined
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
{args.children} {interactivity === 'link' && <Link href="www.wise.com">with a link</Link>}
|
|
77
|
+
</ItemPrompt>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Variants = () => {
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<div className="m-b-2">
|
|
87
|
+
<ItemPrompt type={Sentiment.POSITIVE}>
|
|
88
|
+
Positive prompt <Link href="www.wise.com">with a link to amazing content</Link>
|
|
89
|
+
</ItemPrompt>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="m-b-2">
|
|
92
|
+
<ItemPrompt type={Sentiment.NEGATIVE}>
|
|
93
|
+
Negative prompt <Link href="www.wise.com">with a link to amazing content</Link>
|
|
94
|
+
</ItemPrompt>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="m-b-2">
|
|
97
|
+
<ItemPrompt type={Sentiment.NEUTRAL}>
|
|
98
|
+
Neutral prompt <Link href="www.wise.com">with a link to amazing content</Link>
|
|
99
|
+
</ItemPrompt>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="m-b-2">
|
|
102
|
+
<ItemPrompt type={Sentiment.WARNING}>
|
|
103
|
+
Warning prompt <Link href="www.wise.com">with a link to amazing content</Link>
|
|
104
|
+
</ItemPrompt>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="m-b-2">
|
|
107
|
+
<ItemPrompt type="discount">
|
|
108
|
+
Discount prompt <Link href="www.wise.com">with a link to amazing content</Link>
|
|
109
|
+
</ItemPrompt>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="m-b-2">
|
|
112
|
+
<ItemPrompt type="savings">
|
|
113
|
+
Savings prompt <Link href="www.wise.com">with a link to amazing content</Link>
|
|
114
|
+
</ItemPrompt>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="m-b-2">
|
|
117
|
+
<ItemPrompt
|
|
118
|
+
type="discount"
|
|
119
|
+
action={{
|
|
120
|
+
href: 'www.wise.com',
|
|
121
|
+
target: '_self',
|
|
122
|
+
'aria-label': 'hidden anchor tags are accessible, right?',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
The whole prompt is secretly clickable
|
|
126
|
+
</ItemPrompt>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="m-b-2">
|
|
129
|
+
<ItemPrompt
|
|
130
|
+
type="negative"
|
|
131
|
+
action={{
|
|
132
|
+
href: 'www.wise.com',
|
|
133
|
+
target: '_self',
|
|
134
|
+
'aria-label': 'clickable <a>',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
The whole prompt is secretly clickable as an anchor tag
|
|
138
|
+
</ItemPrompt>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="m-b-2">
|
|
141
|
+
<ItemPrompt
|
|
142
|
+
type="warning"
|
|
143
|
+
action={{
|
|
144
|
+
onClick: () => {
|
|
145
|
+
console.log('clicked');
|
|
146
|
+
},
|
|
147
|
+
'aria-label': 'clickable button',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
The whole prompt is secretly clickable as a button
|
|
151
|
+
</ItemPrompt>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="m-b-2">
|
|
154
|
+
<ItemPrompt
|
|
155
|
+
type="neutral"
|
|
156
|
+
action={{
|
|
157
|
+
href: 'www.wise.com',
|
|
158
|
+
target: '_self',
|
|
159
|
+
'aria-label': 'clickable prompt',
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
The whole prompt is secretly clickable
|
|
163
|
+
</ItemPrompt>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="m-b-2" style={{ maxWidth: '300px' }}>
|
|
166
|
+
<ItemPrompt type="warning">The prompt has very long text that wraps {lorem40}</ItemPrompt>
|
|
167
|
+
</div>
|
|
168
|
+
</>
|
|
169
|
+
);
|
|
170
|
+
};
|