Skip to content

Commit f60642f

Browse files
committed
feat: implement tooltip base functionality
1 parent aa46133 commit f60642f

File tree

4 files changed

+186
-36
lines changed

4 files changed

+186
-36
lines changed
Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,133 @@
11
@import "@react-ck/theme";
22
@import "@react-ck/elevation";
3+
@import "@react-ck/text";
34

4-
$padding-v: #{get-spacing(0.5)};
5+
$outer-spacing: get-spacing(1);
6+
$inner-spacing-y: get-spacing(1);
7+
$inner-spacing-x: get-spacing(2);
8+
$caret-spacing: get-spacing(1.66);
9+
10+
@mixin animation-initial {
11+
opacity: 0;
12+
}
13+
14+
@keyframes appear {
15+
from {
16+
@include animation-initial;
17+
}
18+
to {
19+
opacity: 1;
20+
}
21+
}
522

623
.container {
7-
padding: #{$padding-v} 0;
24+
@include define-css-var(tooltip, max-height, var(--pe-max-height));
25+
@include define-css-var(tooltip, max-width, var(--pe-max-width));
26+
@include define-css-var(tooltip, container-padding-x, 0);
27+
@include define-css-var(tooltip, container-padding-y, 0);
28+
29+
padding: #{get-css-var(tooltip, container-padding-y)} #{get-css-var(tooltip, container-padding-x)};
830
box-sizing: border-box;
931
z-index: map-get-strict($elevation, popup);
32+
33+
@include animation-initial;
34+
35+
will-change: opacity;
36+
animation: appear 0.2s ease forwards;
37+
38+
// Apply vertical spacing
39+
&[class*="top"],
40+
&[class*="bottom"] {
41+
@include define-css-var(tooltip, container-padding-y, $outer-spacing);
42+
@include define-css-var(
43+
tooltip,
44+
max-height,
45+
calc(var(--pe-max-height) - #{$outer-spacing} * 2)
46+
);
47+
48+
&[class*="start"] .content::before {
49+
left: $caret-spacing;
50+
}
51+
52+
&[class*="center"] .content::before {
53+
left: 50%;
54+
transform: translateX(-50%);
55+
}
56+
57+
&[class*="end"] .content::before {
58+
right: $caret-spacing;
59+
}
60+
}
61+
62+
&[class*="left"],
63+
&[class*="right"] {
64+
@include define-css-var(tooltip, container-padding-x, $outer-spacing);
65+
@include define-css-var(tooltip, max-width, calc(var(--pe-max-width) - #{$outer-spacing} * 2));
66+
67+
&[class*="start"] .content::before {
68+
top: $caret-spacing;
69+
}
70+
71+
&[class*="center"] .content::before {
72+
top: 50%;
73+
transform: translateY(-50%);
74+
}
75+
76+
&[class*="end"] .content::before {
77+
bottom: $caret-spacing;
78+
}
79+
}
80+
81+
&[class*="top"] {
82+
.content::before {
83+
top: 100%;
84+
border-color: get-color(neutral-light-1) transparent transparent transparent;
85+
filter: drop-shadow(0 3px 2px get-color(neutral-light-3));
86+
}
87+
}
88+
89+
&[class*="bottom"] {
90+
.content::before {
91+
bottom: 100%;
92+
border-color: transparent transparent get-color(neutral-light-1) transparent;
93+
filter: drop-shadow(0 -3px 2px get-color(neutral-light-3));
94+
}
95+
}
96+
97+
&[class*="left"] {
98+
.content::before {
99+
left: 100%;
100+
border-color: transparent transparent transparent get-color(neutral-light-1);
101+
filter: drop-shadow(3px 0 2px get-color(neutral-light-3));
102+
}
103+
}
104+
105+
&[class*="right"] {
106+
.content::before {
107+
right: 100%;
108+
border-color: transparent get-color(neutral-light-1) transparent transparent;
109+
filter: drop-shadow(-3px 0 2px get-color(neutral-light-3));
110+
}
111+
}
10112
}
11113

12114
.content {
13-
padding: get-spacing(2);
14-
max-height: calc(var(--pe-max-height) - #{$padding-v} * 2);
15-
max-width: var(--pe-max-width);
115+
@include text-base;
116+
@include text-variation(extra-small);
117+
118+
position: relative;
119+
padding: #{$inner-spacing-y} #{$inner-spacing-x};
120+
max-height: get-css-var(tooltip, max-height);
121+
max-width: get-css-var(tooltip, max-width);
16122
box-sizing: border-box;
123+
124+
&::before {
125+
content: "";
126+
position: absolute;
127+
display: block;
128+
border-style: solid;
129+
border-width: get-spacing(0.66);
130+
width: 0px;
131+
height: 0px;
132+
}
17133
}
Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import React from "react";
1+
import styles from "./index.module.scss";
2+
import React, { useEffect, useState } from "react";
23
import { Layer } from "@react-ck/layers";
34
import { PositionEngine, type PositionEngineProps } from "../position-engine";
45
import { ScrollableContainer } from "../scrollable-container";
56
import { Card } from "@react-ck/card";
6-
import styles from "./index.module.scss";
7+
import classNames from "classnames";
78

89
export interface TooltipProps {
910
anchor: PositionEngineProps["anchorRef"];
1011
position?: PositionEngineProps["position"];
1112
children?: React.ReactNode;
13+
/**
14+
* The tooltip will open/close on hover by default,
15+
* if you pass true/false, this behavior will be overridden and should be managed by the consumer
16+
*/
1217
open?: boolean;
1318
}
1419

@@ -17,22 +22,58 @@ export const Tooltip = ({
1722
position = "auto",
1823
open,
1924
children,
20-
}: Readonly<TooltipProps>): React.ReactNode =>
21-
open && (
22-
<PositionEngine
23-
exclude={["left", "right", "end"]}
24-
position={position}
25-
anchorRef={anchor}
26-
render={({ style }) => (
27-
<Layer elevation="popup">
28-
<div style={style} className={styles.container}>
29-
<Card skin="shadowed" spacing="none" className={styles.card}>
30-
<ScrollableContainer horizontal={false} className={styles.content}>
31-
{children}
32-
</ScrollableContainer>
33-
</Card>
34-
</div>
35-
</Layer>
36-
)}
37-
/>
25+
}: Readonly<TooltipProps>): React.ReactNode => {
26+
const [internalOpen, setInternalOpen] = useState(open);
27+
28+
useEffect(() => {
29+
const ref = anchor.current;
30+
31+
if (open !== undefined) {
32+
setInternalOpen(open);
33+
return;
34+
}
35+
36+
if (!ref) throw new Error("Tooltip anchor ref is required");
37+
38+
let to: ReturnType<typeof setTimeout> | undefined = undefined;
39+
40+
function handleMouseEnter(): void {
41+
clearTimeout(to);
42+
to = setTimeout(() => {
43+
setInternalOpen(true);
44+
}, 300);
45+
}
46+
47+
function handleMouseLeave(): void {
48+
clearTimeout(to);
49+
setInternalOpen(false);
50+
}
51+
52+
ref.addEventListener("mouseenter", handleMouseEnter);
53+
ref.addEventListener("mouseleave", handleMouseLeave);
54+
55+
return () => {
56+
clearTimeout(to);
57+
ref.removeEventListener("mouseenter", handleMouseEnter);
58+
ref.removeEventListener("mouseleave", handleMouseLeave);
59+
};
60+
}, [anchor, open]);
61+
62+
return (
63+
internalOpen && (
64+
<PositionEngine
65+
position={position}
66+
anchorRef={anchor}
67+
render={({ style, position }) => (
68+
<Layer elevation="popup">
69+
<div style={style} className={classNames(styles.container, position)}>
70+
<Card skin="shadowed" spacing="none" className={styles.card}>
71+
<div className={styles.content}>{children}</div>
72+
</Card>
73+
</div>
74+
</Layer>
75+
)}
76+
/>
77+
)
3878
);
79+
};

packages/components/card/src/styles/index.module.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
background-color: get-color(neutral-light-1);
99
border-radius: get-css-var(card, border-radius);
10-
overflow: hidden;
1110

1211
// Skins
1312

packages/docs/stories/src/tooltip.stories.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Manager } from "@react-ck/manager";
44
import { configureStory } from "@react-ck/story-config";
55
import { Tooltip } from "@react-ck/provisional/src";
66
import { Button } from "@react-ck/button";
7+
import { faker } from "@faker-js/faker";
78

89
type Story = StoryObj<typeof Tooltip>;
910

@@ -29,7 +30,8 @@ export default meta;
2930

3031
export const Component: Story = {
3132
args: {
32-
open: true,
33+
open: undefined,
34+
position: undefined,
3335
},
3436
render: ({ open, position }): React.ReactElement => {
3537
// eslint-disable-next-line react-hooks/rules-of-hooks -- exception for storybook
@@ -39,15 +41,7 @@ export const Component: Story = {
3941
<>
4042
<Button rootRef={buttonRef}>Button</Button>
4143
<Tooltip anchor={buttonRef} open={open} position={position}>
42-
Content Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione nulla est, quidem
43-
enim a molestias accusantium quo officia provident maxime voluptatem, beatae delectus
44-
aliquid ipsa perferendis accusamus! Eius, laborum quisquam. Eius soluta deserunt
45-
aspernatur tenetur, laudantium quod corrupti natus facilis est ab esse sunt dolore magni
46-
cum accusamus nemo. Optio adipisci itaque exercitationem quo nulla, odit eligendi natus
47-
est cupiditate aperiam nemo, vero explicabo. Non eveniet ipsum, dolores suscipit sit
48-
deserunt doloribus. Dolorum aspernatur iusto, aliquid officiis illo modi vitae.
49-
Exercitationem laudantium inventore nemo harum commodi doloribus totam porro aliquam. Quae
50-
aliquam iusto neque ipsam non? Consequuntur saepe inventore aliquam!
44+
{faker.lorem.sentence(5)}
5145
</Tooltip>
5246
</>
5347
);

0 commit comments

Comments
 (0)