@sproutsocial/seeds-react-tooltip 1.1.19 → 1.1.27
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +63 -0
- package/dist/esm/index.js +131 -123
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +17 -8
- package/dist/index.d.ts +17 -8
- package/dist/index.js +129 -121
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/Tooltip.stories.tsx +125 -0
- package/src/Tooltip.tsx +141 -128
- package/src/TooltipTypes.ts +32 -6
- package/src/__tests__/Tooltip.test.tsx +43 -153
- package/src/__tests__/mapPlacement.test.ts +56 -0
package/src/Tooltip.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
2
|
+
import { useState, useRef, useEffect, useContext } from "react";
|
|
3
3
|
import MOTION from "@sproutsocial/seeds-motion/unitless";
|
|
4
|
-
import
|
|
4
|
+
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
|
|
5
|
+
import { DisablePortalToBodyContext } from "@sproutsocial/seeds-react-portal";
|
|
6
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
5
7
|
import { StyledTooltipContent } from "./styles";
|
|
6
|
-
import type { TypeTooltipProps
|
|
8
|
+
import type { TypeTooltipProps } from "./TooltipTypes";
|
|
7
9
|
|
|
8
10
|
let idCounter = 0;
|
|
9
11
|
|
|
@@ -11,51 +13,44 @@ const hasAttribute = (child: React.ReactNode, attribute: string) => {
|
|
|
11
13
|
return React.isValidElement(child) && child.props[attribute] !== undefined;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{...rest}
|
|
52
|
-
>
|
|
53
|
-
{children}
|
|
54
|
-
</StyledTooltipContent>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
16
|
+
type Side = "top" | "bottom" | "left" | "right";
|
|
17
|
+
type Align = "start" | "center" | "end";
|
|
18
|
+
|
|
19
|
+
// `auto`, `auto-start`, and `auto-end` resolve to `side: "top"` (with align
|
|
20
|
+
// taken from the suffix) and rely on base-ui's Positioner collision flipping at
|
|
21
|
+
// runtime, rather than Popper's old best-initial-side selection. Exported for
|
|
22
|
+
// unit testing of this mapping.
|
|
23
|
+
export function mapPlacement(placement: string): { side: Side; align: Align } {
|
|
24
|
+
const parts = placement.split("-");
|
|
25
|
+
let side: Side;
|
|
26
|
+
let align: Align = "center";
|
|
27
|
+
|
|
28
|
+
switch (parts[0]) {
|
|
29
|
+
case "bottom":
|
|
30
|
+
side = "bottom";
|
|
31
|
+
break;
|
|
32
|
+
case "left":
|
|
33
|
+
side = "left";
|
|
34
|
+
break;
|
|
35
|
+
case "right":
|
|
36
|
+
side = "right";
|
|
37
|
+
break;
|
|
38
|
+
case "top":
|
|
39
|
+
case "auto":
|
|
40
|
+
default:
|
|
41
|
+
side = "top";
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (parts[1] === "start") {
|
|
46
|
+
align = "start";
|
|
47
|
+
} else if (parts[1] === "end") {
|
|
48
|
+
align = "end";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { side, align };
|
|
52
|
+
}
|
|
57
53
|
|
|
58
|
-
/** Core component */
|
|
59
54
|
const Tooltip = ({
|
|
60
55
|
content,
|
|
61
56
|
children,
|
|
@@ -66,18 +61,22 @@ const Tooltip = ({
|
|
|
66
61
|
zIndex = 7,
|
|
67
62
|
qa,
|
|
68
63
|
popoutProps,
|
|
64
|
+
triggerProps,
|
|
69
65
|
truncated = false,
|
|
70
66
|
onFocus,
|
|
71
67
|
onBlur,
|
|
72
68
|
legacyMouseInteraction,
|
|
73
69
|
...rest
|
|
74
70
|
}: TypeTooltipProps) => {
|
|
75
|
-
const [
|
|
76
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
71
|
+
const [open, setOpen] = useState(false);
|
|
77
72
|
const [id] = useState(`Racine-tooltip-${idCounter++}`);
|
|
73
|
+
const triggerRef = useRef<HTMLSpanElement>(null);
|
|
78
74
|
const isInvalidContent = content === null || content === undefined;
|
|
75
|
+
const disablePortalToBody = useContext(DisablePortalToBodyContext);
|
|
76
|
+
const [portalContainer, setPortalContainer] = useState<
|
|
77
|
+
HTMLElement | undefined
|
|
78
|
+
>(undefined);
|
|
79
79
|
|
|
80
|
-
// Compute exitDelay based on legacyMouseInteraction if not explicitly provided
|
|
81
80
|
const resolvedExitDelay =
|
|
82
81
|
exitDelay !== undefined
|
|
83
82
|
? exitDelay
|
|
@@ -85,87 +84,29 @@ const Tooltip = ({
|
|
|
85
84
|
? MOTION.MOTION_DURATION_FAST * 1000
|
|
86
85
|
: 0;
|
|
87
86
|
|
|
88
|
-
// @ts-ignore Will fix during refactor
|
|
89
|
-
const show = (e) => {
|
|
90
|
-
onFocus?.(e);
|
|
91
|
-
setShouldShow(true);
|
|
92
|
-
};
|
|
93
|
-
// @ts-ignore Will fix during refactor
|
|
94
|
-
const hide = (e) => {
|
|
95
|
-
onBlur?.(e);
|
|
96
|
-
setShouldShow(false);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
87
|
const defaultAppearance =
|
|
100
88
|
appearance || (typeof content === "object" ? "box" : "pill");
|
|
101
89
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
setShouldShow(false);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (shouldShow) {
|
|
115
|
-
timeout = setTimeout(() => setIsOpen(true), enterDelay);
|
|
116
|
-
} else {
|
|
117
|
-
timeout = setTimeout(() => {
|
|
118
|
-
setIsOpen(false);
|
|
119
|
-
}, resolvedExitDelay);
|
|
120
|
-
}
|
|
90
|
+
const { side, align } = mapPlacement(placement);
|
|
91
|
+
|
|
92
|
+
const mergedWrapperProps = {
|
|
93
|
+
...popoutProps,
|
|
94
|
+
...triggerProps,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const hasWrapperProps = Object.keys(mergedWrapperProps).length > 0;
|
|
121
98
|
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (disablePortalToBody && triggerRef.current) {
|
|
101
|
+
const dialogContent =
|
|
102
|
+
triggerRef.current.closest<HTMLElement>("[role='dialog']");
|
|
103
|
+
setPortalContainer(dialogContent ?? undefined);
|
|
125
104
|
}
|
|
126
|
-
|
|
127
|
-
documentBody.removeEventListener("keydown", onEsc, { capture: true });
|
|
128
|
-
clearTimeout(timeout);
|
|
129
|
-
};
|
|
130
|
-
}, [isOpen, setShouldShow, shouldShow, enterDelay, resolvedExitDelay]);
|
|
131
|
-
|
|
132
|
-
/** The wrapped content of whats inside the Tooltip */
|
|
133
|
-
const TooltipContent = () => (
|
|
134
|
-
<TooltipBubble
|
|
135
|
-
appearance={defaultAppearance}
|
|
136
|
-
onFocus={show}
|
|
137
|
-
onBlur={hide}
|
|
138
|
-
legacyMouseInteraction={legacyMouseInteraction}
|
|
139
|
-
aria-expanded={isOpen}
|
|
140
|
-
id={id}
|
|
141
|
-
{...rest}
|
|
142
|
-
>
|
|
143
|
-
{content}
|
|
144
|
-
</TooltipBubble>
|
|
145
|
-
);
|
|
105
|
+
}, [disablePortalToBody]);
|
|
146
106
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
content={!isInvalidContent ? TooltipContent : undefined}
|
|
150
|
-
isOpen={isOpen}
|
|
151
|
-
placement={placement}
|
|
152
|
-
qa={{
|
|
153
|
-
"data-qa-tooltip": id,
|
|
154
|
-
...qa,
|
|
155
|
-
}}
|
|
156
|
-
id={id + "-wrapper"}
|
|
157
|
-
focusOnContent={false}
|
|
158
|
-
zIndex={zIndex}
|
|
159
|
-
aria-haspopup="false"
|
|
160
|
-
display={truncated ? "flex" : undefined}
|
|
161
|
-
disableWrapperAria={true} // required so that the child span doesnt take in redundant aria props
|
|
162
|
-
{...popoutProps}
|
|
163
|
-
>
|
|
107
|
+
if (isInvalidContent) {
|
|
108
|
+
return (
|
|
164
109
|
<span
|
|
165
|
-
onBlur={hide}
|
|
166
|
-
onFocus={show}
|
|
167
|
-
onMouseEnter={show}
|
|
168
|
-
onMouseLeave={hide}
|
|
169
110
|
style={
|
|
170
111
|
truncated
|
|
171
112
|
? {
|
|
@@ -175,18 +116,90 @@ const Tooltip = ({
|
|
|
175
116
|
}
|
|
176
117
|
: {}
|
|
177
118
|
}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</span>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const tooltipTree = (
|
|
126
|
+
<BaseTooltip.Root
|
|
127
|
+
open={open}
|
|
128
|
+
onOpenChange={setOpen}
|
|
129
|
+
disabled={isInvalidContent}
|
|
130
|
+
disableHoverablePopup={!legacyMouseInteraction}
|
|
131
|
+
>
|
|
132
|
+
<BaseTooltip.Trigger
|
|
133
|
+
delay={enterDelay}
|
|
134
|
+
closeDelay={resolvedExitDelay}
|
|
135
|
+
render={
|
|
136
|
+
<span
|
|
137
|
+
ref={triggerRef}
|
|
138
|
+
onFocus={onFocus}
|
|
139
|
+
onBlur={onBlur}
|
|
140
|
+
data-qa-popout-isopen={open ? "true" : "false"}
|
|
141
|
+
data-qa-tooltip={id}
|
|
142
|
+
{...(qa || {})}
|
|
143
|
+
style={
|
|
144
|
+
truncated
|
|
145
|
+
? {
|
|
146
|
+
overflow: "hidden",
|
|
147
|
+
textOverflow: "ellipsis",
|
|
148
|
+
whiteSpace: "nowrap",
|
|
149
|
+
display: "inline-flex",
|
|
150
|
+
}
|
|
151
|
+
: undefined
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
}
|
|
178
155
|
>
|
|
179
156
|
{React.isValidElement(children)
|
|
180
157
|
? React.cloneElement(children as React.ReactElement, {
|
|
181
|
-
//** There may be cases where the Tooltip's child needs to properly describe its role as expanding a drawer, in which case that property takes priority */
|
|
182
158
|
"aria-expanded": hasAttribute(children, "aria-expanded")
|
|
183
159
|
? children.props["aria-expanded"]
|
|
184
160
|
: undefined,
|
|
185
|
-
"aria-describedby":
|
|
161
|
+
"aria-describedby": open ? id : undefined,
|
|
186
162
|
})
|
|
187
163
|
: children}
|
|
188
|
-
</
|
|
189
|
-
|
|
164
|
+
</BaseTooltip.Trigger>
|
|
165
|
+
<BaseTooltip.Portal container={portalContainer}>
|
|
166
|
+
<BaseTooltip.Positioner
|
|
167
|
+
side={side}
|
|
168
|
+
align={align}
|
|
169
|
+
sideOffset={4}
|
|
170
|
+
style={{ zIndex }}
|
|
171
|
+
>
|
|
172
|
+
<BaseTooltip.Popup
|
|
173
|
+
id={id}
|
|
174
|
+
render={
|
|
175
|
+
<StyledTooltipContent
|
|
176
|
+
role="tooltip"
|
|
177
|
+
appearance={defaultAppearance}
|
|
178
|
+
borderRadius={defaultAppearance === "box" ? 500 : "5000em"}
|
|
179
|
+
px={400}
|
|
180
|
+
py={defaultAppearance === "box" ? 400 : 200}
|
|
181
|
+
m={200}
|
|
182
|
+
color="text.body"
|
|
183
|
+
bg="container.background.base"
|
|
184
|
+
boxShadow="medium"
|
|
185
|
+
border={500}
|
|
186
|
+
borderColor="container.border.base"
|
|
187
|
+
tabIndex={legacyMouseInteraction ? 0 : undefined}
|
|
188
|
+
{...rest}
|
|
189
|
+
/>
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
{content}
|
|
193
|
+
</BaseTooltip.Popup>
|
|
194
|
+
</BaseTooltip.Positioner>
|
|
195
|
+
</BaseTooltip.Portal>
|
|
196
|
+
</BaseTooltip.Root>
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return hasWrapperProps ? (
|
|
200
|
+
<Box {...mergedWrapperProps}>{tooltipTree}</Box>
|
|
201
|
+
) : (
|
|
202
|
+
tooltipTree
|
|
190
203
|
);
|
|
191
204
|
};
|
|
192
205
|
|
package/src/TooltipTypes.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type { TypePopoutProps } from "@sproutsocial/seeds-react-popout";
|
|
3
2
|
import type { TypeBoxProps } from "@sproutsocial/seeds-react-box";
|
|
4
3
|
|
|
4
|
+
export type TypeTooltipPlacement =
|
|
5
|
+
| "auto"
|
|
6
|
+
| "auto-start"
|
|
7
|
+
| "auto-end"
|
|
8
|
+
| "top"
|
|
9
|
+
| "top-start"
|
|
10
|
+
| "top-end"
|
|
11
|
+
| "bottom"
|
|
12
|
+
| "bottom-start"
|
|
13
|
+
| "bottom-end"
|
|
14
|
+
| "left"
|
|
15
|
+
| "left-start"
|
|
16
|
+
| "left-end"
|
|
17
|
+
| "right"
|
|
18
|
+
| "right-start"
|
|
19
|
+
| "right-end";
|
|
20
|
+
|
|
5
21
|
export interface TypeTooltipProps
|
|
6
22
|
extends Omit<
|
|
7
23
|
TypeBoxProps,
|
|
@@ -13,8 +29,14 @@ export interface TypeTooltipProps
|
|
|
13
29
|
/** The content to be displayed within the tooltip. If there is no content, just the children are rendered */
|
|
14
30
|
content: React.ReactNode;
|
|
15
31
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The placement of the tooltip in relation to the children.
|
|
34
|
+
*
|
|
35
|
+
* Note: `auto`, `auto-start`, and `auto-end` resolve to the `top` side (with
|
|
36
|
+
* the requested alignment) and rely on base-ui's runtime collision flipping,
|
|
37
|
+
* rather than the legacy Popper.js best-initial-side selection.
|
|
38
|
+
*/
|
|
39
|
+
placement?: TypeTooltipPlacement;
|
|
18
40
|
|
|
19
41
|
/** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */
|
|
20
42
|
enterDelay?: number;
|
|
@@ -33,8 +55,13 @@ export interface TypeTooltipProps
|
|
|
33
55
|
qa?: object;
|
|
34
56
|
zIndex?: number;
|
|
35
57
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Use `triggerProps` instead. The Tooltip no longer uses Popout internally. This prop is accepted and merged with triggerProps
|
|
60
|
+
*/
|
|
61
|
+
popoutProps?: Record<string, unknown>;
|
|
62
|
+
|
|
63
|
+
/** Props spread onto an outer wrapper Box rendered around the tooltip. Use this to apply layout styles (e.g. `display`, `width`) or other Box props to the element that wraps the tooltip's children. */
|
|
64
|
+
triggerProps?: TypeBoxProps;
|
|
38
65
|
|
|
39
66
|
/** Truncates text into a single line with ellipsis */
|
|
40
67
|
truncated?: boolean;
|
|
@@ -42,7 +69,6 @@ export interface TypeTooltipProps
|
|
|
42
69
|
ariaProps?: Record<string, string>;
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
46
72
|
export interface TypeTooltipContent
|
|
47
73
|
extends Pick<TypeTooltipProps, "appearance" | "children"> {
|
|
48
74
|
onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|