@transferwise/components 46.166.0 → 46.167.0

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 (83) hide show
  1. package/build/CardContainer/CardContainer.js +34 -0
  2. package/build/CardContainer/CardContainer.js.map +1 -0
  3. package/build/CardContainer/CardContainer.mjs +30 -0
  4. package/build/CardContainer/CardContainer.mjs.map +1 -0
  5. package/build/CardWidget/CardWidget.js +98 -0
  6. package/build/CardWidget/CardWidget.js.map +1 -0
  7. package/build/CardWidget/CardWidget.mjs +94 -0
  8. package/build/CardWidget/CardWidget.mjs.map +1 -0
  9. package/build/CardWidget/CardWidgetAction.js +73 -0
  10. package/build/CardWidget/CardWidgetAction.js.map +1 -0
  11. package/build/CardWidget/CardWidgetAction.mjs +68 -0
  12. package/build/CardWidget/CardWidgetAction.mjs.map +1 -0
  13. package/build/ListItem/ListItem.js +2 -0
  14. package/build/ListItem/ListItem.js.map +1 -1
  15. package/build/ListItem/ListItem.mjs +2 -0
  16. package/build/ListItem/ListItem.mjs.map +1 -1
  17. package/build/index.js +4 -0
  18. package/build/index.js.map +1 -1
  19. package/build/index.mjs +2 -0
  20. package/build/index.mjs.map +1 -1
  21. package/build/main.css +194 -0
  22. package/build/styles/CardContainer/CardContainer.css +108 -0
  23. package/build/styles/CardWidget/CardWidget.css +78 -0
  24. package/build/styles/ListItem/ListItem.css +8 -0
  25. package/build/styles/main.css +194 -0
  26. package/build/types/Body/Body.d.ts +2 -2
  27. package/build/types/CardContainer/CardContainer.d.ts +8 -0
  28. package/build/types/CardContainer/CardContainer.d.ts.map +1 -0
  29. package/build/types/CardContainer/CardContainer.types.d.ts +55 -0
  30. package/build/types/CardContainer/CardContainer.types.d.ts.map +1 -0
  31. package/build/types/CardContainer/_stories/helpers.d.ts +3 -0
  32. package/build/types/CardContainer/_stories/helpers.d.ts.map +1 -0
  33. package/build/types/CardContainer/index.d.ts +3 -0
  34. package/build/types/CardContainer/index.d.ts.map +1 -0
  35. package/build/types/CardWidget/CardWidget.d.ts +10 -0
  36. package/build/types/CardWidget/CardWidget.d.ts.map +1 -0
  37. package/build/types/CardWidget/CardWidget.types.d.ts +55 -0
  38. package/build/types/CardWidget/CardWidget.types.d.ts.map +1 -0
  39. package/build/types/CardWidget/CardWidgetAction.d.ts +54 -0
  40. package/build/types/CardWidget/CardWidgetAction.d.ts.map +1 -0
  41. package/build/types/CardWidget/index.d.ts +4 -0
  42. package/build/types/CardWidget/index.d.ts.map +1 -0
  43. package/build/types/IconButton/IconButton.d.ts +1 -1
  44. package/build/types/ListItem/ListItem.d.ts +1 -1
  45. package/build/types/ListItem/ListItem.d.ts.map +1 -1
  46. package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
  47. package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
  48. package/build/types/Title/Title.d.ts +2 -2
  49. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  50. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  51. package/build/types/index.d.ts +4 -0
  52. package/build/types/index.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/CardContainer/CardContainer.css +108 -0
  55. package/src/CardContainer/CardContainer.less +171 -0
  56. package/src/CardContainer/CardContainer.test.tsx +150 -0
  57. package/src/CardContainer/CardContainer.tsx +51 -0
  58. package/src/CardContainer/CardContainer.types.ts +62 -0
  59. package/src/CardContainer/_stories/CardContainer.story.tsx +167 -0
  60. package/src/CardContainer/_stories/CardContainer.test.story.tsx +103 -0
  61. package/src/CardContainer/_stories/helpers.tsx +50 -0
  62. package/src/CardContainer/index.ts +6 -0
  63. package/src/CardWidget/CardWidget.css +78 -0
  64. package/src/CardWidget/CardWidget.less +107 -0
  65. package/src/CardWidget/CardWidget.test.tsx +221 -0
  66. package/src/CardWidget/CardWidget.tsx +78 -0
  67. package/src/CardWidget/CardWidget.types.ts +63 -0
  68. package/src/CardWidget/CardWidgetAction.tsx +128 -0
  69. package/src/CardWidget/_stories/CardWidget.story.tsx +238 -0
  70. package/src/CardWidget/_stories/CardWidget.test.story.tsx +350 -0
  71. package/src/CardWidget/index.ts +3 -0
  72. package/src/ListItem/ListItem.css +8 -0
  73. package/src/ListItem/ListItem.less +8 -0
  74. package/src/ListItem/ListItem.test.tsx +20 -0
  75. package/src/ListItem/ListItem.tsx +3 -1
  76. package/src/ListItem/_stories/ListItem.layout.test.story.tsx +1 -1
  77. package/src/ListItem/_stories/ListItem.story.tsx +1 -1
  78. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.docs.mdx +122 -0
  79. package/src/Prompt/CriticalBanner/_stories/CriticalBanner.story.tsx +0 -2
  80. package/src/index.ts +13 -0
  81. package/src/main.css +194 -0
  82. package/src/main.less +2 -0
  83. package/src/FlowNavigation/_stories/FlowNavigation.CriticalBanner.docs.mdx +0 -66
