@purpurds/tooltip 3.0.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/.eslintrc.js +15 -0
- package/.lintstagedrc.js +1 -0
- package/.rush/temp/ci_build/@purpurds/tooltip/0a8cee774fbae4db60999e576bef8a203e89bcfa.untar.log +10 -0
- package/.rush/temp/operation/ci_build/all.log +32 -0
- package/.rush/temp/operation/ci_build/state.json +3 -0
- package/.rush/temp/operation/test_unit/all.log +11 -0
- package/.rush/temp/operation/test_unit/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +1140 -0
- package/config/rush-project.json +24 -0
- package/dist/LICENSE.txt +398 -0
- package/dist/styles.css +1 -0
- package/dist/tooltip.cjs.js +22 -0
- package/dist/tooltip.cjs.js.map +1 -0
- package/dist/tooltip.d.ts +31 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.es.js +2825 -0
- package/dist/tooltip.es.js.map +1 -0
- package/dist/tooltip.system.js +22 -0
- package/dist/tooltip.system.js.map +1 -0
- package/package.json +63 -0
- package/readme.mdx +106 -0
- package/src/global.d.ts +4 -0
- package/src/tooltip.module.scss +94 -0
- package/src/tooltip.stories.tsx +95 -0
- package/src/tooltip.test.tsx +126 -0
- package/src/tooltip.tsx +140 -0
- package/tsconfig.json +7 -0
- package/tsconfig.types.json +9 -0
- package/vite.config.mts +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@purpurds/tooltip",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"main": "./dist/tooltip.cjs.js",
|
|
5
|
+
"types": "./dist/tooltip.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"require": "./dist/tooltip.cjs.js",
|
|
9
|
+
"systemjs": "./dist/tooltip.system.js",
|
|
10
|
+
"types": "./dist/tooltip.d.ts",
|
|
11
|
+
"default": "./dist/tooltip.es.js"
|
|
12
|
+
},
|
|
13
|
+
"./styles": "./dist/styles.css"
|
|
14
|
+
},
|
|
15
|
+
"source": "src/tooltip.tsx",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@radix-ui/react-tooltip": "~1.0.7",
|
|
18
|
+
"classnames": "~2.5.0",
|
|
19
|
+
"@purpurds/button": "3.0.1",
|
|
20
|
+
"@purpurds/paragraph": "3.0.1",
|
|
21
|
+
"@purpurds/icon": "3.0.1",
|
|
22
|
+
"@purpurds/tokens": "3.0.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@rushstack/eslint-patch": "~1.7.0",
|
|
26
|
+
"@storybook/blocks": "~7.6.0",
|
|
27
|
+
"@storybook/react": "~7.6.0",
|
|
28
|
+
"@telia/base-rig": "~8.2.0",
|
|
29
|
+
"@telia/react-rig": "~3.2.0",
|
|
30
|
+
"@testing-library/dom": "~9.3.3",
|
|
31
|
+
"@testing-library/jest-dom": "~6.4.0",
|
|
32
|
+
"@testing-library/react": "~14.2.0",
|
|
33
|
+
"@testing-library/user-event": "~14.5.1",
|
|
34
|
+
"@types/node": "18",
|
|
35
|
+
"@types/react-dom": "~18.2.17",
|
|
36
|
+
"@types/react": "~18.2.42",
|
|
37
|
+
"eslint-plugin-testing-library": "~6.2.0",
|
|
38
|
+
"eslint": "~8.57.0",
|
|
39
|
+
"jsdom": "~22.1.0",
|
|
40
|
+
"lint-staged": "~10.5.3",
|
|
41
|
+
"prettier": "~2.8.8",
|
|
42
|
+
"react-dom": "~18.2.0",
|
|
43
|
+
"react": "~18.2.0",
|
|
44
|
+
"typescript": "~5.2.2",
|
|
45
|
+
"vite": "~5.1.0",
|
|
46
|
+
"vitest": "~1.3.0",
|
|
47
|
+
"@purpurds/component-rig": "1.0.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build:dev": "vite",
|
|
51
|
+
"build:watch": "vite build --watch",
|
|
52
|
+
"build": "rm -rf dist && vite build && vite build --mode systemjs",
|
|
53
|
+
"ci:build": "rushx build",
|
|
54
|
+
"coverage": "vitest run --coverage",
|
|
55
|
+
"lint:fix": "eslint . --fix",
|
|
56
|
+
"lint": "lint-staged --no-stash 2>&1",
|
|
57
|
+
"sbdev": "rush sbdev",
|
|
58
|
+
"test:unit": "vitest run --passWithNoTests",
|
|
59
|
+
"test:watch": "vitest --watch",
|
|
60
|
+
"test": "rushx test:unit",
|
|
61
|
+
"typecheck": "tsc -p ./tsconfig.json"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/readme.mdx
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
|
|
2
|
+
|
|
3
|
+
import * as TooltipStories from "./src/tooltip.stories";
|
|
4
|
+
import packageInfo from "./package.json";
|
|
5
|
+
|
|
6
|
+
<Meta name="Docs" title="Components/Tooltip" of={TooltipStories} />
|
|
7
|
+
|
|
8
|
+
# Tooltip
|
|
9
|
+
|
|
10
|
+
<Subtitle>Version {packageInfo.version}</Subtitle>
|
|
11
|
+
|
|
12
|
+
### Showcase
|
|
13
|
+
|
|
14
|
+
<Primary />
|
|
15
|
+
|
|
16
|
+
### Properties
|
|
17
|
+
|
|
18
|
+
<ArgTypes />
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
|
|
22
|
+
#### Via NPM
|
|
23
|
+
|
|
24
|
+
Add the dependency to your consumer app like `"@purpurds/tooltip": "x.y.z"`
|
|
25
|
+
|
|
26
|
+
#### From outside the monorepo (build-time)
|
|
27
|
+
|
|
28
|
+
To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
In MyApp.tsx
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import "@purpurds/tokens/index.css";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
and
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import "@purpurds/tooltip/styles";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
In MyComponent.tsx
|
|
45
|
+
|
|
46
|
+
Standard usage:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Tooltip } from "@purpurds/tooltip";
|
|
50
|
+
|
|
51
|
+
export const MyComponent = () => {
|
|
52
|
+
return <Tooltip triggerAriaLabel="extra information about something">Some content</Tooltip>;
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Setting negative variant with position and alignment:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Tooltip, TOOLTIP_VARIANT, TOOLTIP_POSITION, TOOLTIP_ALIGN } from "@purpurds/tooltip";
|
|
60
|
+
|
|
61
|
+
export const MyComponent = () => {
|
|
62
|
+
return (
|
|
63
|
+
<Tooltip
|
|
64
|
+
triggerAriaLabel="extra information about something"
|
|
65
|
+
variant={TOOLTIP_VARIANT.PRIMARY_NEGATIVE}
|
|
66
|
+
position={TOOLTIP_POSITION.TOP}
|
|
67
|
+
align={TOOLTIP_ALIGN.CENTER}
|
|
68
|
+
>
|
|
69
|
+
Some content
|
|
70
|
+
</Tooltip>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Using custom trigger element:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Tooltip } from "@purpurds/tooltip";
|
|
79
|
+
import { Button, BUTTON_VARIANT } from "@purpurds/button";
|
|
80
|
+
import { IconPetDog } from "@purpurds/icon";
|
|
81
|
+
|
|
82
|
+
export const MyComponent = () => {
|
|
83
|
+
const customTooltipTrigger: ReactNode = (
|
|
84
|
+
<Button variant={BUTTON_VARIANT.PRIMARY}>
|
|
85
|
+
<IconPetDog size="md" />
|
|
86
|
+
This is a custom trigger
|
|
87
|
+
</Button>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return <Tooltip triggerElement={customTooltipTrigger}>Some content</Tooltip>;
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Using jsx in content:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { Tooltip } from "@purpurds/tooltip";
|
|
98
|
+
|
|
99
|
+
export const MyComponent = () => {
|
|
100
|
+
return (
|
|
101
|
+
<Tooltip triggerAriaLabel="extra information about something">
|
|
102
|
+
<div>Hello world! This is the content</div>
|
|
103
|
+
</Tooltip>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
```
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.purpur-tooltip {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
|
|
4
|
+
&__content {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
max-width: calc(17rem * var(--purpur-rescale));
|
|
7
|
+
border-radius: var(--purpur-border-radius-md);
|
|
8
|
+
padding: var(--purpur-spacing-150);
|
|
9
|
+
user-select: none;
|
|
10
|
+
animation-duration: var(--purpur-motion-duration-400);
|
|
11
|
+
animation-timing-function: var(--purpur-motion-easing-ease-out);
|
|
12
|
+
will-change: transform, opacity;
|
|
13
|
+
&--primary {
|
|
14
|
+
background-color: var(--purpur-color-background-tone-on-tone-primary);
|
|
15
|
+
}
|
|
16
|
+
&--primary-negative {
|
|
17
|
+
background-color: var(--purpur-color-background-tone-on-tone-secondary);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
&__content[data-state="delayed-open"][data-side="top"] {
|
|
21
|
+
animation-name: slideDownAndFade;
|
|
22
|
+
}
|
|
23
|
+
&__content[data-state="delayed-open"][data-side="right"] {
|
|
24
|
+
animation-name: slideLeftAndFade;
|
|
25
|
+
}
|
|
26
|
+
&__content[data-state="delayed-open"][data-side="bottom"] {
|
|
27
|
+
animation-name: slideUpAndFade;
|
|
28
|
+
}
|
|
29
|
+
&__content[data-state="delayed-open"][data-side="left"] {
|
|
30
|
+
animation-name: slideRightAndFade;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&__arrow {
|
|
34
|
+
&--primary {
|
|
35
|
+
fill: var(--purpur-color-background-tone-on-tone-primary);
|
|
36
|
+
}
|
|
37
|
+
&--primary-negative {
|
|
38
|
+
fill: var(--purpur-color-background-tone-on-tone-secondary);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&__paragraph {
|
|
43
|
+
&--primary {
|
|
44
|
+
color: var(--purpur-color-text-tone-on-tone-primary);
|
|
45
|
+
}
|
|
46
|
+
&--primary-negative {
|
|
47
|
+
color: var(--purpur-color-text-tone-on-tone-secondary);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@keyframes slideUpAndFade {
|
|
53
|
+
from {
|
|
54
|
+
opacity: 0;
|
|
55
|
+
transform: translateY(2px);
|
|
56
|
+
}
|
|
57
|
+
to {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
transform: translateY(0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes slideRightAndFade {
|
|
64
|
+
from {
|
|
65
|
+
opacity: 0;
|
|
66
|
+
transform: translateX(-2px);
|
|
67
|
+
}
|
|
68
|
+
to {
|
|
69
|
+
opacity: 1;
|
|
70
|
+
transform: translateX(0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes slideDownAndFade {
|
|
75
|
+
from {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transform: translateY(-2px);
|
|
78
|
+
}
|
|
79
|
+
to {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
transform: translateY(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes slideLeftAndFade {
|
|
86
|
+
from {
|
|
87
|
+
opacity: 0;
|
|
88
|
+
transform: translateX(2px);
|
|
89
|
+
}
|
|
90
|
+
to {
|
|
91
|
+
opacity: 1;
|
|
92
|
+
transform: translateX(0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { Button, BUTTON_VARIANT } from "@purpurds/button";
|
|
3
|
+
import { IconPetDog } from "@purpurds/icon";
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
import "@purpurds/button/styles";
|
|
7
|
+
import "@purpurds/icon/styles";
|
|
8
|
+
import "@purpurds/paragraph/styles";
|
|
9
|
+
import { Tooltip, TOOLTIP_ALIGN, TOOLTIP_POSITION, TOOLTIP_VARIANT } from "./tooltip";
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof Tooltip> = {
|
|
12
|
+
title: "Components/Tooltip",
|
|
13
|
+
component: Tooltip,
|
|
14
|
+
decorators: [
|
|
15
|
+
(Story) => (
|
|
16
|
+
<div style={{ padding: "3rem 0 3rem 10rem" }}>
|
|
17
|
+
<Story />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
args: {
|
|
22
|
+
variant: "primary",
|
|
23
|
+
children: "Some tooltip content",
|
|
24
|
+
triggerAriaLabel: "Tooltip button",
|
|
25
|
+
align: "center",
|
|
26
|
+
position: "top",
|
|
27
|
+
},
|
|
28
|
+
argTypes: {
|
|
29
|
+
variant: { options: Object.values(TOOLTIP_VARIANT), control: { type: "select" } },
|
|
30
|
+
position: { options: Object.values(TOOLTIP_POSITION), control: { type: "select" } },
|
|
31
|
+
align: { options: Object.values(TOOLTIP_ALIGN), control: { type: "select" } },
|
|
32
|
+
},
|
|
33
|
+
parameters: {
|
|
34
|
+
design: [
|
|
35
|
+
{
|
|
36
|
+
name: "Tooltip",
|
|
37
|
+
type: "figma",
|
|
38
|
+
url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=33655%3A8134&mode=design&t=XME73YbhUMJE1LLn-1",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const customTooltipTrigger: ReactNode = (
|
|
44
|
+
<Button aria-label="toggleButton" variant={BUTTON_VARIANT.PRIMARY}>
|
|
45
|
+
<IconPetDog size="md" />
|
|
46
|
+
This is a custom trigger
|
|
47
|
+
</Button>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const jsxContent: ReactNode = (
|
|
51
|
+
<div style={{ color: "white" }}>
|
|
52
|
+
Some content in a div{" "}
|
|
53
|
+
<a href="https://www.telia.se" style={{ color: "white" }} target="_blank" rel="noreferrer">
|
|
54
|
+
Telia.se
|
|
55
|
+
</a>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export default meta;
|
|
60
|
+
type Story = StoryObj<typeof Tooltip>;
|
|
61
|
+
|
|
62
|
+
export const Showcase: Story = {
|
|
63
|
+
name: "Primary",
|
|
64
|
+
args: {
|
|
65
|
+
children:
|
|
66
|
+
"This is a longer tooltip text to display that there is a max-width on the tooltip container.",
|
|
67
|
+
position: "bottom",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const TooltipNegative: Story = {
|
|
72
|
+
name: "Primary negative",
|
|
73
|
+
args: {
|
|
74
|
+
variant: "primary-negative",
|
|
75
|
+
},
|
|
76
|
+
parameters: {
|
|
77
|
+
backgrounds: {
|
|
78
|
+
default: "Primary tone-on-tone",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const TooltipWithCustomTrigger: Story = {
|
|
84
|
+
name: "With custom trigger",
|
|
85
|
+
args: {
|
|
86
|
+
triggerElement: customTooltipTrigger,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const TooltipWithJSXContent: Story = {
|
|
91
|
+
name: "With JSX content",
|
|
92
|
+
args: {
|
|
93
|
+
children: jsxContent,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as matchers from "@testing-library/jest-dom/matchers";
|
|
3
|
+
import {
|
|
4
|
+
cleanup,
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
waitFor,
|
|
8
|
+
waitForElementToBeRemoved,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import userEvent from "@testing-library/user-event";
|
|
11
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
12
|
+
|
|
13
|
+
import { Tooltip, TooltipProps } from "./tooltip";
|
|
14
|
+
|
|
15
|
+
class ResizeObserver {
|
|
16
|
+
observe() {
|
|
17
|
+
// do nothing
|
|
18
|
+
}
|
|
19
|
+
unobserve() {
|
|
20
|
+
// do nothing
|
|
21
|
+
}
|
|
22
|
+
disconnect() {
|
|
23
|
+
// do nothing
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
window.ResizeObserver = ResizeObserver;
|
|
28
|
+
expect.extend(matchers);
|
|
29
|
+
|
|
30
|
+
describe("Tooltip", () => {
|
|
31
|
+
afterEach(cleanup);
|
|
32
|
+
|
|
33
|
+
it("should open on hover", async () => {
|
|
34
|
+
renderTooltip();
|
|
35
|
+
await userEvent.hover(screen.getByTestId("tooltip-trigger-button"));
|
|
36
|
+
await waitFor(() => expect(screen.getByTestId("tooltip-content")).toBeDefined());
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should open on click", async () => {
|
|
40
|
+
renderTooltip();
|
|
41
|
+
await userEvent.click(screen.getByTestId("tooltip-trigger-button"));
|
|
42
|
+
await waitFor(() => expect(screen.getByTestId("tooltip-content")).toBeDefined());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should close on escape key", async () => {
|
|
46
|
+
renderTooltip();
|
|
47
|
+
await userEvent.click(screen.getByTestId("tooltip-trigger-button"));
|
|
48
|
+
await waitFor(() => expect(screen.getByTestId("tooltip-content")).toBeDefined());
|
|
49
|
+
userEvent.keyboard("{Escape}");
|
|
50
|
+
await waitForElementToBeRemoved(() => screen.queryByTestId("tooltip-content"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("aria label should be passed to button", () => {
|
|
54
|
+
renderTooltip({ triggerAriaLabel: "tooltipButtonLabel" });
|
|
55
|
+
expect(screen.getByTestId("tooltip-trigger-button")).toHaveAttribute(
|
|
56
|
+
"aria-label",
|
|
57
|
+
"tooltipButtonLabel"
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should render tooltip button", () => {
|
|
62
|
+
renderTooltip();
|
|
63
|
+
expect(screen.getByTestId("tooltip-trigger-button")).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should render custom tooltip trigger", () => {
|
|
67
|
+
renderTooltip({ triggerElement: <span data-testid="tooltip-custom-trigger" /> });
|
|
68
|
+
expect(screen.getByTestId("tooltip-custom-trigger")).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should render text content inside paragraph", async () => {
|
|
72
|
+
renderTooltip({ children: "This is the tooltip content" });
|
|
73
|
+
await userEvent.click(screen.getByTestId("tooltip-trigger-button"));
|
|
74
|
+
await waitFor(() => expect(screen.getByTestId("tooltip-content")).toBeDefined());
|
|
75
|
+
expect(screen.getAllByTestId("tooltip-paragraph")[0].textContent).toContain(
|
|
76
|
+
"This is the tooltip content"
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should not render default paragraph when custom content provided", async () => {
|
|
81
|
+
renderTooltip({
|
|
82
|
+
children: <span data-testid="tooltip-custom-content">Some content in a div</span>,
|
|
83
|
+
});
|
|
84
|
+
await userEvent.click(screen.getByTestId("tooltip-trigger-button"));
|
|
85
|
+
await waitFor(() => expect(screen.getByTestId("tooltip-content")).toBeDefined());
|
|
86
|
+
expect(screen.queryByTestId("tooltip-paragraph")).toBe(null);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should render custom content", async () => {
|
|
90
|
+
renderTooltip({
|
|
91
|
+
children: <span data-testid="tooltip-custom-content">Some content in a div</span>,
|
|
92
|
+
});
|
|
93
|
+
await userEvent.click(screen.getByTestId("tooltip-trigger-button"));
|
|
94
|
+
await waitFor(() => expect(screen.getByTestId("tooltip-content")).toBeDefined());
|
|
95
|
+
expect(screen.getAllByTestId("tooltip-custom-content")[0].textContent).toContain(
|
|
96
|
+
"Some content in a div"
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function renderTooltip({
|
|
102
|
+
["data-testid"]: dataTestId = "tooltip",
|
|
103
|
+
children = "Some text content",
|
|
104
|
+
className,
|
|
105
|
+
variant,
|
|
106
|
+
position,
|
|
107
|
+
align,
|
|
108
|
+
triggerAriaLabel,
|
|
109
|
+
triggerElement,
|
|
110
|
+
...props
|
|
111
|
+
}: Partial<TooltipProps> = {}) {
|
|
112
|
+
render(
|
|
113
|
+
<Tooltip
|
|
114
|
+
className={className}
|
|
115
|
+
variant={variant}
|
|
116
|
+
position={position}
|
|
117
|
+
align={align}
|
|
118
|
+
data-testid={dataTestId}
|
|
119
|
+
triggerAriaLabel={triggerAriaLabel}
|
|
120
|
+
triggerElement={triggerElement}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</Tooltip>
|
|
125
|
+
);
|
|
126
|
+
}
|
package/src/tooltip.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { Children, ForwardedRef, forwardRef, ReactNode, useState } from "react";
|
|
2
|
+
import { Button, BUTTON_VARIANT } from "@purpurds/button";
|
|
3
|
+
import { IconInfo } from "@purpurds/icon";
|
|
4
|
+
import { Paragraph, ParagraphVariant } from "@purpurds/paragraph";
|
|
5
|
+
import { purpurMotionDuration400 } from "@purpurds/tokens";
|
|
6
|
+
import * as RadixTooltip from "@radix-ui/react-tooltip";
|
|
7
|
+
import c from "classnames/bind";
|
|
8
|
+
|
|
9
|
+
import styles from "./tooltip.module.scss";
|
|
10
|
+
|
|
11
|
+
const cx = c.bind(styles);
|
|
12
|
+
|
|
13
|
+
export const TOOLTIP_VARIANT = {
|
|
14
|
+
PRIMARY: "primary",
|
|
15
|
+
PRIMARY_NEGATIVE: "primary-negative",
|
|
16
|
+
} as const;
|
|
17
|
+
export type TooltipVariant = (typeof TOOLTIP_VARIANT)[keyof typeof TOOLTIP_VARIANT];
|
|
18
|
+
|
|
19
|
+
export const TOOLTIP_POSITION = {
|
|
20
|
+
TOP: "top",
|
|
21
|
+
BOTTOM: "bottom",
|
|
22
|
+
LEFT: "left",
|
|
23
|
+
RIGHT: "right",
|
|
24
|
+
} as const;
|
|
25
|
+
export type TooltipPosition = (typeof TOOLTIP_POSITION)[keyof typeof TOOLTIP_POSITION];
|
|
26
|
+
|
|
27
|
+
export const TOOLTIP_ALIGN = {
|
|
28
|
+
START: "start",
|
|
29
|
+
CENTER: "center",
|
|
30
|
+
END: "end",
|
|
31
|
+
} as const;
|
|
32
|
+
export type TooltipAlign = (typeof TOOLTIP_ALIGN)[keyof typeof TOOLTIP_ALIGN];
|
|
33
|
+
|
|
34
|
+
export type TooltipProps = {
|
|
35
|
+
align?: TooltipAlign;
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
variant?: TooltipVariant;
|
|
38
|
+
position?: TooltipPosition;
|
|
39
|
+
["data-testid"]?: string;
|
|
40
|
+
triggerAriaLabel?: string;
|
|
41
|
+
triggerElement?: ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ButtonVariants = {
|
|
46
|
+
primary: BUTTON_VARIANT.TERTIARY_PURPLE,
|
|
47
|
+
["primary-negative"]: BUTTON_VARIANT.TERTIARY_PURPLE_NEGATVIE,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const rootClassName = "purpur-tooltip";
|
|
51
|
+
|
|
52
|
+
export const Tooltip = forwardRef(
|
|
53
|
+
(
|
|
54
|
+
{
|
|
55
|
+
["data-testid"]: dataTestId,
|
|
56
|
+
children,
|
|
57
|
+
className,
|
|
58
|
+
variant = TOOLTIP_VARIANT.PRIMARY,
|
|
59
|
+
position = TOOLTIP_POSITION.TOP,
|
|
60
|
+
align = TOOLTIP_ALIGN.CENTER,
|
|
61
|
+
triggerAriaLabel = "",
|
|
62
|
+
triggerElement,
|
|
63
|
+
...props
|
|
64
|
+
}: TooltipProps,
|
|
65
|
+
ref: ForwardedRef<HTMLButtonElement>
|
|
66
|
+
) => {
|
|
67
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
68
|
+
const classes = cx([
|
|
69
|
+
className,
|
|
70
|
+
rootClassName,
|
|
71
|
+
{
|
|
72
|
+
[`${rootClassName}--${variant}`]: variant,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const tooltipButton = (
|
|
76
|
+
<Button
|
|
77
|
+
ref={ref}
|
|
78
|
+
aria-label={triggerAriaLabel}
|
|
79
|
+
variant={ButtonVariants[variant]}
|
|
80
|
+
iconOnly
|
|
81
|
+
data-testid={`${dataTestId}-trigger-button`}
|
|
82
|
+
>
|
|
83
|
+
<IconInfo size="md" />
|
|
84
|
+
</Button>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div data-testid={dataTestId} className={classes}>
|
|
89
|
+
<RadixTooltip.Provider delayDuration={parseInt(purpurMotionDuration400)}>
|
|
90
|
+
<RadixTooltip.Root open={isOpen} onOpenChange={setIsOpen} {...props}>
|
|
91
|
+
<RadixTooltip.Trigger
|
|
92
|
+
asChild
|
|
93
|
+
onClick={(e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setIsOpen(true);
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{Children.count(triggerElement) === 0 ? tooltipButton : triggerElement}
|
|
99
|
+
</RadixTooltip.Trigger>
|
|
100
|
+
<RadixTooltip.Portal>
|
|
101
|
+
<RadixTooltip.Content
|
|
102
|
+
side={position}
|
|
103
|
+
align={align}
|
|
104
|
+
className={cx([
|
|
105
|
+
styles[`${rootClassName}__content`],
|
|
106
|
+
styles[`${rootClassName}__content--${variant}`],
|
|
107
|
+
])}
|
|
108
|
+
sideOffset={-5}
|
|
109
|
+
data-testid={`${dataTestId}-content`}
|
|
110
|
+
>
|
|
111
|
+
{typeof children === "string" ? (
|
|
112
|
+
<Paragraph
|
|
113
|
+
className={cx([
|
|
114
|
+
styles[`${rootClassName}__paragraph`],
|
|
115
|
+
styles[`${rootClassName}__paragraph--${variant}`],
|
|
116
|
+
])}
|
|
117
|
+
variant={ParagraphVariant.PARAGRAPH100}
|
|
118
|
+
data-testid={`${dataTestId}-paragraph`}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</Paragraph>
|
|
122
|
+
) : (
|
|
123
|
+
children
|
|
124
|
+
)}
|
|
125
|
+
<RadixTooltip.Arrow
|
|
126
|
+
className={cx([
|
|
127
|
+
styles[`${rootClassName}__arrow`],
|
|
128
|
+
styles[`${rootClassName}__arrow--${variant}`],
|
|
129
|
+
])}
|
|
130
|
+
/>
|
|
131
|
+
</RadixTooltip.Content>
|
|
132
|
+
</RadixTooltip.Portal>
|
|
133
|
+
</RadixTooltip.Root>
|
|
134
|
+
</RadixTooltip.Provider>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
Tooltip.displayName = "Tooltip";
|
package/tsconfig.json
ADDED