@transferwise/components 46.82.0 → 46.83.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.
- package/build/alert/Alert.js +2 -9
- package/build/alert/Alert.js.map +1 -1
- package/build/alert/Alert.mjs +2 -9
- package/build/alert/Alert.mjs.map +1 -1
- package/build/avatarLayout/AvatarLayout.js +111 -0
- package/build/avatarLayout/AvatarLayout.js.map +1 -0
- package/build/avatarLayout/AvatarLayout.mjs +109 -0
- package/build/avatarLayout/AvatarLayout.mjs.map +1 -0
- package/build/button/Button.js +1 -1
- package/build/button/Button.js.map +1 -1
- package/build/button/Button.mjs +1 -1
- package/build/button/Button.mjs.map +1 -1
- package/build/i18n/commonMessages/Button.messages.js.map +1 -0
- package/build/i18n/commonMessages/Button.messages.mjs.map +1 -0
- package/build/i18n/es.json +5 -0
- package/build/i18n/es.json.js +5 -0
- package/build/i18n/es.json.js.map +1 -1
- package/build/i18n/es.json.mjs +5 -0
- package/build/i18n/es.json.mjs.map +1 -1
- package/build/i18n/pl.json +5 -0
- package/build/i18n/pl.json.js +5 -0
- package/build/i18n/pl.json.js.map +1 -1
- package/build/i18n/pl.json.mjs +5 -0
- package/build/i18n/pl.json.mjs.map +1 -1
- package/build/i18n/ro.json +5 -0
- package/build/i18n/ro.json.js +5 -0
- package/build/i18n/ro.json.js.map +1 -1
- package/build/i18n/ro.json.mjs +5 -0
- package/build/i18n/ro.json.mjs.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/index.mjs +1 -0
- package/build/index.mjs.map +1 -1
- package/build/logo/Logo.js +11 -131
- package/build/logo/Logo.js.map +1 -1
- package/build/logo/Logo.mjs +1 -121
- package/build/logo/Logo.mjs.map +1 -1
- package/build/logo/logo-assets.js +134 -0
- package/build/logo/logo-assets.js.map +1 -0
- package/build/logo/logo-assets.mjs +125 -0
- package/build/logo/logo-assets.mjs.map +1 -0
- package/build/main.css +30 -0
- package/build/styles/avatarLayout/AvatarLayout.css +30 -0
- package/build/styles/main.css +30 -0
- package/build/types/alert/Alert.d.ts +1 -5
- package/build/types/alert/Alert.d.ts.map +1 -1
- package/build/types/avatarLayout/AvatarLayout.d.ts +11 -0
- package/build/types/avatarLayout/AvatarLayout.d.ts.map +1 -0
- package/build/types/avatarLayout/index.d.ts +3 -0
- package/build/types/avatarLayout/index.d.ts.map +1 -0
- package/build/types/i18n/commonMessages/Button.messages.d.ts.map +1 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/logo/Logo.d.ts.map +1 -1
- package/build/types/logo/logo-assets.d.ts +10 -0
- package/build/types/logo/logo-assets.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveAnchor/index.d.ts +3 -0
- package/build/types/primitives/PrimitiveAnchor/index.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveAnchor/src/PrimitiveAnchor.d.ts +14 -0
- package/build/types/primitives/PrimitiveAnchor/src/PrimitiveAnchor.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveAnchor/src/PrimitiveAnchor.types.d.ts +21 -0
- package/build/types/primitives/PrimitiveAnchor/src/PrimitiveAnchor.types.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveAnchor/src/index.d.ts +3 -0
- package/build/types/primitives/PrimitiveAnchor/src/index.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveButton/index.d.ts +3 -0
- package/build/types/primitives/PrimitiveButton/index.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveButton/src/PrimitiveButton.d.ts +14 -0
- package/build/types/primitives/PrimitiveButton/src/PrimitiveButton.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveButton/src/PrimitiveButton.types.d.ts +21 -0
- package/build/types/primitives/PrimitiveButton/src/PrimitiveButton.types.d.ts.map +1 -0
- package/build/types/primitives/PrimitiveButton/src/index.d.ts +3 -0
- package/build/types/primitives/PrimitiveButton/src/index.d.ts.map +1 -0
- package/build/types/primitives/index.d.ts +6 -0
- package/build/types/primitives/index.d.ts.map +1 -0
- package/build/types/primitives/types.d.ts +34 -0
- package/build/types/primitives/types.d.ts.map +1 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/alert/Alert.spec.story.tsx +0 -82
- package/src/alert/Alert.spec.tsx +0 -30
- package/src/alert/Alert.tsx +51 -67
- package/src/avatarLayout/AvatarLayout.css +30 -0
- package/src/avatarLayout/AvatarLayout.less +39 -0
- package/src/avatarLayout/AvatarLayout.story.tsx +277 -0
- package/src/avatarLayout/AvatarLayout.tsx +91 -0
- package/src/avatarLayout/index.ts +2 -0
- package/src/button/Button.spec.tsx +1 -1
- package/src/button/Button.tsx +1 -1
- package/src/i18n/es.json +5 -0
- package/src/i18n/pl.json +5 -0
- package/src/i18n/ro.json +5 -0
- package/src/index.ts +2 -0
- package/src/logo/Logo.tsx +10 -8
- package/src/logo/__snapshots__/Logo.spec.tsx.snap +16 -16
- package/src/logo/logo-assets.tsx +137 -0
- package/src/main.css +30 -0
- package/src/main.less +1 -0
- package/src/primitives/PrimitiveAnchor/index.ts +2 -0
- package/src/primitives/PrimitiveAnchor/src/PrimitiveAnchor.tsx +122 -0
- package/src/primitives/PrimitiveAnchor/src/PrimitiveAnchor.types.ts +28 -0
- package/src/primitives/PrimitiveAnchor/src/index.ts +6 -0
- package/src/primitives/PrimitiveAnchor/stories/PrimitiveAnchor.story.tsx +77 -0
- package/src/primitives/PrimitiveAnchor/stories/PrimitiveAnchor.tests.story.tsx +169 -0
- package/src/primitives/PrimitiveAnchor/test/PrimitiveAnchor.spec.tsx +95 -0
- package/src/primitives/PrimitiveButton/index.ts +2 -0
- package/src/primitives/PrimitiveButton/src/PrimitiveButton.tsx +131 -0
- package/src/primitives/PrimitiveButton/src/PrimitiveButton.types.ts +28 -0
- package/src/primitives/PrimitiveButton/src/index.ts +6 -0
- package/src/primitives/PrimitiveButton/stories/PrimitiveButton.story.tsx +73 -0
- package/src/primitives/PrimitiveButton/stories/PrimitiveButton.tests.story.tsx +230 -0
- package/src/primitives/PrimitiveButton/test/PrimitiveButton.spec.tsx +114 -0
- package/src/primitives/index.ts +14 -0
- package/src/primitives/types.ts +40 -0
- package/src/test-utils/index.tsx +1 -0
- package/build/button/Button.messages.js.map +0 -1
- package/build/button/Button.messages.mjs.map +0 -1
- package/build/logo/svg/flag-inverse.svg +0 -1
- package/build/logo/svg/flag-platform-white.svg +0 -1
- package/build/logo/svg/flag-platform.svg +0 -1
- package/build/logo/svg/flag.svg +0 -1
- package/build/logo/svg/logo-business-inverse.svg +0 -1
- package/build/logo/svg/logo-business.svg +0 -1
- package/build/logo/svg/logo-inverse.svg +0 -1
- package/build/logo/svg/logo-platform-white.svg +0 -1
- package/build/logo/svg/logo-platform.svg +0 -1
- package/build/logo/svg/logo.svg +0 -1
- package/build/types/button/Button.messages.d.ts.map +0 -1
- package/src/logo/svg/flag-inverse.svg +0 -1
- package/src/logo/svg/flag-platform-white.svg +0 -1
- package/src/logo/svg/flag-platform.svg +0 -1
- package/src/logo/svg/flag.svg +0 -1
- package/src/logo/svg/logo-business-inverse.svg +0 -1
- package/src/logo/svg/logo-business.svg +0 -1
- package/src/logo/svg/logo-inverse.svg +0 -1
- package/src/logo/svg/logo-platform-white.svg +0 -1
- package/src/logo/svg/logo-platform.svg +0 -1
- package/src/logo/svg/logo.svg +0 -1
- /package/build/{button → i18n/commonMessages}/Button.messages.js +0 -0
- /package/build/{button → i18n/commonMessages}/Button.messages.mjs +0 -0
- /package/build/types/{button → i18n/commonMessages}/Button.messages.d.ts +0 -0
- /package/src/{button → i18n/commonMessages}/Button.messages.ts +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import type { PrimitiveAnchorProps } from '..';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Primitive `PrimitiveAnchor` component is customisable link element that can be
|
|
7
|
+
* used in various parts of the Wise Design System internally. It supports
|
|
8
|
+
* different states such as disabled and provides event handlers for common
|
|
9
|
+
* interactions like click, focus, blur, mouse enter, mouse leave, and key down.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link PrimitiveAnchor} for further information.
|
|
12
|
+
* @see {@link https://storybook.wise.design/?path=/docs/primitive-anchor--docs|Storybook Wise Design}
|
|
13
|
+
*/
|
|
14
|
+
const PrimitiveAnchor = forwardRef<HTMLAnchorElement, PrimitiveAnchorProps>(
|
|
15
|
+
(
|
|
16
|
+
{
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
href,
|
|
20
|
+
id,
|
|
21
|
+
disabled = false,
|
|
22
|
+
testId,
|
|
23
|
+
onClick,
|
|
24
|
+
onFocus,
|
|
25
|
+
onBlur,
|
|
26
|
+
onMouseEnter,
|
|
27
|
+
onMouseLeave,
|
|
28
|
+
onKeyDown,
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
ref,
|
|
32
|
+
) => {
|
|
33
|
+
const anchorClasses = clsx(
|
|
34
|
+
'wds-Anchor',
|
|
35
|
+
{
|
|
36
|
+
'wds-Anchor--disabled': disabled,
|
|
37
|
+
},
|
|
38
|
+
className,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const handleClick = useCallback(
|
|
42
|
+
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
43
|
+
if (disabled) {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
} else {
|
|
46
|
+
onClick?.(event);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[disabled, onClick],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const handleFocus = useCallback(
|
|
53
|
+
(event: React.FocusEvent<HTMLAnchorElement>) => {
|
|
54
|
+
onFocus?.(event);
|
|
55
|
+
},
|
|
56
|
+
[onFocus],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const handleBlur = useCallback(
|
|
60
|
+
(event: React.FocusEvent<HTMLAnchorElement>) => {
|
|
61
|
+
onBlur?.(event);
|
|
62
|
+
},
|
|
63
|
+
[onBlur],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const handleMouseEnter = useCallback(
|
|
67
|
+
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
68
|
+
onMouseEnter?.(event);
|
|
69
|
+
},
|
|
70
|
+
[onMouseEnter],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const handleMouseLeave = useCallback(
|
|
74
|
+
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
|
75
|
+
onMouseLeave?.(event);
|
|
76
|
+
},
|
|
77
|
+
[onMouseLeave],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const handleKeyDown = useCallback(
|
|
81
|
+
(event: React.KeyboardEvent<HTMLAnchorElement>) => {
|
|
82
|
+
onKeyDown?.(event);
|
|
83
|
+
},
|
|
84
|
+
[onKeyDown],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The following props are set to handle the `disabled` state for the link:
|
|
89
|
+
*
|
|
90
|
+
* - `aria-disabled`: Exposes the link as disabled to assistive technologies.
|
|
91
|
+
* - `href`: Removed when `disabled` is true to prevent navigation.
|
|
92
|
+
* - `role`: Set to 'link' when `disabled` is true to ensure the element
|
|
93
|
+
* is still exposed as a link.
|
|
94
|
+
*
|
|
95
|
+
* For more details, refer to Scott O'Hara's article on disabling links:
|
|
96
|
+
* https://www.scottohara.me/blog/2021/05/28/disabled-links.html
|
|
97
|
+
*/
|
|
98
|
+
const anchorProps = {
|
|
99
|
+
'aria-disabled': disabled,
|
|
100
|
+
className: anchorClasses,
|
|
101
|
+
'data-testid': testId,
|
|
102
|
+
href: disabled ? undefined : href,
|
|
103
|
+
id,
|
|
104
|
+
ref,
|
|
105
|
+
role: disabled ? 'link' : undefined,
|
|
106
|
+
rel: props.target === '_blank' ? 'noopener noreferrer' : undefined,
|
|
107
|
+
onClick: handleClick,
|
|
108
|
+
onFocus: handleFocus,
|
|
109
|
+
onBlur: handleBlur,
|
|
110
|
+
onMouseEnter: handleMouseEnter,
|
|
111
|
+
onMouseLeave: handleMouseLeave,
|
|
112
|
+
onKeyDown: handleKeyDown,
|
|
113
|
+
...props,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return <a {...anchorProps}>{children}</a>;
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
PrimitiveAnchor.displayName = 'PrimitiveAnchor';
|
|
121
|
+
|
|
122
|
+
export default PrimitiveAnchor;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { BasePrimitiveProps, StyleProp } from '../..';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type aliases for an anchor component.
|
|
6
|
+
*/
|
|
7
|
+
export type PrimitiveAnchorAttributes = React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
8
|
+
export type PrimitiveAnchorElementRef = React.Ref<HTMLAnchorElement>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Properties for the anchor component.
|
|
12
|
+
*/
|
|
13
|
+
export interface PrimitiveAnchorProps
|
|
14
|
+
extends BasePrimitiveProps,
|
|
15
|
+
StyleProp,
|
|
16
|
+
Omit<PrimitiveAnchorAttributes, 'role'> {
|
|
17
|
+
/** Content of the anchor */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
|
|
20
|
+
/** The URL to navigate to when the anchor is clicked */
|
|
21
|
+
href: string;
|
|
22
|
+
|
|
23
|
+
/** Disable the anchor */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
|
|
26
|
+
/** Reference to the anchor element */
|
|
27
|
+
ref?: PrimitiveAnchorElementRef;
|
|
28
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
import PrimitiveAnchor from '..';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
component: PrimitiveAnchor,
|
|
7
|
+
title: 'Primitives/Anchor',
|
|
8
|
+
args: {
|
|
9
|
+
children: 'Click me',
|
|
10
|
+
href: 'https://example.com',
|
|
11
|
+
onClick: fn(),
|
|
12
|
+
onBlur: fn(),
|
|
13
|
+
onFocus: fn(),
|
|
14
|
+
onKeyDown: fn(),
|
|
15
|
+
onMouseEnter: fn(),
|
|
16
|
+
onMouseLeave: fn(),
|
|
17
|
+
},
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
} satisfies Meta<typeof PrimitiveAnchor>;
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof PrimitiveAnchor>;
|
|
22
|
+
|
|
23
|
+
export const Basic: Story = {};
|
|
24
|
+
|
|
25
|
+
export const WithLongName: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
children: 'Anchor with a really long name',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const WithCustomClass: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
className: 'custom-class',
|
|
34
|
+
},
|
|
35
|
+
decorators: [
|
|
36
|
+
(Story) => (
|
|
37
|
+
<div>
|
|
38
|
+
<style>
|
|
39
|
+
{`
|
|
40
|
+
.custom-class {
|
|
41
|
+
background-color: red;
|
|
42
|
+
color: white;
|
|
43
|
+
border: 2px solid black;
|
|
44
|
+
padding: 10px 20px;
|
|
45
|
+
font-size: 16px;
|
|
46
|
+
}
|
|
47
|
+
`}
|
|
48
|
+
</style>
|
|
49
|
+
<Story />
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const WithTargetBlank: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
target: '_blank',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithRelNoopener: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
rel: 'noopener noreferrer',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithCustomStyle: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
style: { color: 'red' },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const Disabled: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
disabled: true,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { userEvent, within, expect } from '@storybook/test';
|
|
3
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import PrimitiveAnchor from '..';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Primitives/Anchor/Tests',
|
|
8
|
+
component: PrimitiveAnchor,
|
|
9
|
+
args: {
|
|
10
|
+
children: 'Click me',
|
|
11
|
+
href: 'https://example.com',
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof PrimitiveAnchor>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
const wait = async (duration = 500) =>
|
|
20
|
+
new Promise<void>((resolve) => {
|
|
21
|
+
setTimeout(resolve, duration);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const ClickInteraction: Story = {
|
|
25
|
+
play: async ({ canvasElement }) => {
|
|
26
|
+
const canvas = within(canvasElement);
|
|
27
|
+
const link = canvas.getByRole('link');
|
|
28
|
+
await userEvent.click(link);
|
|
29
|
+
await expect(link).toHaveTextContent('Clicked!');
|
|
30
|
+
},
|
|
31
|
+
render: function Render(args) {
|
|
32
|
+
const [clicked, setClicked] = useState(false);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<PrimitiveAnchor
|
|
36
|
+
{...args}
|
|
37
|
+
onClick={(event) => {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
setClicked(true);
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{clicked ? 'Clicked!' : 'Click me'}
|
|
43
|
+
</PrimitiveAnchor>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const FocusInteraction: Story = {
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
const canvas = within(canvasElement);
|
|
51
|
+
const link = canvas.getByRole('link');
|
|
52
|
+
await userEvent.tab();
|
|
53
|
+
await expect(link).toHaveFocus();
|
|
54
|
+
await expect(link).toHaveTextContent('Focused!');
|
|
55
|
+
},
|
|
56
|
+
render: function Render(args) {
|
|
57
|
+
const [focused, setFocused] = useState(false);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<PrimitiveAnchor {...args} onFocus={() => setFocused(true)}>
|
|
61
|
+
{focused ? 'Focused!' : 'Click me'}
|
|
62
|
+
</PrimitiveAnchor>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const BlurInteraction: Story = {
|
|
68
|
+
play: async ({ canvasElement }) => {
|
|
69
|
+
const canvas = within(canvasElement);
|
|
70
|
+
const link = canvas.getByRole('link');
|
|
71
|
+
await userEvent.tab();
|
|
72
|
+
await expect(link).toHaveFocus();
|
|
73
|
+
await userEvent.tab();
|
|
74
|
+
await expect(link).not.toHaveFocus();
|
|
75
|
+
await expect(link).toHaveTextContent('Blurred!');
|
|
76
|
+
},
|
|
77
|
+
render: function Render(args) {
|
|
78
|
+
const [blurred, setBlurred] = useState(false);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<PrimitiveAnchor {...args} onBlur={() => setBlurred(true)}>
|
|
82
|
+
{blurred ? 'Blurred!' : 'Click me'}
|
|
83
|
+
</PrimitiveAnchor>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const MouseEnterInteraction: Story = {
|
|
89
|
+
play: async ({ canvasElement }) => {
|
|
90
|
+
const canvas = within(canvasElement);
|
|
91
|
+
const link = canvas.getByRole('link');
|
|
92
|
+
await expect(link).toHaveTextContent('Click me');
|
|
93
|
+
await userEvent.hover(link);
|
|
94
|
+
await expect(link).toHaveTextContent('Hovered!');
|
|
95
|
+
},
|
|
96
|
+
render: function Render(args) {
|
|
97
|
+
const [hovered, setHovered] = useState(false);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<PrimitiveAnchor {...args} onMouseEnter={() => setHovered(true)}>
|
|
101
|
+
{hovered ? 'Hovered!' : 'Click me'}
|
|
102
|
+
</PrimitiveAnchor>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const MouseLeaveInteraction: Story = {
|
|
108
|
+
play: async ({ canvasElement }) => {
|
|
109
|
+
const canvas = within(canvasElement);
|
|
110
|
+
const link = canvas.getByRole('link');
|
|
111
|
+
await expect(link).toHaveTextContent('Click me');
|
|
112
|
+
await userEvent.unhover(link);
|
|
113
|
+
await expect(link).toHaveTextContent('Left!');
|
|
114
|
+
},
|
|
115
|
+
render: function Render(args) {
|
|
116
|
+
const [left, setLeft] = useState(false);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<PrimitiveAnchor {...args} onMouseLeave={() => setLeft(true)}>
|
|
120
|
+
{left ? 'Left!' : 'Click me'}
|
|
121
|
+
</PrimitiveAnchor>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const KeyDownInteraction: Story = {
|
|
127
|
+
play: async ({ canvasElement }) => {
|
|
128
|
+
const canvas = within(canvasElement);
|
|
129
|
+
const link = canvas.getByRole('link');
|
|
130
|
+
await expect(link).toHaveTextContent('Click me');
|
|
131
|
+
await userEvent.tab();
|
|
132
|
+
await wait();
|
|
133
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
134
|
+
await expect(link).toHaveTextContent('Key Pressed!');
|
|
135
|
+
},
|
|
136
|
+
render: function Render(args) {
|
|
137
|
+
const [keyPressed, setKeyPressed] = useState(false);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<PrimitiveAnchor
|
|
141
|
+
{...args}
|
|
142
|
+
onKeyDown={(event) => {
|
|
143
|
+
if (event.key === 'ArrowDown') {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
setKeyPressed(true);
|
|
146
|
+
}
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{keyPressed ? 'Key Pressed!' : 'Click me'}
|
|
150
|
+
</PrimitiveAnchor>
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const TabIndexBehavior: Story = {
|
|
156
|
+
play: async ({ canvasElement }) => {
|
|
157
|
+
const canvas = within(canvasElement);
|
|
158
|
+
const link = canvas.getByRole('link');
|
|
159
|
+
await userEvent.tab();
|
|
160
|
+
await expect(link).not.toHaveFocus();
|
|
161
|
+
},
|
|
162
|
+
render: function Render(args) {
|
|
163
|
+
return (
|
|
164
|
+
<PrimitiveAnchor {...args} disabled tabIndex={-1}>
|
|
165
|
+
Click me
|
|
166
|
+
</PrimitiveAnchor>
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import PrimitiveAnchor from '..';
|
|
3
|
+
|
|
4
|
+
describe('PrimitiveAnchor', () => {
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
children: 'Click me',
|
|
7
|
+
href: 'https://example.com',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const renderAnchor = (props?: Partial<typeof defaultProps>) => {
|
|
11
|
+
return render(<PrimitiveAnchor {...defaultProps} {...props} />);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
it('renders an anchor by default', () => {
|
|
15
|
+
renderAnchor();
|
|
16
|
+
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByRole('link')).toHaveTextContent('Click me');
|
|
18
|
+
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://example.com');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('applies the correct classes based on props', () => {
|
|
22
|
+
const props = {
|
|
23
|
+
...defaultProps,
|
|
24
|
+
className: 'custom-class',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
renderAnchor(props);
|
|
28
|
+
|
|
29
|
+
const link = screen.getByRole('link');
|
|
30
|
+
expect(link).toHaveClass('wds-Anchor');
|
|
31
|
+
expect(link).toHaveClass('custom-class');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('disables the anchor when isDisabled is true', () => {
|
|
35
|
+
const props = {
|
|
36
|
+
...defaultProps,
|
|
37
|
+
disabled: true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
renderAnchor(props);
|
|
41
|
+
|
|
42
|
+
const link = screen.getByRole('link');
|
|
43
|
+
expect(link).toHaveAttribute('aria-disabled', 'true');
|
|
44
|
+
expect(link).toHaveClass('wds-Anchor--disabled');
|
|
45
|
+
expect(link).not.toHaveAttribute('href', 'https://example.com');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('sets data-testid attribute', () => {
|
|
49
|
+
const props = {
|
|
50
|
+
...defaultProps,
|
|
51
|
+
testId: 'custom-id',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
renderAnchor(props);
|
|
55
|
+
|
|
56
|
+
const link = screen.getByTestId('custom-id');
|
|
57
|
+
expect(link).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('sets the target attribute to _blank', () => {
|
|
61
|
+
const props = {
|
|
62
|
+
...defaultProps,
|
|
63
|
+
target: '_blank',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
renderAnchor(props);
|
|
67
|
+
|
|
68
|
+
const link = screen.getByRole('link');
|
|
69
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('sets the rel attribute to noopener noreferrer', () => {
|
|
73
|
+
const props = {
|
|
74
|
+
...defaultProps,
|
|
75
|
+
rel: 'noopener noreferrer',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
renderAnchor(props);
|
|
79
|
+
|
|
80
|
+
const link = screen.getByRole('link');
|
|
81
|
+
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('applies custom styles', () => {
|
|
85
|
+
const props = {
|
|
86
|
+
...defaultProps,
|
|
87
|
+
style: { color: 'red' },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
renderAnchor(props);
|
|
91
|
+
|
|
92
|
+
const link = screen.getByRole('link');
|
|
93
|
+
expect(link).toHaveStyle('color: red');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* eslint-disable react/button-has-type */
|
|
2
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { useIntl, MessageDescriptor } from 'react-intl';
|
|
5
|
+
import messages from '../../../i18n/commonMessages/Button.messages';
|
|
6
|
+
import type { PrimitiveButtonProps } from '..';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Primitive `PrimitiveButton` component can be used in various parts the Wise Design
|
|
10
|
+
* System internally. It supports different states such as disabled and loading,
|
|
11
|
+
* and provides event handlers for common interactions like click, focus, blur,
|
|
12
|
+
* mouse enter, mouse leave, and keydown.
|
|
13
|
+
*
|
|
14
|
+
* @see {@link PrimitiveButton} for further information.
|
|
15
|
+
* @see {@link https://storybook.wise.design/?path=/docs/primitive-button--docs|Storybook Wise Design}
|
|
16
|
+
*/
|
|
17
|
+
const PrimitiveButton = forwardRef<HTMLButtonElement, PrimitiveButtonProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
children,
|
|
21
|
+
className,
|
|
22
|
+
id,
|
|
23
|
+
disabled = false,
|
|
24
|
+
loading = false,
|
|
25
|
+
testId,
|
|
26
|
+
type = 'button',
|
|
27
|
+
onClick,
|
|
28
|
+
onFocus,
|
|
29
|
+
onBlur,
|
|
30
|
+
onMouseEnter,
|
|
31
|
+
onMouseLeave,
|
|
32
|
+
onKeyDown,
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref,
|
|
36
|
+
) => {
|
|
37
|
+
const intl = useIntl();
|
|
38
|
+
const classNames = clsx(
|
|
39
|
+
'wds-Button',
|
|
40
|
+
{
|
|
41
|
+
'wds-Button--disabled': disabled,
|
|
42
|
+
'wds-Button--loading': loading,
|
|
43
|
+
},
|
|
44
|
+
className,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const handleClick = useCallback(
|
|
48
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
49
|
+
if (disabled || loading) {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
} else {
|
|
52
|
+
onClick?.(event);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
[disabled, loading, onClick],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const handleFocus = useCallback(
|
|
59
|
+
(event: React.FocusEvent<HTMLButtonElement>) => {
|
|
60
|
+
onFocus?.(event);
|
|
61
|
+
},
|
|
62
|
+
[onFocus],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleBlur = useCallback(
|
|
66
|
+
(event: React.FocusEvent<HTMLButtonElement>) => {
|
|
67
|
+
onBlur?.(event);
|
|
68
|
+
},
|
|
69
|
+
[onBlur],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const handleMouseEnter = useCallback(
|
|
73
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
74
|
+
onMouseEnter?.(event);
|
|
75
|
+
},
|
|
76
|
+
[onMouseEnter],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const handleMouseLeave = useCallback(
|
|
80
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
81
|
+
onMouseLeave?.(event);
|
|
82
|
+
},
|
|
83
|
+
[onMouseLeave],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const handleKeyDown = useCallback(
|
|
87
|
+
(event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
88
|
+
onKeyDown?.(event);
|
|
89
|
+
},
|
|
90
|
+
[onKeyDown],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The following props are used to manage the `disabled` and `loading`
|
|
95
|
+
* states for the button:
|
|
96
|
+
*
|
|
97
|
+
* - `aria-disabled`: Communicates to assistive technologies that the button
|
|
98
|
+
* is disabled when it is either disabled or loading.
|
|
99
|
+
* - `aria-label`: Provides an accessible label for the button, using a
|
|
100
|
+
* localized loading message when the button is in a loading state.
|
|
101
|
+
* - `aria-live`: Ensures that updates to the button's content are announced
|
|
102
|
+
* by assistive technologies, set to 'polite' during loading.
|
|
103
|
+
*/
|
|
104
|
+
const buttonProps = {
|
|
105
|
+
'aria-disabled': disabled || loading,
|
|
106
|
+
'aria-label': loading
|
|
107
|
+
? intl.formatMessage(messages.loadingAriaLabel as MessageDescriptor)
|
|
108
|
+
: props['aria-label'],
|
|
109
|
+
'aria-live': (loading ? 'polite' : 'off') as 'polite' | 'off' | 'assertive' | undefined,
|
|
110
|
+
className: classNames,
|
|
111
|
+
'data-testid': testId,
|
|
112
|
+
disabled,
|
|
113
|
+
id,
|
|
114
|
+
ref,
|
|
115
|
+
type,
|
|
116
|
+
onBlur: handleBlur,
|
|
117
|
+
onClick: handleClick,
|
|
118
|
+
onFocus: handleFocus,
|
|
119
|
+
onMouseEnter: handleMouseEnter,
|
|
120
|
+
onMouseLeave: handleMouseLeave,
|
|
121
|
+
onKeyDown: handleKeyDown,
|
|
122
|
+
...props,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return <button {...buttonProps}>{children}</button>;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
PrimitiveButton.displayName = 'PrimitiveButton';
|
|
130
|
+
|
|
131
|
+
export default PrimitiveButton;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { BasePrimitiveProps, StyleProp } from '../..';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type aliases for a button component.
|
|
6
|
+
*/
|
|
7
|
+
export type PrimitiveButtonAttributes = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'role'>;
|
|
8
|
+
export type PrimitiveButtonElementRef = React.Ref<HTMLButtonElement>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Properties for the Button component.
|
|
12
|
+
*/
|
|
13
|
+
export interface PrimitiveButtonProps
|
|
14
|
+
extends BasePrimitiveProps,
|
|
15
|
+
StyleProp,
|
|
16
|
+
PrimitiveButtonAttributes {
|
|
17
|
+
/** Content of the button */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
|
|
20
|
+
/** Disable the button */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
|
|
23
|
+
/** Loading state of the button */
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
|
|
26
|
+
/** Reference to the button element */
|
|
27
|
+
ref?: PrimitiveButtonElementRef;
|
|
28
|
+
}
|