@transferwise/components 0.0.0-experimental-63c3fc6 → 0.0.0-experimental-5f03867
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/Snackbar/Snackbar.js +178 -102
- package/build/Snackbar/Snackbar.js.map +1 -1
- package/build/Snackbar/Snackbar.messages.js +14 -0
- package/build/Snackbar/Snackbar.messages.js.map +1 -0
- package/build/Snackbar/Snackbar.messages.mjs +10 -0
- package/build/Snackbar/Snackbar.messages.mjs.map +1 -0
- package/build/Snackbar/Snackbar.mjs +180 -101
- package/build/Snackbar/Snackbar.mjs.map +1 -1
- package/build/Snackbar/SnackbarContext.js.map +1 -1
- package/build/Snackbar/SnackbarContext.mjs.map +1 -1
- package/build/Snackbar/SnackbarPortal.js +12 -0
- package/build/Snackbar/SnackbarPortal.js.map +1 -0
- package/build/Snackbar/SnackbarPortal.mjs +8 -0
- package/build/Snackbar/SnackbarPortal.mjs.map +1 -0
- package/build/Snackbar/SnackbarProvider.js +24 -28
- package/build/Snackbar/SnackbarProvider.js.map +1 -1
- package/build/Snackbar/SnackbarProvider.mjs +25 -29
- package/build/Snackbar/SnackbarProvider.mjs.map +1 -1
- package/build/Snackbar/constants.js +17 -0
- package/build/Snackbar/constants.js.map +1 -0
- package/build/Snackbar/constants.mjs +11 -0
- package/build/Snackbar/constants.mjs.map +1 -0
- package/build/Snackbar/useSnackbarAutoDismiss.js +66 -0
- package/build/Snackbar/useSnackbarAutoDismiss.js.map +1 -0
- package/build/Snackbar/useSnackbarAutoDismiss.mjs +62 -0
- package/build/Snackbar/useSnackbarAutoDismiss.mjs.map +1 -0
- package/build/Snackbar/useSnackbarManager.js +153 -0
- package/build/Snackbar/useSnackbarManager.js.map +1 -0
- package/build/Snackbar/useSnackbarManager.mjs +149 -0
- package/build/Snackbar/useSnackbarManager.mjs.map +1 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.js +135 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.js.map +1 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.mjs +131 -0
- package/build/Snackbar/useSnackbarSwipeDismiss.mjs.map +1 -0
- package/build/i18n/en.json +1 -0
- package/build/i18n/en.json.js +1 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +1 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +0 -4
- package/build/index.js.map +1 -1
- package/build/index.mjs +0 -2
- package/build/index.mjs.map +1 -1
- package/build/main.css +116 -191
- package/build/styles/Snackbar/Snackbar.css +117 -54
- package/build/styles/main.css +116 -191
- package/build/types/Snackbar/Snackbar.d.ts +55 -9
- package/build/types/Snackbar/Snackbar.d.ts.map +1 -1
- package/build/types/Snackbar/Snackbar.messages.d.ts +9 -0
- package/build/types/Snackbar/Snackbar.messages.d.ts.map +1 -0
- package/build/types/Snackbar/Snackbar.types.d.ts +12 -0
- package/build/types/Snackbar/Snackbar.types.d.ts.map +1 -0
- package/build/types/Snackbar/SnackbarContext.d.ts +2 -2
- package/build/types/Snackbar/SnackbarContext.d.ts.map +1 -1
- package/build/types/Snackbar/SnackbarPortal.d.ts +4 -0
- package/build/types/Snackbar/SnackbarPortal.d.ts.map +1 -0
- package/build/types/Snackbar/SnackbarProvider.d.ts +7 -0
- package/build/types/Snackbar/SnackbarProvider.d.ts.map +1 -1
- package/build/types/Snackbar/constants.d.ts +9 -0
- package/build/types/Snackbar/constants.d.ts.map +1 -0
- package/build/types/Snackbar/index.d.ts +1 -1
- package/build/types/Snackbar/index.d.ts.map +1 -1
- package/build/types/Snackbar/useSnackbar.d.ts +1 -1
- package/build/types/Snackbar/useSnackbar.d.ts.map +1 -1
- package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts +13 -0
- package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts.map +1 -0
- package/build/types/Snackbar/useSnackbarManager.d.ts +20 -0
- package/build/types/Snackbar/useSnackbarManager.d.ts.map +1 -0
- package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts +22 -0
- package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts.map +1 -0
- package/build/types/index.d.ts +1 -5
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +2 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/Snackbar/Snackbar.css +117 -54
- package/src/Snackbar/Snackbar.less +139 -60
- package/src/Snackbar/Snackbar.messages.ts +9 -0
- package/src/Snackbar/Snackbar.test.tsx +989 -119
- package/src/Snackbar/Snackbar.tsx +208 -93
- package/src/Snackbar/Snackbar.types.ts +17 -0
- package/src/Snackbar/SnackbarContext.ts +2 -2
- package/src/Snackbar/SnackbarPortal.tsx +8 -0
- package/src/Snackbar/SnackbarProvider.test.tsx +783 -0
- package/src/Snackbar/SnackbarProvider.tsx +27 -19
- package/src/Snackbar/_stories/Snackbar.accessibility.docs.mdx +66 -0
- package/src/Snackbar/_stories/Snackbar.migration.docs.mdx +64 -0
- package/src/Snackbar/_stories/Snackbar.rtl.test.story.tsx +16 -8
- package/src/Snackbar/_stories/Snackbar.story.tsx +389 -61
- package/src/Snackbar/_stories/Snackbar.test.story.tsx +171 -47
- package/src/Snackbar/constants.ts +10 -0
- package/src/Snackbar/index.ts +1 -1
- package/src/Snackbar/useSnackbarAutoDismiss.ts +74 -0
- package/src/Snackbar/useSnackbarManager.ts +169 -0
- package/src/Snackbar/useSnackbarSwipeDismiss.ts +169 -0
- package/src/i18n/en.json +1 -0
- package/src/index.ts +1 -14
- package/src/main.css +116 -191
- package/src/main.less +0 -2
- package/build/CardContainer/CardContainer.js +0 -33
- package/build/CardContainer/CardContainer.js.map +0 -1
- package/build/CardContainer/CardContainer.mjs +0 -29
- package/build/CardContainer/CardContainer.mjs.map +0 -1
- package/build/CardWidget/CardWidget.js +0 -94
- package/build/CardWidget/CardWidget.js.map +0 -1
- package/build/CardWidget/CardWidget.mjs +0 -90
- package/build/CardWidget/CardWidget.mjs.map +0 -1
- package/build/CardWidget/CardWidgetAction.js +0 -70
- package/build/CardWidget/CardWidgetAction.js.map +0 -1
- package/build/CardWidget/CardWidgetAction.mjs +0 -65
- package/build/CardWidget/CardWidgetAction.mjs.map +0 -1
- package/build/styles/CardContainer/CardContainer.css +0 -74
- package/build/styles/CardWidget/CardWidget.css +0 -64
- package/build/types/CardContainer/CardContainer.d.ts +0 -8
- package/build/types/CardContainer/CardContainer.d.ts.map +0 -1
- package/build/types/CardContainer/CardContainer.types.d.ts +0 -44
- package/build/types/CardContainer/CardContainer.types.d.ts.map +0 -1
- package/build/types/CardContainer/_stories/helpers.d.ts +0 -3
- package/build/types/CardContainer/_stories/helpers.d.ts.map +0 -1
- package/build/types/CardContainer/index.d.ts +0 -3
- package/build/types/CardContainer/index.d.ts.map +0 -1
- package/build/types/CardWidget/CardWidget.d.ts +0 -8
- package/build/types/CardWidget/CardWidget.d.ts.map +0 -1
- package/build/types/CardWidget/CardWidget.types.d.ts +0 -52
- package/build/types/CardWidget/CardWidget.types.d.ts.map +0 -1
- package/build/types/CardWidget/CardWidgetAction.d.ts +0 -54
- package/build/types/CardWidget/CardWidgetAction.d.ts.map +0 -1
- package/build/types/CardWidget/index.d.ts +0 -4
- package/build/types/CardWidget/index.d.ts.map +0 -1
- package/src/CardContainer/CardContainer.css +0 -74
- package/src/CardContainer/CardContainer.less +0 -112
- package/src/CardContainer/CardContainer.test.tsx +0 -118
- package/src/CardContainer/CardContainer.tsx +0 -39
- package/src/CardContainer/CardContainer.types.ts +0 -50
- package/src/CardContainer/_stories/CardContainer.story.tsx +0 -136
- package/src/CardContainer/_stories/CardContainer.test.story.tsx +0 -103
- package/src/CardContainer/_stories/helpers.tsx +0 -50
- package/src/CardContainer/index.ts +0 -6
- package/src/CardWidget/CardWidget.css +0 -64
- package/src/CardWidget/CardWidget.less +0 -82
- package/src/CardWidget/CardWidget.test.tsx +0 -189
- package/src/CardWidget/CardWidget.tsx +0 -74
- package/src/CardWidget/CardWidget.types.ts +0 -60
- package/src/CardWidget/CardWidgetAction.tsx +0 -126
- package/src/CardWidget/_stories/CardWidget.story.tsx +0 -209
- package/src/CardWidget/_stories/CardWidget.test.story.tsx +0 -350
- package/src/CardWidget/index.ts +0 -3
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
-
import { useTheme } from '@wise/components-theming';
|
|
3
|
-
|
|
4
|
-
import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
5
|
-
import { withVariantConfig } from '../../../.storybook/helpers';
|
|
6
|
-
import Body from '../../Body';
|
|
7
|
-
import Title from '../../Title';
|
|
8
|
-
import CardContainer from '..';
|
|
9
|
-
import type { CardContainerProps, CardContainerSpotlight } from '..';
|
|
10
|
-
|
|
11
|
-
const meta: Meta<typeof CardContainer> = {
|
|
12
|
-
component: CardContainer,
|
|
13
|
-
tags: ['!autodocs', '!manifest'],
|
|
14
|
-
title: 'Layouts/CardContainer/Tests',
|
|
15
|
-
};
|
|
16
|
-
export default meta;
|
|
17
|
-
type Story = StoryObj<CardContainerProps>;
|
|
18
|
-
|
|
19
|
-
const SPOTLIGHTS: (CardContainerSpotlight | undefined)[] = [undefined, 'active', 'inactive'];
|
|
20
|
-
|
|
21
|
-
const ThemeTitle = () => {
|
|
22
|
-
const { theme, screenMode } = useTheme();
|
|
23
|
-
return (
|
|
24
|
-
<Title type="title-subsection">
|
|
25
|
-
{theme} / {screenMode}
|
|
26
|
-
</Title>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** Fills the content area, so a padding-none card's corners show the clipping. */
|
|
31
|
-
const FullBleedContent = () => (
|
|
32
|
-
<div style={{ height: '80px', background: 'var(--color-background-screen-active)' }} />
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Every spotlight against every theme, with and without padding. Without padding, the content
|
|
37
|
-
* should be clipped to the rounded corners, and the `inactive` dashed stroke should stay whole.
|
|
38
|
-
*
|
|
39
|
-
* The `active` spotlight's resting shadow is only applied in the light `personal`, `business` and
|
|
40
|
-
* `platform` themes; every `dark`, `bright-green` and `forest-green` column should show a flat
|
|
41
|
-
* surface.
|
|
42
|
-
*/
|
|
43
|
-
export const AllVariants: Story = {
|
|
44
|
-
...withVariantConfig([
|
|
45
|
-
'default',
|
|
46
|
-
'dark',
|
|
47
|
-
'bright-green',
|
|
48
|
-
'forest-green',
|
|
49
|
-
'business',
|
|
50
|
-
'business--bright-green',
|
|
51
|
-
'business--forest-green',
|
|
52
|
-
'platform',
|
|
53
|
-
'platform--forest-green',
|
|
54
|
-
]),
|
|
55
|
-
render: () => (
|
|
56
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px', maxWidth: '420px' }}>
|
|
57
|
-
<ThemeTitle />
|
|
58
|
-
{SPOTLIGHTS.map((spotlight) => (
|
|
59
|
-
<CardContainer key={spotlight ?? 'none'} spotlight={spotlight}>
|
|
60
|
-
<Body>Spotlight: {spotlight ?? 'not set'}</Body>
|
|
61
|
-
<Body>Second line, to show the content stacks flush.</Body>
|
|
62
|
-
</CardContainer>
|
|
63
|
-
))}
|
|
64
|
-
{SPOTLIGHTS.map((spotlight) => (
|
|
65
|
-
<CardContainer key={`none-${spotlight ?? 'none'}`} spotlight={spotlight} padding="none">
|
|
66
|
-
<FullBleedContent />
|
|
67
|
-
</CardContainer>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
),
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The inactive spotlight's dashed stroke and the padding should mirror in RTL. Without padding,
|
|
75
|
-
* the text should start at the inline-start edge.
|
|
76
|
-
*/
|
|
77
|
-
export const RTLDiff: Story = {
|
|
78
|
-
render: () => {
|
|
79
|
-
const i18n = useRtlDiffI18n();
|
|
80
|
-
return (
|
|
81
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px', width: 420 }}>
|
|
82
|
-
{SPOTLIGHTS.map((spotlight) => (
|
|
83
|
-
<CardContainer key={spotlight ?? 'none'} spotlight={spotlight}>
|
|
84
|
-
<Body>{i18n.content}</Body>
|
|
85
|
-
</CardContainer>
|
|
86
|
-
))}
|
|
87
|
-
{SPOTLIGHTS.map((spotlight) => (
|
|
88
|
-
<CardContainer key={`none-${spotlight ?? 'none'}`} spotlight={spotlight} padding="none">
|
|
89
|
-
<Body>{i18n.content}</Body>
|
|
90
|
-
</CardContainer>
|
|
91
|
-
))}
|
|
92
|
-
</div>
|
|
93
|
-
);
|
|
94
|
-
},
|
|
95
|
-
...withRTLDiff({
|
|
96
|
-
i18n: {
|
|
97
|
-
content: {
|
|
98
|
-
en: 'Your virtual card is ready to use.',
|
|
99
|
-
ar: 'بطاقتك الافتراضية جاهزة للاستخدام.',
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
}),
|
|
103
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Card, Jar, Travel } from '@transferwise/icons';
|
|
2
|
-
|
|
3
|
-
import Body from '../../Body';
|
|
4
|
-
import List from '../../List';
|
|
5
|
-
import ListItem from '../../ListItem';
|
|
6
|
-
|
|
7
|
-
export const SampleContent = () => (
|
|
8
|
-
<>
|
|
9
|
-
<Body>Your virtual card is ready to use.</Body>
|
|
10
|
-
<Body>Spend in 40+ currencies with the real exchange rate.</Body>
|
|
11
|
-
</>
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
export const CardsList = () => (
|
|
15
|
-
<List>
|
|
16
|
-
<ListItem
|
|
17
|
-
title="•••• 1582"
|
|
18
|
-
subtitle="Current account"
|
|
19
|
-
valueTitle="£0 spent"
|
|
20
|
-
media={
|
|
21
|
-
<ListItem.AvatarView>
|
|
22
|
-
<Card />
|
|
23
|
-
</ListItem.AvatarView>
|
|
24
|
-
}
|
|
25
|
-
control={<ListItem.Navigation href="#card-1582" />}
|
|
26
|
-
/>
|
|
27
|
-
<ListItem
|
|
28
|
-
title="•••• 9982"
|
|
29
|
-
subtitle="Jar"
|
|
30
|
-
valueTitle="£410 / £500 day"
|
|
31
|
-
media={
|
|
32
|
-
<ListItem.AvatarView>
|
|
33
|
-
<Jar />
|
|
34
|
-
</ListItem.AvatarView>
|
|
35
|
-
}
|
|
36
|
-
control={<ListItem.Navigation href="#card-9982" />}
|
|
37
|
-
/>
|
|
38
|
-
<ListItem
|
|
39
|
-
title="•••• 5507"
|
|
40
|
-
subtitle="Travel"
|
|
41
|
-
valueTitle="£200 / £500 day"
|
|
42
|
-
media={
|
|
43
|
-
<ListItem.AvatarView>
|
|
44
|
-
<Travel />
|
|
45
|
-
</ListItem.AvatarView>
|
|
46
|
-
}
|
|
47
|
-
control={<ListItem.Navigation href="#card-5507" />}
|
|
48
|
-
/>
|
|
49
|
-
</List>
|
|
50
|
-
);
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
.wds-card-widget {
|
|
2
|
-
--CardWidget-inset: var(--size-8);
|
|
3
|
-
--CardWidget-header-spacing: var(--size-8);
|
|
4
|
-
--CardWidget-actions-gap: var(--size-8);
|
|
5
|
-
}
|
|
6
|
-
.wds-card-widget__header {
|
|
7
|
-
align-items: center;
|
|
8
|
-
-moz-column-gap: var(--size-24);
|
|
9
|
-
column-gap: var(--size-24);
|
|
10
|
-
container-type: inline-size;
|
|
11
|
-
display: flex;
|
|
12
|
-
flex-wrap: wrap;
|
|
13
|
-
padding-inline-start: var(--CardWidget-inset);
|
|
14
|
-
row-gap: var(--size-8);
|
|
15
|
-
}
|
|
16
|
-
.wds-card-widget__header:not(:last-child) {
|
|
17
|
-
margin-block-end: var(--CardWidget-header-spacing);
|
|
18
|
-
}
|
|
19
|
-
.wds-card-widget__title {
|
|
20
|
-
align-items: center;
|
|
21
|
-
color: var(--color-content-secondary);
|
|
22
|
-
display: grid;
|
|
23
|
-
flex: 1 1 0;
|
|
24
|
-
margin: 0;
|
|
25
|
-
min-block-size: var(--size-32);
|
|
26
|
-
}
|
|
27
|
-
@container (width <= 360px) {
|
|
28
|
-
.wds-card-widget__title {
|
|
29
|
-
flex-basis: auto;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
.wds-card-widget__title-link.np-link {
|
|
33
|
-
color: inherit;
|
|
34
|
-
display: inline;
|
|
35
|
-
font: inherit;
|
|
36
|
-
letter-spacing: inherit;
|
|
37
|
-
text-align: start;
|
|
38
|
-
-webkit-text-decoration: none;
|
|
39
|
-
text-decoration: none;
|
|
40
|
-
}
|
|
41
|
-
.wds-card-widget__title-link.np-link:hover,
|
|
42
|
-
.wds-card-widget__title-link.np-link:focus-visible {
|
|
43
|
-
-webkit-text-decoration: underline;
|
|
44
|
-
text-decoration: underline;
|
|
45
|
-
}
|
|
46
|
-
.wds-card-widget__title-link.np-link .wds-card-widget__title-chevron {
|
|
47
|
-
white-space: nowrap;
|
|
48
|
-
}
|
|
49
|
-
.wds-card-widget__title-link.np-link .wds-card-widget__title-chevron .tw-icon {
|
|
50
|
-
display: inline-block;
|
|
51
|
-
margin: 0;
|
|
52
|
-
vertical-align: text-bottom;
|
|
53
|
-
}
|
|
54
|
-
.wds-card-widget__actions {
|
|
55
|
-
align-self: start;
|
|
56
|
-
align-items: center;
|
|
57
|
-
display: flex;
|
|
58
|
-
flex: none;
|
|
59
|
-
gap: var(--CardWidget-actions-gap);
|
|
60
|
-
margin-inline-start: auto;
|
|
61
|
-
}
|
|
62
|
-
.wds-card-widget--with-header .wds-card-widget__content {
|
|
63
|
-
padding-inline: var(--CardWidget-inset);
|
|
64
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Above this header width a long title wraps beside the action corner. At or below it, the title
|
|
2
|
-
// only shares a line with the actions while it fits on one line, so it keeps room to read.
|
|
3
|
-
@wds-card-widget-actions-stack-width: 360px;
|
|
4
|
-
|
|
5
|
-
.wds-card-widget {
|
|
6
|
-
--CardWidget-inset: var(--size-8);
|
|
7
|
-
--CardWidget-header-spacing: var(--size-8);
|
|
8
|
-
--CardWidget-actions-gap: var(--size-8);
|
|
9
|
-
|
|
10
|
-
// The header's title row is at least as tall as the 32px action buttons, so cards side by side
|
|
11
|
-
// line up their titles and content whether or not their headers have actions, or have stacked
|
|
12
|
-
// them. The actions wrap under the title when they don't fit beside it.
|
|
13
|
-
&__header {
|
|
14
|
-
align-items: center;
|
|
15
|
-
column-gap: var(--size-24);
|
|
16
|
-
container-type: inline-size;
|
|
17
|
-
display: flex;
|
|
18
|
-
flex-wrap: wrap;
|
|
19
|
-
padding-inline-start: var(--CardWidget-inset);
|
|
20
|
-
row-gap: var(--size-8);
|
|
21
|
-
|
|
22
|
-
&:not(:last-child) {
|
|
23
|
-
margin-bottom: var(--CardWidget-header-spacing);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
&__title {
|
|
28
|
-
align-items: center;
|
|
29
|
-
color: var(--color-content-secondary);
|
|
30
|
-
display: grid;
|
|
31
|
-
flex: 1 1 0;
|
|
32
|
-
margin: 0;
|
|
33
|
-
min-height: var(--size-32);
|
|
34
|
-
|
|
35
|
-
@container (width <= @wds-card-widget-actions-stack-width) {
|
|
36
|
-
flex-basis: auto;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// The title is a `Link`, but keeps the title's typography and colour rather than the link
|
|
41
|
-
// text style, and flows inline so the chevron can follow its last word.
|
|
42
|
-
&__title-link.np-link {
|
|
43
|
-
color: inherit;
|
|
44
|
-
display: inline;
|
|
45
|
-
font: inherit;
|
|
46
|
-
letter-spacing: inherit;
|
|
47
|
-
// `Link` renders a button when there is no `href`, which would otherwise centre the title.
|
|
48
|
-
text-align: start;
|
|
49
|
-
text-decoration: none;
|
|
50
|
-
|
|
51
|
-
&:hover,
|
|
52
|
-
&:focus-visible {
|
|
53
|
-
text-decoration: underline;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Nested so it outweighs `Link`'s own `.np-link .tw-icon`, which loads later.
|
|
57
|
-
.wds-card-widget__title-chevron {
|
|
58
|
-
white-space: nowrap;
|
|
59
|
-
|
|
60
|
-
.tw-icon {
|
|
61
|
-
display: inline-block;
|
|
62
|
-
margin: 0;
|
|
63
|
-
vertical-align: text-bottom;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
&__actions {
|
|
69
|
-
// Stays in the top corner when a long title wraps onto more lines.
|
|
70
|
-
align-self: start;
|
|
71
|
-
align-items: center;
|
|
72
|
-
display: flex;
|
|
73
|
-
flex: none;
|
|
74
|
-
gap: var(--CardWidget-actions-gap);
|
|
75
|
-
margin-inline-start: auto;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// With a header, the content is inset further from both inline edges.
|
|
79
|
-
&--with-header &__content {
|
|
80
|
-
padding-inline: var(--CardWidget-inset);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { Plus, Settings } from '@transferwise/icons';
|
|
2
|
-
|
|
3
|
-
import { mockMatchMedia, render, screen, userEvent } from '../test-utils';
|
|
4
|
-
import CardWidget from './CardWidget';
|
|
5
|
-
import type { CardWidgetHeader } from './CardWidget.types';
|
|
6
|
-
|
|
7
|
-
mockMatchMedia();
|
|
8
|
-
|
|
9
|
-
describe('CardWidget', () => {
|
|
10
|
-
const content = 'Widget content';
|
|
11
|
-
|
|
12
|
-
const renderWidget = (header: Partial<CardWidgetHeader> = {}) =>
|
|
13
|
-
render(
|
|
14
|
-
<CardWidget data-testid="widget" header={{ title: 'Cards', ...header }}>
|
|
15
|
-
{content}
|
|
16
|
-
</CardWidget>,
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
describe('rendering', () => {
|
|
20
|
-
it('renders children without a header', () => {
|
|
21
|
-
render(<CardWidget data-testid="widget">{content}</CardWidget>);
|
|
22
|
-
expect(screen.getByText(content)).toBeInTheDocument();
|
|
23
|
-
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
|
24
|
-
expect(screen.getByTestId('widget')).not.toHaveClass('wds-card-widget--with-header');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('renders on a CardContainer', () => {
|
|
28
|
-
render(
|
|
29
|
-
<CardWidget data-testid="widget" spotlight="active" as="article" className="custom">
|
|
30
|
-
{content}
|
|
31
|
-
</CardWidget>,
|
|
32
|
-
);
|
|
33
|
-
const root = screen.getByTestId('widget');
|
|
34
|
-
expect(root.tagName).toBe('ARTICLE');
|
|
35
|
-
expect(root).toHaveClass(
|
|
36
|
-
'wds-card-container',
|
|
37
|
-
'wds-card-container--active',
|
|
38
|
-
'wds-card-widget',
|
|
39
|
-
'custom',
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('always keeps the default padding', () => {
|
|
44
|
-
// `padding` is not a CardWidget prop, but untyped consumers can still pass it.
|
|
45
|
-
const untypedProps = { padding: 'none' } as object;
|
|
46
|
-
render(
|
|
47
|
-
<CardWidget data-testid="widget" {...untypedProps}>
|
|
48
|
-
{content}
|
|
49
|
-
</CardWidget>,
|
|
50
|
-
);
|
|
51
|
-
expect(screen.getByTestId('widget')).not.toHaveClass('wds-card-container--padding-none');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('renders the title as an h3 by default', () => {
|
|
55
|
-
renderWidget();
|
|
56
|
-
expect(screen.getByRole('heading', { level: 3, name: 'Cards' })).toBeInTheDocument();
|
|
57
|
-
expect(screen.getByTestId('widget')).toHaveClass('wds-card-widget--with-header');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('renders the title as the given heading level', () => {
|
|
61
|
-
renderWidget({ as: 'h2' });
|
|
62
|
-
expect(screen.getByRole('heading', { level: 2, name: 'Cards' })).toBeInTheDocument();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('renders the header without content', () => {
|
|
66
|
-
render(<CardWidget header={{ title: 'Cards' }} />);
|
|
67
|
-
expect(screen.getByRole('heading', { name: 'Cards' })).toBeInTheDocument();
|
|
68
|
-
expect(document.querySelector('.wds-card-widget__content')).not.toBeInTheDocument();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('titleAction', () => {
|
|
73
|
-
it('renders the title as a link inside the heading', () => {
|
|
74
|
-
renderWidget({ titleAction: { href: '/cards' } });
|
|
75
|
-
const link = screen.getByRole('link', { name: 'Cards' });
|
|
76
|
-
expect(link).toHaveAttribute('href', '/cards');
|
|
77
|
-
expect(screen.getByRole('heading', { level: 3 })).toContainElement(link);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('calls the titleAction onClick handler', async () => {
|
|
81
|
-
const onClick = jest.fn();
|
|
82
|
-
renderWidget({ titleAction: { href: '#cards', onClick } });
|
|
83
|
-
await userEvent.click(screen.getByRole('link', { name: 'Cards' }));
|
|
84
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('renders the title as a button when titleAction has only onClick', async () => {
|
|
88
|
-
const onClick = jest.fn();
|
|
89
|
-
renderWidget({ titleAction: { onClick } });
|
|
90
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cards' }));
|
|
91
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('marks a title link that opens in a new tab', () => {
|
|
95
|
-
renderWidget({ titleAction: { href: '/cards', target: '_blank' } });
|
|
96
|
-
expect(screen.getByRole('link', { name: /Cards/u })).toHaveAttribute('rel', 'noreferrer');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('keeps the title link alongside actions', () => {
|
|
100
|
-
renderWidget({ titleAction: { href: '/cards' }, action: { label: 'Order card' } });
|
|
101
|
-
expect(screen.getByRole('link', { name: 'Cards' })).toBeInTheDocument();
|
|
102
|
-
expect(screen.getByRole('button', { name: 'Order card' })).toBeInTheDocument();
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('actions', () => {
|
|
107
|
-
it('renders a labelled action as a button', () => {
|
|
108
|
-
renderWidget({ action: { label: 'Order card' } });
|
|
109
|
-
expect(screen.getByRole('button', { name: 'Order card' })).toBeInTheDocument();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('renders a labelled action with an href as a link', () => {
|
|
113
|
-
renderWidget({ action: { label: 'Order card', href: '/cards/order' } });
|
|
114
|
-
expect(screen.getByRole('link', { name: 'Order card' })).toHaveAttribute(
|
|
115
|
-
'href',
|
|
116
|
-
'/cards/order',
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('names an unlabelled action from its aria-label', () => {
|
|
121
|
-
renderWidget({ action: { 'aria-label': 'Card settings', icon: <Settings size={16} /> } });
|
|
122
|
-
expect(screen.getByRole('button', { name: 'Card settings' })).toBeInTheDocument();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('calls the action onClick handler', async () => {
|
|
126
|
-
const onClick = jest.fn();
|
|
127
|
-
renderWidget({ action: { label: 'Order card', onClick } });
|
|
128
|
-
await userEvent.click(screen.getByRole('button', { name: 'Order card' }));
|
|
129
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('does not call onClick when disabled', async () => {
|
|
133
|
-
const onClick = jest.fn();
|
|
134
|
-
renderWidget({ action: { label: 'Order card', disabled: true, onClick } });
|
|
135
|
-
await userEvent.click(screen.getByRole('button', { name: 'Order card' }));
|
|
136
|
-
expect(onClick).not.toHaveBeenCalled();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('renders actionSecondary before action in the DOM', () => {
|
|
140
|
-
renderWidget({ action: { label: 'Order card' }, actionSecondary: { label: 'Manage' } });
|
|
141
|
-
const [first, second] = screen.getAllByRole('button');
|
|
142
|
-
expect(first).toHaveAccessibleName('Manage');
|
|
143
|
-
expect(second).toHaveAccessibleName('Order card');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('takes button priority from the slot the action is passed to', () => {
|
|
147
|
-
renderWidget({ action: { label: 'Order card' }, actionSecondary: { label: 'Manage' } });
|
|
148
|
-
expect(screen.getByRole('button', { name: 'Order card' })).toHaveClass(
|
|
149
|
-
'wds-Button--secondary',
|
|
150
|
-
);
|
|
151
|
-
expect(screen.getByRole('button', { name: 'Manage' })).toHaveClass(
|
|
152
|
-
'wds-Button--secondary-neutral',
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('takes icon button priority from the slot the action is passed to', () => {
|
|
157
|
-
renderWidget({
|
|
158
|
-
action: { 'aria-label': 'Add', icon: <Plus size={16} /> },
|
|
159
|
-
actionSecondary: { 'aria-label': 'Settings', icon: <Settings size={16} /> },
|
|
160
|
-
});
|
|
161
|
-
expect(screen.getByRole('button', { name: 'Add' })).toHaveClass(
|
|
162
|
-
'np-icon-button-secondary-default',
|
|
163
|
-
);
|
|
164
|
-
expect(screen.getByRole('button', { name: 'Settings' })).toHaveClass(
|
|
165
|
-
'np-icon-button-tertiary-default',
|
|
166
|
-
);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('keeps the lower emphasis when actionSecondary is used on its own', () => {
|
|
170
|
-
renderWidget({ actionSecondary: { label: 'Manage' } });
|
|
171
|
-
expect(screen.getByRole('button', { name: 'Manage' })).toHaveClass(
|
|
172
|
-
'wds-Button--secondary-neutral',
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('renders an arbitrary node in place of the action buttons', () => {
|
|
177
|
-
renderWidget({ action: <span>Custom node</span> });
|
|
178
|
-
expect(screen.getByText('Custom node')).toBeInTheDocument();
|
|
179
|
-
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('renders an element as given, even when it has a label prop', () => {
|
|
183
|
-
const CustomAction = ({ label }: { label: string }) => <span>{label}</span>;
|
|
184
|
-
renderWidget({ action: <CustomAction label="Custom action" /> });
|
|
185
|
-
expect(screen.getByText('Custom action')).toBeInTheDocument();
|
|
186
|
-
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { ChevronEnd } from '@transferwise/icons';
|
|
2
|
-
import { clsx } from 'clsx';
|
|
3
|
-
import { forwardRef } from 'react';
|
|
4
|
-
|
|
5
|
-
import CardContainer from '../CardContainer';
|
|
6
|
-
import { Typography } from '../common';
|
|
7
|
-
import Link from '../Link';
|
|
8
|
-
import Title from '../Title';
|
|
9
|
-
import type { CardWidgetHeader, CardWidgetProps } from './CardWidget.types';
|
|
10
|
-
import CardWidgetAction, {
|
|
11
|
-
isCardWidgetActionButton,
|
|
12
|
-
type CardWidgetActionSlot,
|
|
13
|
-
} from './CardWidgetAction';
|
|
14
|
-
|
|
15
|
-
const renderAction = (action: CardWidgetHeader['action'], slot: CardWidgetActionSlot) =>
|
|
16
|
-
isCardWidgetActionButton(action) ? <CardWidgetAction action={action} slot={slot} /> : action;
|
|
17
|
-
|
|
18
|
-
const CardWidgetHeaderComponent = ({
|
|
19
|
-
title,
|
|
20
|
-
as = 'h3',
|
|
21
|
-
titleAction,
|
|
22
|
-
action,
|
|
23
|
-
actionSecondary,
|
|
24
|
-
}: CardWidgetHeader) => (
|
|
25
|
-
<div className="wds-card-widget__header">
|
|
26
|
-
<Title as={as} type={Typography.TITLE_GROUP} className="wds-card-widget__title">
|
|
27
|
-
{titleAction ? (
|
|
28
|
-
<Link
|
|
29
|
-
href={titleAction.href}
|
|
30
|
-
target={titleAction.target}
|
|
31
|
-
className="wds-card-widget__title-link"
|
|
32
|
-
onClick={titleAction.onClick}
|
|
33
|
-
>
|
|
34
|
-
{title}
|
|
35
|
-
{/* The word joiner keeps the chevron on the title's last line instead of wrapping alone. */}
|
|
36
|
-
<span className="wds-card-widget__title-chevron" aria-hidden>
|
|
37
|
-
{'\u2060'}
|
|
38
|
-
<ChevronEnd size={16} />
|
|
39
|
-
</span>
|
|
40
|
-
</Link>
|
|
41
|
-
) : (
|
|
42
|
-
title
|
|
43
|
-
)}
|
|
44
|
-
</Title>
|
|
45
|
-
{(action || actionSecondary) && (
|
|
46
|
-
<div className="wds-card-widget__actions">
|
|
47
|
-
{actionSecondary && renderAction(actionSecondary, 'actionSecondary')}
|
|
48
|
-
{action && renderAction(action, 'action')}
|
|
49
|
-
</div>
|
|
50
|
-
)}
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* A `CardContainer` with an optional header — a title, a title link and up to two actions — above
|
|
56
|
-
* its content.
|
|
57
|
-
*/
|
|
58
|
-
const CardWidget = forwardRef<HTMLElement, CardWidgetProps>(
|
|
59
|
-
({ header, children, className, ...props }, ref) => (
|
|
60
|
-
<CardContainer
|
|
61
|
-
ref={ref}
|
|
62
|
-
className={clsx('wds-card-widget', header && 'wds-card-widget--with-header', className)}
|
|
63
|
-
{...props}
|
|
64
|
-
padding="md"
|
|
65
|
-
>
|
|
66
|
-
{header && <CardWidgetHeaderComponent {...header} />}
|
|
67
|
-
{children != null && <div className="wds-card-widget__content">{children}</div>}
|
|
68
|
-
</CardContainer>
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
CardWidget.displayName = 'CardWidget';
|
|
73
|
-
|
|
74
|
-
export default CardWidget;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
import type { Heading } from '../common';
|
|
4
|
-
import type { CardContainerProps } from '../CardContainer';
|
|
5
|
-
import type { LinkProps } from '../Link';
|
|
6
|
-
import type { CardWidgetActionButton } from './CardWidgetAction';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Props passed to the [Link](https://storybook.wise.design/?path=/docs/typography-link--docs)
|
|
10
|
-
* that the title is rendered as. A trailing chevron is rendered automatically, so do not put one
|
|
11
|
-
* in `title`.
|
|
12
|
-
*/
|
|
13
|
-
export type CardWidgetTitleAction = Pick<LinkProps, 'href' | 'target' | 'onClick'>;
|
|
14
|
-
|
|
15
|
-
export interface CardWidgetHeader {
|
|
16
|
-
/** The widget's title. */
|
|
17
|
-
title: ReactNode;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Heading element rendered for the title.
|
|
21
|
-
* @default 'h3'
|
|
22
|
-
*/
|
|
23
|
-
as?: Heading;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Makes the title a link with a trailing chevron.
|
|
27
|
-
* @default undefined
|
|
28
|
-
*/
|
|
29
|
-
titleAction?: CardWidgetTitleAction;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A `CardWidgetActionButton` object (with a `label` or an `icon`) renders a small button,
|
|
33
|
-
* anchored to the inline end. Any other node is rendered as given.
|
|
34
|
-
* @default undefined
|
|
35
|
-
*/
|
|
36
|
-
action?: CardWidgetActionButton | ReactNode;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Second action, at a lower emphasis, rendered before `action`. Two is the maximum, to keep
|
|
40
|
-
* the title legible. Takes the same values as `action`, and keeps its lower emphasis when used
|
|
41
|
-
* on its own.
|
|
42
|
-
* @default undefined
|
|
43
|
-
*/
|
|
44
|
-
actionSecondary?: CardWidgetActionButton | ReactNode;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The widget always has the card's default padding, so its header and content line up. */
|
|
48
|
-
export interface CardWidgetProps extends Omit<CardContainerProps, 'padding'> {
|
|
49
|
-
/**
|
|
50
|
-
* The widget's header: a title, optionally a link, and up to two actions.
|
|
51
|
-
* @default undefined
|
|
52
|
-
*/
|
|
53
|
-
header?: CardWidgetHeader;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The widget's content, rendered under the header.
|
|
57
|
-
* @default undefined
|
|
58
|
-
*/
|
|
59
|
-
children?: ReactNode;
|
|
60
|
-
}
|