@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.
Files changed (147) hide show
  1. package/build/Snackbar/Snackbar.js +178 -102
  2. package/build/Snackbar/Snackbar.js.map +1 -1
  3. package/build/Snackbar/Snackbar.messages.js +14 -0
  4. package/build/Snackbar/Snackbar.messages.js.map +1 -0
  5. package/build/Snackbar/Snackbar.messages.mjs +10 -0
  6. package/build/Snackbar/Snackbar.messages.mjs.map +1 -0
  7. package/build/Snackbar/Snackbar.mjs +180 -101
  8. package/build/Snackbar/Snackbar.mjs.map +1 -1
  9. package/build/Snackbar/SnackbarContext.js.map +1 -1
  10. package/build/Snackbar/SnackbarContext.mjs.map +1 -1
  11. package/build/Snackbar/SnackbarPortal.js +12 -0
  12. package/build/Snackbar/SnackbarPortal.js.map +1 -0
  13. package/build/Snackbar/SnackbarPortal.mjs +8 -0
  14. package/build/Snackbar/SnackbarPortal.mjs.map +1 -0
  15. package/build/Snackbar/SnackbarProvider.js +24 -28
  16. package/build/Snackbar/SnackbarProvider.js.map +1 -1
  17. package/build/Snackbar/SnackbarProvider.mjs +25 -29
  18. package/build/Snackbar/SnackbarProvider.mjs.map +1 -1
  19. package/build/Snackbar/constants.js +17 -0
  20. package/build/Snackbar/constants.js.map +1 -0
  21. package/build/Snackbar/constants.mjs +11 -0
  22. package/build/Snackbar/constants.mjs.map +1 -0
  23. package/build/Snackbar/useSnackbarAutoDismiss.js +66 -0
  24. package/build/Snackbar/useSnackbarAutoDismiss.js.map +1 -0
  25. package/build/Snackbar/useSnackbarAutoDismiss.mjs +62 -0
  26. package/build/Snackbar/useSnackbarAutoDismiss.mjs.map +1 -0
  27. package/build/Snackbar/useSnackbarManager.js +153 -0
  28. package/build/Snackbar/useSnackbarManager.js.map +1 -0
  29. package/build/Snackbar/useSnackbarManager.mjs +149 -0
  30. package/build/Snackbar/useSnackbarManager.mjs.map +1 -0
  31. package/build/Snackbar/useSnackbarSwipeDismiss.js +135 -0
  32. package/build/Snackbar/useSnackbarSwipeDismiss.js.map +1 -0
  33. package/build/Snackbar/useSnackbarSwipeDismiss.mjs +131 -0
  34. package/build/Snackbar/useSnackbarSwipeDismiss.mjs.map +1 -0
  35. package/build/i18n/en.json +1 -0
  36. package/build/i18n/en.json.js +1 -0
  37. package/build/i18n/en.json.js.map +1 -1
  38. package/build/i18n/en.json.mjs +1 -0
  39. package/build/i18n/en.json.mjs.map +1 -1
  40. package/build/index.js +0 -4
  41. package/build/index.js.map +1 -1
  42. package/build/index.mjs +0 -2
  43. package/build/index.mjs.map +1 -1
  44. package/build/main.css +116 -191
  45. package/build/styles/Snackbar/Snackbar.css +117 -54
  46. package/build/styles/main.css +116 -191
  47. package/build/types/Snackbar/Snackbar.d.ts +55 -9
  48. package/build/types/Snackbar/Snackbar.d.ts.map +1 -1
  49. package/build/types/Snackbar/Snackbar.messages.d.ts +9 -0
  50. package/build/types/Snackbar/Snackbar.messages.d.ts.map +1 -0
  51. package/build/types/Snackbar/Snackbar.types.d.ts +12 -0
  52. package/build/types/Snackbar/Snackbar.types.d.ts.map +1 -0
  53. package/build/types/Snackbar/SnackbarContext.d.ts +2 -2
  54. package/build/types/Snackbar/SnackbarContext.d.ts.map +1 -1
  55. package/build/types/Snackbar/SnackbarPortal.d.ts +4 -0
  56. package/build/types/Snackbar/SnackbarPortal.d.ts.map +1 -0
  57. package/build/types/Snackbar/SnackbarProvider.d.ts +7 -0
  58. package/build/types/Snackbar/SnackbarProvider.d.ts.map +1 -1
  59. package/build/types/Snackbar/constants.d.ts +9 -0
  60. package/build/types/Snackbar/constants.d.ts.map +1 -0
  61. package/build/types/Snackbar/index.d.ts +1 -1
  62. package/build/types/Snackbar/index.d.ts.map +1 -1
  63. package/build/types/Snackbar/useSnackbar.d.ts +1 -1
  64. package/build/types/Snackbar/useSnackbar.d.ts.map +1 -1
  65. package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts +13 -0
  66. package/build/types/Snackbar/useSnackbarAutoDismiss.d.ts.map +1 -0
  67. package/build/types/Snackbar/useSnackbarManager.d.ts +20 -0
  68. package/build/types/Snackbar/useSnackbarManager.d.ts.map +1 -0
  69. package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts +22 -0
  70. package/build/types/Snackbar/useSnackbarSwipeDismiss.d.ts.map +1 -0
  71. package/build/types/index.d.ts +1 -5
  72. package/build/types/index.d.ts.map +1 -1
  73. package/build/types/test-utils/index.d.ts +2 -0
  74. package/build/types/test-utils/index.d.ts.map +1 -1
  75. package/package.json +3 -3
  76. package/src/Snackbar/Snackbar.css +117 -54
  77. package/src/Snackbar/Snackbar.less +139 -60
  78. package/src/Snackbar/Snackbar.messages.ts +9 -0
  79. package/src/Snackbar/Snackbar.test.tsx +989 -119
  80. package/src/Snackbar/Snackbar.tsx +208 -93
  81. package/src/Snackbar/Snackbar.types.ts +17 -0
  82. package/src/Snackbar/SnackbarContext.ts +2 -2
  83. package/src/Snackbar/SnackbarPortal.tsx +8 -0
  84. package/src/Snackbar/SnackbarProvider.test.tsx +783 -0
  85. package/src/Snackbar/SnackbarProvider.tsx +27 -19
  86. package/src/Snackbar/_stories/Snackbar.accessibility.docs.mdx +66 -0
  87. package/src/Snackbar/_stories/Snackbar.migration.docs.mdx +64 -0
  88. package/src/Snackbar/_stories/Snackbar.rtl.test.story.tsx +16 -8
  89. package/src/Snackbar/_stories/Snackbar.story.tsx +389 -61
  90. package/src/Snackbar/_stories/Snackbar.test.story.tsx +171 -47
  91. package/src/Snackbar/constants.ts +10 -0
  92. package/src/Snackbar/index.ts +1 -1
  93. package/src/Snackbar/useSnackbarAutoDismiss.ts +74 -0
  94. package/src/Snackbar/useSnackbarManager.ts +169 -0
  95. package/src/Snackbar/useSnackbarSwipeDismiss.ts +169 -0
  96. package/src/i18n/en.json +1 -0
  97. package/src/index.ts +1 -14
  98. package/src/main.css +116 -191
  99. package/src/main.less +0 -2
  100. package/build/CardContainer/CardContainer.js +0 -33
  101. package/build/CardContainer/CardContainer.js.map +0 -1
  102. package/build/CardContainer/CardContainer.mjs +0 -29
  103. package/build/CardContainer/CardContainer.mjs.map +0 -1
  104. package/build/CardWidget/CardWidget.js +0 -94
  105. package/build/CardWidget/CardWidget.js.map +0 -1
  106. package/build/CardWidget/CardWidget.mjs +0 -90
  107. package/build/CardWidget/CardWidget.mjs.map +0 -1
  108. package/build/CardWidget/CardWidgetAction.js +0 -70
  109. package/build/CardWidget/CardWidgetAction.js.map +0 -1
  110. package/build/CardWidget/CardWidgetAction.mjs +0 -65
  111. package/build/CardWidget/CardWidgetAction.mjs.map +0 -1
  112. package/build/styles/CardContainer/CardContainer.css +0 -74
  113. package/build/styles/CardWidget/CardWidget.css +0 -64
  114. package/build/types/CardContainer/CardContainer.d.ts +0 -8
  115. package/build/types/CardContainer/CardContainer.d.ts.map +0 -1
  116. package/build/types/CardContainer/CardContainer.types.d.ts +0 -44
  117. package/build/types/CardContainer/CardContainer.types.d.ts.map +0 -1
  118. package/build/types/CardContainer/_stories/helpers.d.ts +0 -3
  119. package/build/types/CardContainer/_stories/helpers.d.ts.map +0 -1
  120. package/build/types/CardContainer/index.d.ts +0 -3
  121. package/build/types/CardContainer/index.d.ts.map +0 -1
  122. package/build/types/CardWidget/CardWidget.d.ts +0 -8
  123. package/build/types/CardWidget/CardWidget.d.ts.map +0 -1
  124. package/build/types/CardWidget/CardWidget.types.d.ts +0 -52
  125. package/build/types/CardWidget/CardWidget.types.d.ts.map +0 -1
  126. package/build/types/CardWidget/CardWidgetAction.d.ts +0 -54
  127. package/build/types/CardWidget/CardWidgetAction.d.ts.map +0 -1
  128. package/build/types/CardWidget/index.d.ts +0 -4
  129. package/build/types/CardWidget/index.d.ts.map +0 -1
  130. package/src/CardContainer/CardContainer.css +0 -74
  131. package/src/CardContainer/CardContainer.less +0 -112
  132. package/src/CardContainer/CardContainer.test.tsx +0 -118
  133. package/src/CardContainer/CardContainer.tsx +0 -39
  134. package/src/CardContainer/CardContainer.types.ts +0 -50
  135. package/src/CardContainer/_stories/CardContainer.story.tsx +0 -136
  136. package/src/CardContainer/_stories/CardContainer.test.story.tsx +0 -103
  137. package/src/CardContainer/_stories/helpers.tsx +0 -50
  138. package/src/CardContainer/index.ts +0 -6
  139. package/src/CardWidget/CardWidget.css +0 -64
  140. package/src/CardWidget/CardWidget.less +0 -82
  141. package/src/CardWidget/CardWidget.test.tsx +0 -189
  142. package/src/CardWidget/CardWidget.tsx +0 -74
  143. package/src/CardWidget/CardWidget.types.ts +0 -60
  144. package/src/CardWidget/CardWidgetAction.tsx +0 -126
  145. package/src/CardWidget/_stories/CardWidget.story.tsx +0 -209
  146. package/src/CardWidget/_stories/CardWidget.test.story.tsx +0 -350
  147. 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,6 +0,0 @@
1
- export { default } from './CardContainer';
2
- export type {
3
- CardContainerProps,
4
- CardContainerSpotlight,
5
- CardContainerPadding,
6
- } from './CardContainer.types';
@@ -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
- }