@@ -0,0 +1,221 @@
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('states', () => {
107
+ it("always follows the title link, instead of the CardContainer's own states", () => {
108
+ // `interactive` is not a CardWidget prop, but untyped consumers can still pass it.
109
+ const untypedProps = { interactive: true } as object;
110
+ render(
111
+ <CardWidget
112
+ data-testid="widget"
113
+ header={{ title: 'Cards', titleAction: { href: '/cards' } }}
114
+ {...untypedProps}
115
+ >
116
+ {content}
117
+ </CardWidget>,
118
+ );
119
+ const root = screen.getByTestId('widget');
120
+ expect(root).not.toHaveClass('wds-card-container--interactive');
121
+ expect(root).not.toHaveAttribute('interactive');
122
+ });
123
+ });
124
+
125
+ describe('actions', () => {
126
+ it('renders a labelled action as a button', () => {
127
+ renderWidget({ action: { label: 'Order card' } });
128
+ expect(screen.getByRole('button', { name: 'Order card' })).toBeInTheDocument();
129
+ });
130
+
131
+ it('renders a labelled action with an href as a link', () => {
132
+ renderWidget({ action: { label: 'Order card', href: '/cards/order' } });
133
+ expect(screen.getByRole('link', { name: 'Order card' })).toHaveAttribute(
134
+ 'href',
135
+ '/cards/order',
136
+ );
137
+ });
138
+
139
+ it('names an unlabelled action from its aria-label', () => {
140
+ renderWidget({ action: { 'aria-label': 'Card settings', icon: <Settings size={16} /> } });
141
+ expect(screen.getByRole('button', { name: 'Card settings' })).toBeInTheDocument();
142
+ });
143
+
144
+ it('calls the action onClick handler', async () => {
145
+ const onClick = jest.fn();
146
+ renderWidget({ action: { label: 'Order card', onClick } });
147
+ await userEvent.click(screen.getByRole('button', { name: 'Order card' }));
148
+ expect(onClick).toHaveBeenCalledTimes(1);
149
+ });
150
+
151
+ it('does not submit a surrounding form', async () => {
152
+ const onSubmit = jest.fn((event: React.FormEvent) => event.preventDefault());
153
+ render(
154
+ <form method="post" onSubmit={onSubmit}>
155
+ <CardWidget header={{ title: 'Cards', action: { label: 'Order card' } }} />
156
+ </form>,
157
+ );
158
+ const button = screen.getByRole('button', { name: 'Order card' });
159
+ expect(button).toHaveAttribute('type', 'button');
160
+ await userEvent.click(button);
161
+ expect(onSubmit).not.toHaveBeenCalled();
162
+ });
163
+
164
+ it('does not call onClick when disabled', async () => {
165
+ const onClick = jest.fn();
166
+ renderWidget({ action: { label: 'Order card', disabled: true, onClick } });
167
+ await userEvent.click(screen.getByRole('button', { name: 'Order card' }));
168
+ expect(onClick).not.toHaveBeenCalled();
169
+ });
170
+
171
+ it('renders actionSecondary before action in the DOM', () => {
172
+ renderWidget({ action: { label: 'Order card' }, actionSecondary: { label: 'Manage' } });
173
+ const [first, second] = screen.getAllByRole('button');
174
+ expect(first).toHaveAccessibleName('Manage');
175
+ expect(second).toHaveAccessibleName('Order card');
176
+ });
177
+
178
+ it('takes button priority from the slot the action is passed to', () => {
179
+ renderWidget({ action: { label: 'Order card' }, actionSecondary: { label: 'Manage' } });
180
+ expect(screen.getByRole('button', { name: 'Order card' })).toHaveClass(
181
+ 'wds-Button--secondary',
182
+ );
183
+ expect(screen.getByRole('button', { name: 'Manage' })).toHaveClass(
184
+ 'wds-Button--secondary-neutral',
185
+ );
186
+ });
187
+
188
+ it('takes icon button priority from the slot the action is passed to', () => {
189
+ renderWidget({
190
+ action: { 'aria-label': 'Add', icon: <Plus size={16} /> },
191
+ actionSecondary: { 'aria-label': 'Settings', icon: <Settings size={16} /> },
192
+ });
193
+ expect(screen.getByRole('button', { name: 'Add' })).toHaveClass(
194
+ 'np-icon-button-secondary-default',
195
+ );
196
+ expect(screen.getByRole('button', { name: 'Settings' })).toHaveClass(
197
+ 'np-icon-button-tertiary-default',
198
+ );
199
+ });
200
+
201
+ it('keeps the lower emphasis when actionSecondary is used on its own', () => {
202
+ renderWidget({ actionSecondary: { label: 'Manage' } });
203
+ expect(screen.getByRole('button', { name: 'Manage' })).toHaveClass(
204
+ 'wds-Button--secondary-neutral',
205
+ );
206
+ });
207
+
208
+ it('renders an arbitrary node in place of the action buttons', () => {
209
+ renderWidget({ action: <span>Custom node</span> });
210
+ expect(screen.getByText('Custom node')).toBeInTheDocument();
211
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
212
+ });
213
+
214
+ it('renders an element as given, even when it has a label prop', () => {
215
+ const CustomAction = ({ label }: { label: string }) => <span>{label}</span>;
216
+ renderWidget({ action: <CustomAction label="Custom action" /> });
217
+ expect(screen.getByText('Custom action')).toBeInTheDocument();
218
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,78 @@
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. The card shows as hovered or pressed while the title link is, and as hovered while
57
+ * the link has keyboard focus. Hovering or pressing the rest of the card, or its actions, leaves it
58
+ * at rest.
59
+ */
60
+ const CardWidget = forwardRef<HTMLElement, CardWidgetProps>(
61
+ ({ header, children, className, ...props }, ref) => (
62
+ <CardContainer
63
+ ref={ref}
64
+ className={clsx('wds-card-widget', header && 'wds-card-widget--with-header', className)}
65
+ {...props}
66
+ // The widget's states follow its title link alone, so the card's own are left off.
67
+ interactive={false}
68
+ padding="md"
69
+ >
70
+ {header && <CardWidgetHeaderComponent {...header} />}
71
+ {children != null && <div className="wds-card-widget__content">{children}</div>}
72
+ </CardContainer>
73
+ ),
74
+ );
75
+
76
+ CardWidget.displayName = 'CardWidget';
77
+
78
+ export default CardWidget;
@@ -0,0 +1,63 @@
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
+ /**
48
+ * The widget always has the card's default padding, so its header and content line up, and its
49
+ * states always follow its title link.
50
+ */
51
+ export interface CardWidgetProps extends Omit<CardContainerProps, 'padding' | 'interactive'> {
52
+ /**
53
+ * The widget's header: a title, optionally a link, and up to two actions.
54
+ * @default undefined
55
+ */
56
+ header?: CardWidgetHeader;
57
+
58
+ /**
59
+ * The widget's content, rendered under the header.
60
+ * @default undefined
61
+ */
62
+ children?: ReactNode;
63
+ }
@@ -0,0 +1,128 @@
1
+ import { isValidElement, type MouseEventHandler, type ReactNode } from 'react';
2
+
3
+ import Button from '../Button';
4
+ import type { ButtonPriority, ButtonProps } from '../Button/Button.types';
5
+ import IconButton, { type IconButtonProps } from '../IconButton';
6
+
7
+ /** The action slot a `CardWidgetActionButton` was passed to, which is what determines its emphasis. */
8
+ export type CardWidgetActionSlot = 'action' | 'actionSecondary';
9
+
10
+ type CardWidgetActionButtonBase = Pick<ButtonProps, 'href' | 'target' | 'disabled'> & {
11
+ /**
12
+ * `HTMLElement` rather than a narrower target, because an action is a `button` or an `a`
13
+ * depending on whether `href` is set.
14
+ * @default undefined
15
+ */
16
+ onClick?: MouseEventHandler<HTMLElement>;
17
+
18
+ /**
19
+ * Rendered after `label`, or as the only content of an unlabelled action.
20
+ * Size it yourself — 16px is expected in both cases.
21
+ * @default undefined
22
+ */
23
+ icon?: ReactNode;
24
+ 'data-testid'?: string;
25
+ };
26
+
27
+ /** An action with a visible label, rendered as a small button. */
28
+ type CardWidgetLabelledActionButton = CardWidgetActionButtonBase & {
29
+ /** Non-rendering values are excluded, so an unlabelled action has to meet the icon-only rules. */
30
+ label: Exclude<ButtonProps['children'], null | undefined | boolean>;
31
+ /** @default undefined */
32
+ 'aria-label'?: string;
33
+ };
34
+
35
+ /**
36
+ * An action with no visible label, rendered as a 32px icon button. The icon is
37
+ * the only thing on screen and `aria-label` is the only accessible name, so
38
+ * both are required.
39
+ */
40
+ type CardWidgetIconActionButton = CardWidgetActionButtonBase & {
41
+ label?: never;
42
+ icon: ReactNode;
43
+ 'aria-label': string;
44
+ };
45
+
46
+ /**
47
+ * An action the `CardWidget` header renders as a button. With a `label` it is a small `Button`; without
48
+ * one, a 32px `IconButton`, which requires both an `icon` and an `aria-label`. Emphasis is not
49
+ * part of the object — it comes from the slot the action is passed to.
50
+ */
51
+ export type CardWidgetActionButton = CardWidgetLabelledActionButton | CardWidgetIconActionButton;
52
+
53
+ const BUTTON_PRIORITY = {
54
+ action: 'secondary',
55
+ actionSecondary: 'secondary-neutral',
56
+ } as const satisfies Record<CardWidgetActionSlot, ButtonPriority>;
57
+
58
+ const ICON_BUTTON_PRIORITY = {
59
+ action: 'secondary',
60
+ actionSecondary: 'tertiary',
61
+ } as const satisfies Record<CardWidgetActionSlot, IconButtonProps['priority']>;
62
+
63
+ /**
64
+ * Tells a `CardWidgetActionButton` object apart from any other node passed to an action slot.
65
+ * Every button action has a `label` or an `icon`, which no React element, portal or array does.
66
+ */
67
+ export const isCardWidgetActionButton = (value: unknown): value is CardWidgetActionButton =>
68
+ typeof value === 'object' &&
69
+ value !== null &&
70
+ !isValidElement(value) &&
71
+ !Array.isArray(value) &&
72
+ ('label' in value || 'icon' in value);
73
+
74
+ /** Renders a `CardWidgetActionButton` at the emphasis of the slot it was passed to. */
75
+ const CardWidgetAction = function CardWidgetAction({
76
+ action: {
77
+ label,
78
+ icon,
79
+ 'aria-label': ariaLabel,
80
+ 'data-testid': dataTestId,
81
+ href,
82
+ target,
83
+ onClick,
84
+ disabled,
85
+ },
86
+ slot,
87
+ }: {
88
+ action: CardWidgetActionButton;
89
+ slot: CardWidgetActionSlot;
90
+ }) {
91
+ if (label === undefined) {
92
+ return (
93
+ <IconButton
94
+ size={32}
95
+ priority={ICON_BUTTON_PRIORITY[slot]}
96
+ aria-label={ariaLabel}
97
+ href={href}
98
+ target={target}
99
+ disabled={disabled}
100
+ data-testid={dataTestId}
101
+ onClick={onClick}
102
+ >
103
+ {icon}
104
+ </IconButton>
105
+ );
106
+ }
107
+
108
+ return (
109
+ <Button
110
+ v2
111
+ size="sm"
112
+ priority={BUTTON_PRIORITY[slot]}
113
+ aria-label={ariaLabel}
114
+ href={href}
115
+ target={target}
116
+ disabled={disabled}
117
+ addonEnd={icon ? { type: 'icon', value: icon } : undefined}
118
+ // The v2 resolver drops `Button`'s own `button` default, so an action would submit a form.
119
+ type="button"
120
+ data-testid={dataTestId}
121
+ onClick={onClick}
122
+ >
123
+ {label}
124
+ </Button>
125
+ );
126
+ };
127
+
128
+ export default CardWidgetAction;