@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/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
+ ```
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -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
+ }
@@ -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
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "@purpurds/component-rig",
3
+ "compilerOptions": {
4
+ "rootDir": "src"
5
+ },
6
+ "include": ["src"]
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@purpurds/component-rig/tsconfig.types.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/*.test.*", "**/*.stories.*"]
9
+ }
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "vite";
2
+ import { getConfig } from "@purpurds/component-rig/vite.config";
3
+ import pkg from "./package.json";
4
+
5
+ export default defineConfig(({ mode }) => getConfig(pkg, mode));