@sproutsocial/seeds-react-tooltip 1.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/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +7 -0
- package/dist/esm/index.js +173 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +50 -0
- package/src/Tooltip.stories.tsx +129 -0
- package/src/Tooltip.tsx +180 -0
- package/src/TooltipTypes.ts +41 -0
- package/src/__tests__/Tooltip.test.tsx +194 -0
- package/src/__tests__/Tooltip.typetest.tsx +27 -0
- package/src/index.ts +5 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +11 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
package/.eslintignore
ADDED
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
yarn run v1.22.22
|
|
2
|
+
$ tsup --dts
|
|
3
|
+
CLI Building entry: src/index.ts
|
|
4
|
+
CLI Using tsconfig: tsconfig.json
|
|
5
|
+
CLI tsup v8.0.2
|
|
6
|
+
CLI Using tsup config: /home/runner/work/seeds/seeds/seeds-react/seeds-react-tooltip/tsup.config.ts
|
|
7
|
+
CLI Target: es2022
|
|
8
|
+
CLI Cleaning output folder
|
|
9
|
+
CJS Build start
|
|
10
|
+
ESM Build start
|
|
11
|
+
ESM dist/esm/index.js 4.49 KB
|
|
12
|
+
ESM dist/esm/index.js.map 10.17 KB
|
|
13
|
+
ESM ⚡️ Build success in 146ms
|
|
14
|
+
CJS dist/index.js 6.47 KB
|
|
15
|
+
CJS dist/index.js.map 10.28 KB
|
|
16
|
+
CJS ⚡️ Build success in 151ms
|
|
17
|
+
DTS Build start
|
|
18
|
+
DTS ⚡️ Build success in 15731ms
|
|
19
|
+
DTS dist/index.d.ts 1.91 KB
|
|
20
|
+
DTS dist/index.d.mts 1.91 KB
|
|
21
|
+
Done in 20.16s.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// src/Tooltip.tsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import MOTION from "@sproutsocial/seeds-motion/unitless";
|
|
5
|
+
import Popout from "@sproutsocial/seeds-react-popout";
|
|
6
|
+
|
|
7
|
+
// src/styles.ts
|
|
8
|
+
import styled from "styled-components";
|
|
9
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
10
|
+
var StyledTooltipContent = styled(Box)`
|
|
11
|
+
font-family: ${(props) => props.theme.fontFamily};
|
|
12
|
+
${(props) => props.theme.typography[200]}
|
|
13
|
+
text-align: ${(props) => props.appearance === "box" ? "left" : "center"};
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
// src/Tooltip.tsx
|
|
17
|
+
import { jsx } from "react/jsx-runtime";
|
|
18
|
+
var idCounter = 0;
|
|
19
|
+
var hasAttribute = (child, attribute) => {
|
|
20
|
+
return React.isValidElement(child) && child.props[attribute] !== void 0;
|
|
21
|
+
};
|
|
22
|
+
var TooltipBubble = ({
|
|
23
|
+
appearance = "pill",
|
|
24
|
+
children,
|
|
25
|
+
onFocus,
|
|
26
|
+
onBlur,
|
|
27
|
+
...rest
|
|
28
|
+
}) => {
|
|
29
|
+
const handleFocus = (e) => {
|
|
30
|
+
onFocus(e);
|
|
31
|
+
};
|
|
32
|
+
const handleBlur = (e) => {
|
|
33
|
+
onBlur(e);
|
|
34
|
+
};
|
|
35
|
+
return /* @__PURE__ */ jsx(
|
|
36
|
+
StyledTooltipContent,
|
|
37
|
+
{
|
|
38
|
+
role: "tooltip",
|
|
39
|
+
appearance,
|
|
40
|
+
borderRadius: appearance === "box" ? 500 : "5000em",
|
|
41
|
+
px: 400,
|
|
42
|
+
py: appearance === "box" ? 400 : 200,
|
|
43
|
+
m: 200,
|
|
44
|
+
color: "text.body",
|
|
45
|
+
bg: "container.background.base",
|
|
46
|
+
boxShadow: "medium",
|
|
47
|
+
border: 500,
|
|
48
|
+
borderColor: "container.border.base",
|
|
49
|
+
onFocus: handleFocus,
|
|
50
|
+
onBlur: handleBlur,
|
|
51
|
+
onMouseEnter: handleFocus,
|
|
52
|
+
onMouseLeave: handleBlur,
|
|
53
|
+
tabIndex: 0,
|
|
54
|
+
...rest,
|
|
55
|
+
children
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
var Tooltip = ({
|
|
60
|
+
content,
|
|
61
|
+
children,
|
|
62
|
+
enterDelay = MOTION.MOTION_DURATION_FAST * 1e3,
|
|
63
|
+
placement = "auto",
|
|
64
|
+
appearance,
|
|
65
|
+
zIndex = 7,
|
|
66
|
+
qa,
|
|
67
|
+
popoutProps,
|
|
68
|
+
truncated = false,
|
|
69
|
+
onFocus,
|
|
70
|
+
onBlur,
|
|
71
|
+
...rest
|
|
72
|
+
}) => {
|
|
73
|
+
const [shouldShow, setShouldShow] = useState(false);
|
|
74
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
75
|
+
const [id] = useState(`Racine-tooltip-${idCounter++}`);
|
|
76
|
+
const isInvalidContent = content === null || content === void 0;
|
|
77
|
+
const show = (e) => {
|
|
78
|
+
onFocus?.(e);
|
|
79
|
+
setShouldShow(true);
|
|
80
|
+
};
|
|
81
|
+
const hide = (e) => {
|
|
82
|
+
onBlur?.(e);
|
|
83
|
+
setShouldShow(false);
|
|
84
|
+
};
|
|
85
|
+
const exitDelay = MOTION.MOTION_DURATION_FAST * 1e3;
|
|
86
|
+
const defaultAppearance = appearance || (typeof content === "object" ? "box" : "pill");
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const documentBody = document.body;
|
|
89
|
+
let timeout;
|
|
90
|
+
const onEsc = (e) => {
|
|
91
|
+
if (["Escape", "Esc"].includes(e.key)) {
|
|
92
|
+
setIsOpen(false);
|
|
93
|
+
setShouldShow(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
if (shouldShow) {
|
|
97
|
+
timeout = setTimeout(() => setIsOpen(true), enterDelay);
|
|
98
|
+
} else {
|
|
99
|
+
timeout = setTimeout(() => {
|
|
100
|
+
setIsOpen(false);
|
|
101
|
+
}, exitDelay);
|
|
102
|
+
}
|
|
103
|
+
if (isOpen) {
|
|
104
|
+
documentBody.addEventListener("keydown", onEsc, { capture: true });
|
|
105
|
+
}
|
|
106
|
+
return () => {
|
|
107
|
+
documentBody.removeEventListener("keydown", onEsc, { capture: true });
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
};
|
|
110
|
+
}, [isOpen, setShouldShow, shouldShow, enterDelay, exitDelay]);
|
|
111
|
+
const TooltipContent = () => /* @__PURE__ */ jsx(
|
|
112
|
+
TooltipBubble,
|
|
113
|
+
{
|
|
114
|
+
appearance: defaultAppearance,
|
|
115
|
+
onFocus: show,
|
|
116
|
+
onBlur: hide,
|
|
117
|
+
"aria-expanded": isOpen,
|
|
118
|
+
id,
|
|
119
|
+
...rest,
|
|
120
|
+
children: content
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
return /* @__PURE__ */ jsx(
|
|
124
|
+
Popout,
|
|
125
|
+
{
|
|
126
|
+
content: !isInvalidContent ? TooltipContent : void 0,
|
|
127
|
+
isOpen,
|
|
128
|
+
placement,
|
|
129
|
+
qa: {
|
|
130
|
+
"data-qa-tooltip": id,
|
|
131
|
+
...qa
|
|
132
|
+
},
|
|
133
|
+
id: id + "-wrapper",
|
|
134
|
+
focusOnContent: false,
|
|
135
|
+
zIndex,
|
|
136
|
+
"aria-haspopup": "false",
|
|
137
|
+
display: truncated ? "flex" : void 0,
|
|
138
|
+
disableWrapperAria: true,
|
|
139
|
+
...popoutProps,
|
|
140
|
+
children: /* @__PURE__ */ jsx(
|
|
141
|
+
"span",
|
|
142
|
+
{
|
|
143
|
+
onBlur: hide,
|
|
144
|
+
onFocus: show,
|
|
145
|
+
onMouseEnter: show,
|
|
146
|
+
onMouseLeave: hide,
|
|
147
|
+
style: truncated ? {
|
|
148
|
+
overflow: "hidden",
|
|
149
|
+
textOverflow: "ellipsis",
|
|
150
|
+
whiteSpace: "nowrap"
|
|
151
|
+
} : {},
|
|
152
|
+
children: React.isValidElement(children) ? React.cloneElement(children, {
|
|
153
|
+
//** There may be cases where the Tooltip's child needs to properly describe its role as expanding a drawer, in which case that propery takes priority */
|
|
154
|
+
"aria-expanded": hasAttribute(children, "aria-expanded") ? children.props["aria-expanded"] : isOpen,
|
|
155
|
+
"aria-describedby": id
|
|
156
|
+
}) : children
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
var Tooltip_default = Tooltip;
|
|
163
|
+
|
|
164
|
+
// src/TooltipTypes.ts
|
|
165
|
+
import "react";
|
|
166
|
+
|
|
167
|
+
// src/index.ts
|
|
168
|
+
var src_default = Tooltip_default;
|
|
169
|
+
export {
|
|
170
|
+
Tooltip_default as Tooltip,
|
|
171
|
+
src_default as default
|
|
172
|
+
};
|
|
173
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Tooltip.tsx","../../src/styles.ts","../../src/TooltipTypes.ts","../../src/index.ts"],"sourcesContent":["import * as React from \"react\";\nimport { useState, useEffect } from \"react\";\nimport MOTION from \"@sproutsocial/seeds-motion/unitless\";\nimport Popout from \"@sproutsocial/seeds-react-popout\";\nimport { StyledTooltipContent } from \"./styles\";\nimport type { TypeTooltipProps, TypeTooltipContent } from \"./TooltipTypes\";\n\nlet idCounter = 0;\n\nconst hasAttribute = (child: React.ReactNode, attribute: string) => {\n return React.isValidElement(child) && child.props[attribute] !== undefined;\n};\n\n/** Tooltip Styled Popout wrapper for handling events */\nconst TooltipBubble = ({\n appearance = \"pill\",\n children,\n onFocus,\n onBlur,\n ...rest\n}: TypeTooltipContent) => {\n // @ts-ignore Will fix during refactor\n const handleFocus = (e) => {\n onFocus(e);\n };\n // @ts-ignore Will fix during refactor\n const handleBlur = (e) => {\n onBlur(e);\n };\n return (\n <StyledTooltipContent\n role=\"tooltip\"\n appearance={appearance}\n borderRadius={appearance === \"box\" ? 500 : \"5000em\"}\n px={400}\n py={appearance === \"box\" ? 400 : 200}\n m={200}\n color=\"text.body\"\n bg=\"container.background.base\"\n boxShadow=\"medium\"\n border={500}\n borderColor=\"container.border.base\"\n onFocus={handleFocus}\n onBlur={handleBlur}\n onMouseEnter={handleFocus}\n onMouseLeave={handleBlur}\n tabIndex={0}\n {...rest}\n >\n {children}\n </StyledTooltipContent>\n );\n};\n\n/** Core component */\nconst Tooltip = ({\n content,\n children,\n enterDelay = MOTION.MOTION_DURATION_FAST * 1000,\n placement = \"auto\",\n appearance,\n zIndex = 7,\n qa,\n popoutProps,\n truncated = false,\n onFocus,\n onBlur,\n ...rest\n}: TypeTooltipProps) => {\n const [shouldShow, setShouldShow] = useState(false);\n const [isOpen, setIsOpen] = useState(false);\n const [id] = useState(`Racine-tooltip-${idCounter++}`);\n const isInvalidContent = content === null || content === undefined;\n\n // @ts-ignore Will fix during refactor\n const show = (e) => {\n onFocus?.(e);\n setShouldShow(true);\n };\n // @ts-ignore Will fix during refactor\n const hide = (e) => {\n onBlur?.(e);\n setShouldShow(false);\n };\n\n const exitDelay = MOTION.MOTION_DURATION_FAST * 1000;\n const defaultAppearance =\n appearance || (typeof content === \"object\" ? \"box\" : \"pill\");\n\n /** Handles all the logic around whether to display/not display */\n useEffect(() => {\n const documentBody = document.body;\n let timeout;\n const onEsc = (e: KeyboardEvent): void => {\n // older browsers use \"Esc\"\n if ([\"Escape\", \"Esc\"].includes(e.key)) {\n setIsOpen(false);\n setShouldShow(false);\n }\n };\n\n if (shouldShow) {\n timeout = setTimeout(() => setIsOpen(true), enterDelay);\n } else {\n timeout = setTimeout(() => {\n setIsOpen(false);\n }, exitDelay);\n }\n\n // We only want listeners from the tooltip if its open in the first place\n if (isOpen) {\n documentBody.addEventListener(\"keydown\", onEsc, { capture: true });\n }\n return () => {\n documentBody.removeEventListener(\"keydown\", onEsc, { capture: true });\n clearTimeout(timeout);\n };\n }, [isOpen, setShouldShow, shouldShow, enterDelay, exitDelay]);\n\n /** The wrapped content of whats inside the Tooltip */\n const TooltipContent = () => (\n <TooltipBubble\n appearance={defaultAppearance}\n onFocus={show}\n onBlur={hide}\n aria-expanded={isOpen}\n id={id}\n {...rest}\n >\n {content}\n </TooltipBubble>\n );\n\n return (\n <Popout\n content={!isInvalidContent ? TooltipContent : undefined}\n isOpen={isOpen}\n placement={placement}\n qa={{\n \"data-qa-tooltip\": id,\n ...qa,\n }}\n id={id + \"-wrapper\"}\n focusOnContent={false}\n zIndex={zIndex}\n aria-haspopup=\"false\"\n display={truncated ? \"flex\" : undefined}\n disableWrapperAria={true} // required so that the child span doesnt take in redundant aria props\n {...popoutProps}\n >\n <span\n onBlur={hide}\n onFocus={show}\n onMouseEnter={show}\n onMouseLeave={hide}\n style={\n truncated\n ? {\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n }\n : {}\n }\n >\n {React.isValidElement(children)\n ? React.cloneElement(children as React.ReactElement, {\n //** There may be cases where the Tooltip's child needs to properly describe its role as expanding a drawer, in which case that propery takes priority */\n \"aria-expanded\": hasAttribute(children, \"aria-expanded\")\n ? children.props[\"aria-expanded\"]\n : isOpen,\n \"aria-describedby\": id,\n })\n : children}\n </span>\n </Popout>\n );\n};\n\nexport default Tooltip;\n","import styled from \"styled-components\";\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport type { TypeTooltipContent } from \"./TooltipTypes\";\n\nexport const StyledTooltipContent = styled(Box)<\n Pick<TypeTooltipContent, \"appearance\">\n>`\n font-family: ${(props) => props.theme.fontFamily};\n ${(props) => props.theme.typography[200]}\n text-align: ${(props) => (props.appearance === \"box\" ? \"left\" : \"center\")};\n`;\n","import * as React from \"react\";\nimport type { TypePopoutProps } from \"@sproutsocial/seeds-react-popout\";\nimport type { TypeBoxProps } from \"@sproutsocial/seeds-react-box\";\n\nexport interface TypeTooltipProps\n extends Omit<\n TypeBoxProps,\n \"children\" | \"content\" | \"onMouseEnter\" | \"onMouseLeave\"\n > {\n /** The content that the tooltip should be attached to. Hovering or focusing this element will cause the tooltip to appear */\n children: React.ReactNode;\n\n /** The content to be displayed within the tooltip. If there is no content, just the children are rendered */\n content: React.ReactNode;\n\n /** The placement of the tooltip in relation to the children */\n placement?: TypePopoutProps[\"placement\"];\n\n /** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */\n enterDelay?: number;\n\n /** Used to override the appearance of the Tooltip content. By default, strings will have the 'pill' appearance, and more complex content will have the 'box' appearance. You can change those defaults by setting this prop. */\n appearance?: \"pill\" | \"box\";\n qa?: object;\n zIndex?: number;\n\n /** Props to be spread onto the underlying Popout component */\n popoutProps?: Partial<TypePopoutProps>;\n\n /** Truncates text into a single line with ellipsis */\n truncated?: boolean;\n\n ariaProps?: Record<string, string>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface TypeTooltipContent\n extends Pick<TypeTooltipProps, \"appearance\" | \"children\"> {\n onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;\n onBlur: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;\n}\n","import Tooltip from \"./Tooltip\";\n\nexport default Tooltip;\nexport { Tooltip };\nexport * from \"./TooltipTypes\";\n"],"mappings":"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","names":[]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { TypePopoutProps } from '@sproutsocial/seeds-react-popout';
|
|
4
|
+
import { TypeBoxProps } from '@sproutsocial/seeds-react-box';
|
|
5
|
+
|
|
6
|
+
interface TypeTooltipProps extends Omit<TypeBoxProps, "children" | "content" | "onMouseEnter" | "onMouseLeave"> {
|
|
7
|
+
/** The content that the tooltip should be attached to. Hovering or focusing this element will cause the tooltip to appear */
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/** The content to be displayed within the tooltip. If there is no content, just the children are rendered */
|
|
10
|
+
content: React.ReactNode;
|
|
11
|
+
/** The placement of the tooltip in relation to the children */
|
|
12
|
+
placement?: TypePopoutProps["placement"];
|
|
13
|
+
/** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */
|
|
14
|
+
enterDelay?: number;
|
|
15
|
+
/** Used to override the appearance of the Tooltip content. By default, strings will have the 'pill' appearance, and more complex content will have the 'box' appearance. You can change those defaults by setting this prop. */
|
|
16
|
+
appearance?: "pill" | "box";
|
|
17
|
+
qa?: object;
|
|
18
|
+
zIndex?: number;
|
|
19
|
+
/** Props to be spread onto the underlying Popout component */
|
|
20
|
+
popoutProps?: Partial<TypePopoutProps>;
|
|
21
|
+
/** Truncates text into a single line with ellipsis */
|
|
22
|
+
truncated?: boolean;
|
|
23
|
+
ariaProps?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
interface TypeTooltipContent extends Pick<TypeTooltipProps, "appearance" | "children"> {
|
|
26
|
+
onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|
|
27
|
+
onBlur: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Core component */
|
|
31
|
+
declare const Tooltip: ({ content, children, enterDelay, placement, appearance, zIndex, qa, popoutProps, truncated, onFocus, onBlur, ...rest }: TypeTooltipProps) => react_jsx_runtime.JSX.Element;
|
|
32
|
+
|
|
33
|
+
export { Tooltip, type TypeTooltipContent, type TypeTooltipProps, Tooltip as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { TypePopoutProps } from '@sproutsocial/seeds-react-popout';
|
|
4
|
+
import { TypeBoxProps } from '@sproutsocial/seeds-react-box';
|
|
5
|
+
|
|
6
|
+
interface TypeTooltipProps extends Omit<TypeBoxProps, "children" | "content" | "onMouseEnter" | "onMouseLeave"> {
|
|
7
|
+
/** The content that the tooltip should be attached to. Hovering or focusing this element will cause the tooltip to appear */
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/** The content to be displayed within the tooltip. If there is no content, just the children are rendered */
|
|
10
|
+
content: React.ReactNode;
|
|
11
|
+
/** The placement of the tooltip in relation to the children */
|
|
12
|
+
placement?: TypePopoutProps["placement"];
|
|
13
|
+
/** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */
|
|
14
|
+
enterDelay?: number;
|
|
15
|
+
/** Used to override the appearance of the Tooltip content. By default, strings will have the 'pill' appearance, and more complex content will have the 'box' appearance. You can change those defaults by setting this prop. */
|
|
16
|
+
appearance?: "pill" | "box";
|
|
17
|
+
qa?: object;
|
|
18
|
+
zIndex?: number;
|
|
19
|
+
/** Props to be spread onto the underlying Popout component */
|
|
20
|
+
popoutProps?: Partial<TypePopoutProps>;
|
|
21
|
+
/** Truncates text into a single line with ellipsis */
|
|
22
|
+
truncated?: boolean;
|
|
23
|
+
ariaProps?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
interface TypeTooltipContent extends Pick<TypeTooltipProps, "appearance" | "children"> {
|
|
26
|
+
onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|
|
27
|
+
onBlur: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Core component */
|
|
31
|
+
declare const Tooltip: ({ content, children, enterDelay, placement, appearance, zIndex, qa, popoutProps, truncated, onFocus, onBlur, ...rest }: TypeTooltipProps) => react_jsx_runtime.JSX.Element;
|
|
32
|
+
|
|
33
|
+
export { Tooltip, type TypeTooltipContent, type TypeTooltipProps, Tooltip as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Tooltip: () => Tooltip_default,
|
|
34
|
+
default: () => src_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
|
|
38
|
+
// src/Tooltip.tsx
|
|
39
|
+
var React = __toESM(require("react"));
|
|
40
|
+
var import_react = require("react");
|
|
41
|
+
var import_unitless = __toESM(require("@sproutsocial/seeds-motion/unitless"));
|
|
42
|
+
var import_seeds_react_popout = __toESM(require("@sproutsocial/seeds-react-popout"));
|
|
43
|
+
|
|
44
|
+
// src/styles.ts
|
|
45
|
+
var import_styled_components = __toESM(require("styled-components"));
|
|
46
|
+
var import_seeds_react_box = __toESM(require("@sproutsocial/seeds-react-box"));
|
|
47
|
+
var StyledTooltipContent = (0, import_styled_components.default)(import_seeds_react_box.default)`
|
|
48
|
+
font-family: ${(props) => props.theme.fontFamily};
|
|
49
|
+
${(props) => props.theme.typography[200]}
|
|
50
|
+
text-align: ${(props) => props.appearance === "box" ? "left" : "center"};
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
// src/Tooltip.tsx
|
|
54
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
55
|
+
var idCounter = 0;
|
|
56
|
+
var hasAttribute = (child, attribute) => {
|
|
57
|
+
return React.isValidElement(child) && child.props[attribute] !== void 0;
|
|
58
|
+
};
|
|
59
|
+
var TooltipBubble = ({
|
|
60
|
+
appearance = "pill",
|
|
61
|
+
children,
|
|
62
|
+
onFocus,
|
|
63
|
+
onBlur,
|
|
64
|
+
...rest
|
|
65
|
+
}) => {
|
|
66
|
+
const handleFocus = (e) => {
|
|
67
|
+
onFocus(e);
|
|
68
|
+
};
|
|
69
|
+
const handleBlur = (e) => {
|
|
70
|
+
onBlur(e);
|
|
71
|
+
};
|
|
72
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
73
|
+
StyledTooltipContent,
|
|
74
|
+
{
|
|
75
|
+
role: "tooltip",
|
|
76
|
+
appearance,
|
|
77
|
+
borderRadius: appearance === "box" ? 500 : "5000em",
|
|
78
|
+
px: 400,
|
|
79
|
+
py: appearance === "box" ? 400 : 200,
|
|
80
|
+
m: 200,
|
|
81
|
+
color: "text.body",
|
|
82
|
+
bg: "container.background.base",
|
|
83
|
+
boxShadow: "medium",
|
|
84
|
+
border: 500,
|
|
85
|
+
borderColor: "container.border.base",
|
|
86
|
+
onFocus: handleFocus,
|
|
87
|
+
onBlur: handleBlur,
|
|
88
|
+
onMouseEnter: handleFocus,
|
|
89
|
+
onMouseLeave: handleBlur,
|
|
90
|
+
tabIndex: 0,
|
|
91
|
+
...rest,
|
|
92
|
+
children
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
var Tooltip = ({
|
|
97
|
+
content,
|
|
98
|
+
children,
|
|
99
|
+
enterDelay = import_unitless.default.MOTION_DURATION_FAST * 1e3,
|
|
100
|
+
placement = "auto",
|
|
101
|
+
appearance,
|
|
102
|
+
zIndex = 7,
|
|
103
|
+
qa,
|
|
104
|
+
popoutProps,
|
|
105
|
+
truncated = false,
|
|
106
|
+
onFocus,
|
|
107
|
+
onBlur,
|
|
108
|
+
...rest
|
|
109
|
+
}) => {
|
|
110
|
+
const [shouldShow, setShouldShow] = (0, import_react.useState)(false);
|
|
111
|
+
const [isOpen, setIsOpen] = (0, import_react.useState)(false);
|
|
112
|
+
const [id] = (0, import_react.useState)(`Racine-tooltip-${idCounter++}`);
|
|
113
|
+
const isInvalidContent = content === null || content === void 0;
|
|
114
|
+
const show = (e) => {
|
|
115
|
+
onFocus?.(e);
|
|
116
|
+
setShouldShow(true);
|
|
117
|
+
};
|
|
118
|
+
const hide = (e) => {
|
|
119
|
+
onBlur?.(e);
|
|
120
|
+
setShouldShow(false);
|
|
121
|
+
};
|
|
122
|
+
const exitDelay = import_unitless.default.MOTION_DURATION_FAST * 1e3;
|
|
123
|
+
const defaultAppearance = appearance || (typeof content === "object" ? "box" : "pill");
|
|
124
|
+
(0, import_react.useEffect)(() => {
|
|
125
|
+
const documentBody = document.body;
|
|
126
|
+
let timeout;
|
|
127
|
+
const onEsc = (e) => {
|
|
128
|
+
if (["Escape", "Esc"].includes(e.key)) {
|
|
129
|
+
setIsOpen(false);
|
|
130
|
+
setShouldShow(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
if (shouldShow) {
|
|
134
|
+
timeout = setTimeout(() => setIsOpen(true), enterDelay);
|
|
135
|
+
} else {
|
|
136
|
+
timeout = setTimeout(() => {
|
|
137
|
+
setIsOpen(false);
|
|
138
|
+
}, exitDelay);
|
|
139
|
+
}
|
|
140
|
+
if (isOpen) {
|
|
141
|
+
documentBody.addEventListener("keydown", onEsc, { capture: true });
|
|
142
|
+
}
|
|
143
|
+
return () => {
|
|
144
|
+
documentBody.removeEventListener("keydown", onEsc, { capture: true });
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
};
|
|
147
|
+
}, [isOpen, setShouldShow, shouldShow, enterDelay, exitDelay]);
|
|
148
|
+
const TooltipContent = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
149
|
+
TooltipBubble,
|
|
150
|
+
{
|
|
151
|
+
appearance: defaultAppearance,
|
|
152
|
+
onFocus: show,
|
|
153
|
+
onBlur: hide,
|
|
154
|
+
"aria-expanded": isOpen,
|
|
155
|
+
id,
|
|
156
|
+
...rest,
|
|
157
|
+
children: content
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
161
|
+
import_seeds_react_popout.default,
|
|
162
|
+
{
|
|
163
|
+
content: !isInvalidContent ? TooltipContent : void 0,
|
|
164
|
+
isOpen,
|
|
165
|
+
placement,
|
|
166
|
+
qa: {
|
|
167
|
+
"data-qa-tooltip": id,
|
|
168
|
+
...qa
|
|
169
|
+
},
|
|
170
|
+
id: id + "-wrapper",
|
|
171
|
+
focusOnContent: false,
|
|
172
|
+
zIndex,
|
|
173
|
+
"aria-haspopup": "false",
|
|
174
|
+
display: truncated ? "flex" : void 0,
|
|
175
|
+
disableWrapperAria: true,
|
|
176
|
+
...popoutProps,
|
|
177
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
178
|
+
"span",
|
|
179
|
+
{
|
|
180
|
+
onBlur: hide,
|
|
181
|
+
onFocus: show,
|
|
182
|
+
onMouseEnter: show,
|
|
183
|
+
onMouseLeave: hide,
|
|
184
|
+
style: truncated ? {
|
|
185
|
+
overflow: "hidden",
|
|
186
|
+
textOverflow: "ellipsis",
|
|
187
|
+
whiteSpace: "nowrap"
|
|
188
|
+
} : {},
|
|
189
|
+
children: React.isValidElement(children) ? React.cloneElement(children, {
|
|
190
|
+
//** There may be cases where the Tooltip's child needs to properly describe its role as expanding a drawer, in which case that propery takes priority */
|
|
191
|
+
"aria-expanded": hasAttribute(children, "aria-expanded") ? children.props["aria-expanded"] : isOpen,
|
|
192
|
+
"aria-describedby": id
|
|
193
|
+
}) : children
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
var Tooltip_default = Tooltip;
|
|
200
|
+
|
|
201
|
+
// src/TooltipTypes.ts
|
|
202
|
+
var React2 = require("react");
|
|
203
|
+
|
|
204
|
+
// src/index.ts
|
|
205
|
+
var src_default = Tooltip_default;
|
|
206
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
207
|
+
0 && (module.exports = {
|
|
208
|
+
Tooltip
|
|
209
|
+
});
|
|
210
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Tooltip.tsx","../src/styles.ts","../src/TooltipTypes.ts"],"sourcesContent":["import Tooltip from \"./Tooltip\";\n\nexport default Tooltip;\nexport { Tooltip };\nexport * from \"./TooltipTypes\";\n","import * as React from \"react\";\nimport { useState, useEffect } from \"react\";\nimport MOTION from \"@sproutsocial/seeds-motion/unitless\";\nimport Popout from \"@sproutsocial/seeds-react-popout\";\nimport { StyledTooltipContent } from \"./styles\";\nimport type { TypeTooltipProps, TypeTooltipContent } from \"./TooltipTypes\";\n\nlet idCounter = 0;\n\nconst hasAttribute = (child: React.ReactNode, attribute: string) => {\n return React.isValidElement(child) && child.props[attribute] !== undefined;\n};\n\n/** Tooltip Styled Popout wrapper for handling events */\nconst TooltipBubble = ({\n appearance = \"pill\",\n children,\n onFocus,\n onBlur,\n ...rest\n}: TypeTooltipContent) => {\n // @ts-ignore Will fix during refactor\n const handleFocus = (e) => {\n onFocus(e);\n };\n // @ts-ignore Will fix during refactor\n const handleBlur = (e) => {\n onBlur(e);\n };\n return (\n <StyledTooltipContent\n role=\"tooltip\"\n appearance={appearance}\n borderRadius={appearance === \"box\" ? 500 : \"5000em\"}\n px={400}\n py={appearance === \"box\" ? 400 : 200}\n m={200}\n color=\"text.body\"\n bg=\"container.background.base\"\n boxShadow=\"medium\"\n border={500}\n borderColor=\"container.border.base\"\n onFocus={handleFocus}\n onBlur={handleBlur}\n onMouseEnter={handleFocus}\n onMouseLeave={handleBlur}\n tabIndex={0}\n {...rest}\n >\n {children}\n </StyledTooltipContent>\n );\n};\n\n/** Core component */\nconst Tooltip = ({\n content,\n children,\n enterDelay = MOTION.MOTION_DURATION_FAST * 1000,\n placement = \"auto\",\n appearance,\n zIndex = 7,\n qa,\n popoutProps,\n truncated = false,\n onFocus,\n onBlur,\n ...rest\n}: TypeTooltipProps) => {\n const [shouldShow, setShouldShow] = useState(false);\n const [isOpen, setIsOpen] = useState(false);\n const [id] = useState(`Racine-tooltip-${idCounter++}`);\n const isInvalidContent = content === null || content === undefined;\n\n // @ts-ignore Will fix during refactor\n const show = (e) => {\n onFocus?.(e);\n setShouldShow(true);\n };\n // @ts-ignore Will fix during refactor\n const hide = (e) => {\n onBlur?.(e);\n setShouldShow(false);\n };\n\n const exitDelay = MOTION.MOTION_DURATION_FAST * 1000;\n const defaultAppearance =\n appearance || (typeof content === \"object\" ? \"box\" : \"pill\");\n\n /** Handles all the logic around whether to display/not display */\n useEffect(() => {\n const documentBody = document.body;\n let timeout;\n const onEsc = (e: KeyboardEvent): void => {\n // older browsers use \"Esc\"\n if ([\"Escape\", \"Esc\"].includes(e.key)) {\n setIsOpen(false);\n setShouldShow(false);\n }\n };\n\n if (shouldShow) {\n timeout = setTimeout(() => setIsOpen(true), enterDelay);\n } else {\n timeout = setTimeout(() => {\n setIsOpen(false);\n }, exitDelay);\n }\n\n // We only want listeners from the tooltip if its open in the first place\n if (isOpen) {\n documentBody.addEventListener(\"keydown\", onEsc, { capture: true });\n }\n return () => {\n documentBody.removeEventListener(\"keydown\", onEsc, { capture: true });\n clearTimeout(timeout);\n };\n }, [isOpen, setShouldShow, shouldShow, enterDelay, exitDelay]);\n\n /** The wrapped content of whats inside the Tooltip */\n const TooltipContent = () => (\n <TooltipBubble\n appearance={defaultAppearance}\n onFocus={show}\n onBlur={hide}\n aria-expanded={isOpen}\n id={id}\n {...rest}\n >\n {content}\n </TooltipBubble>\n );\n\n return (\n <Popout\n content={!isInvalidContent ? TooltipContent : undefined}\n isOpen={isOpen}\n placement={placement}\n qa={{\n \"data-qa-tooltip\": id,\n ...qa,\n }}\n id={id + \"-wrapper\"}\n focusOnContent={false}\n zIndex={zIndex}\n aria-haspopup=\"false\"\n display={truncated ? \"flex\" : undefined}\n disableWrapperAria={true} // required so that the child span doesnt take in redundant aria props\n {...popoutProps}\n >\n <span\n onBlur={hide}\n onFocus={show}\n onMouseEnter={show}\n onMouseLeave={hide}\n style={\n truncated\n ? {\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n }\n : {}\n }\n >\n {React.isValidElement(children)\n ? React.cloneElement(children as React.ReactElement, {\n //** There may be cases where the Tooltip's child needs to properly describe its role as expanding a drawer, in which case that propery takes priority */\n \"aria-expanded\": hasAttribute(children, \"aria-expanded\")\n ? children.props[\"aria-expanded\"]\n : isOpen,\n \"aria-describedby\": id,\n })\n : children}\n </span>\n </Popout>\n );\n};\n\nexport default Tooltip;\n","import styled from \"styled-components\";\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport type { TypeTooltipContent } from \"./TooltipTypes\";\n\nexport const StyledTooltipContent = styled(Box)<\n Pick<TypeTooltipContent, \"appearance\">\n>`\n font-family: ${(props) => props.theme.fontFamily};\n ${(props) => props.theme.typography[200]}\n text-align: ${(props) => (props.appearance === \"box\" ? \"left\" : \"center\")};\n`;\n","import * as React from \"react\";\nimport type { TypePopoutProps } from \"@sproutsocial/seeds-react-popout\";\nimport type { TypeBoxProps } from \"@sproutsocial/seeds-react-box\";\n\nexport interface TypeTooltipProps\n extends Omit<\n TypeBoxProps,\n \"children\" | \"content\" | \"onMouseEnter\" | \"onMouseLeave\"\n > {\n /** The content that the tooltip should be attached to. Hovering or focusing this element will cause the tooltip to appear */\n children: React.ReactNode;\n\n /** The content to be displayed within the tooltip. If there is no content, just the children are rendered */\n content: React.ReactNode;\n\n /** The placement of the tooltip in relation to the children */\n placement?: TypePopoutProps[\"placement\"];\n\n /** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */\n enterDelay?: number;\n\n /** Used to override the appearance of the Tooltip content. By default, strings will have the 'pill' appearance, and more complex content will have the 'box' appearance. You can change those defaults by setting this prop. */\n appearance?: \"pill\" | \"box\";\n qa?: object;\n zIndex?: number;\n\n /** Props to be spread onto the underlying Popout component */\n popoutProps?: Partial<TypePopoutProps>;\n\n /** Truncates text into a single line with ellipsis */\n truncated?: boolean;\n\n ariaProps?: Record<string, string>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-interface\nexport interface TypeTooltipContent\n extends Pick<TypeTooltipProps, \"appearance\" | \"children\"> {\n onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;\n onBlur: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,YAAuB;AACvB,mBAAoC;AACpC,sBAAmB;AACnB,gCAAmB;;;ACHnB,+BAAmB;AACnB,6BAAgB;AAGT,IAAM,2BAAuB,yBAAAA,SAAO,uBAAAC,OAAG;AAAA,iBAG7B,CAAC,UAAU,MAAM,MAAM,UAAU;AAAA,IAC9C,CAAC,UAAU,MAAM,MAAM,WAAW,GAAG,CAAC;AAAA,gBAC1B,CAAC,UAAW,MAAM,eAAe,QAAQ,SAAS,QAAS;AAAA;;;ADqBvE;AAvBJ,IAAI,YAAY;AAEhB,IAAM,eAAe,CAAC,OAAwB,cAAsB;AAClE,SAAa,qBAAe,KAAK,KAAK,MAAM,MAAM,SAAS,MAAM;AACnE;AAGA,IAAM,gBAAgB,CAAC;AAAA,EACrB,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA0B;AAExB,QAAM,cAAc,CAAC,MAAM;AACzB,YAAQ,CAAC;AAAA,EACX;AAEA,QAAM,aAAa,CAAC,MAAM;AACxB,WAAO,CAAC;AAAA,EACV;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,cAAc,eAAe,QAAQ,MAAM;AAAA,MAC3C,IAAI;AAAA,MACJ,IAAI,eAAe,QAAQ,MAAM;AAAA,MACjC,GAAG;AAAA,MACH,OAAM;AAAA,MACN,IAAG;AAAA,MACH,WAAU;AAAA,MACV,QAAQ;AAAA,MACR,aAAY;AAAA,MACZ,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,cAAc;AAAA,MACd,UAAU;AAAA,MACT,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAGA,IAAM,UAAU,CAAC;AAAA,EACf;AAAA,EACA;AAAA,EACA,aAAa,gBAAAC,QAAO,uBAAuB;AAAA,EAC3C,YAAY;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAAwB;AACtB,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAS,KAAK;AAClD,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAS,KAAK;AAC1C,QAAM,CAAC,EAAE,QAAI,uBAAS,kBAAkB,WAAW,EAAE;AACrD,QAAM,mBAAmB,YAAY,QAAQ,YAAY;AAGzD,QAAM,OAAO,CAAC,MAAM;AAClB,cAAU,CAAC;AACX,kBAAc,IAAI;AAAA,EACpB;AAEA,QAAM,OAAO,CAAC,MAAM;AAClB,aAAS,CAAC;AACV,kBAAc,KAAK;AAAA,EACrB;AAEA,QAAM,YAAY,gBAAAA,QAAO,uBAAuB;AAChD,QAAM,oBACJ,eAAe,OAAO,YAAY,WAAW,QAAQ;AAGvD,8BAAU,MAAM;AACd,UAAM,eAAe,SAAS;AAC9B,QAAI;AACJ,UAAM,QAAQ,CAAC,MAA2B;AAExC,UAAI,CAAC,UAAU,KAAK,EAAE,SAAS,EAAE,GAAG,GAAG;AACrC,kBAAU,KAAK;AACf,sBAAc,KAAK;AAAA,MACrB;AAAA,IACF;AAEA,QAAI,YAAY;AACd,gBAAU,WAAW,MAAM,UAAU,IAAI,GAAG,UAAU;AAAA,IACxD,OAAO;AACL,gBAAU,WAAW,MAAM;AACzB,kBAAU,KAAK;AAAA,MACjB,GAAG,SAAS;AAAA,IACd;AAGA,QAAI,QAAQ;AACV,mBAAa,iBAAiB,WAAW,OAAO,EAAE,SAAS,KAAK,CAAC;AAAA,IACnE;AACA,WAAO,MAAM;AACX,mBAAa,oBAAoB,WAAW,OAAO,EAAE,SAAS,KAAK,CAAC;AACpE,mBAAa,OAAO;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,QAAQ,eAAe,YAAY,YAAY,SAAS,CAAC;AAG7D,QAAM,iBAAiB,MACrB;AAAA,IAAC;AAAA;AAAA,MACC,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,iBAAe;AAAA,MACf;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAGF,SACE;AAAA,IAAC,0BAAAC;AAAA,IAAA;AAAA,MACC,SAAS,CAAC,mBAAmB,iBAAiB;AAAA,MAC9C;AAAA,MACA;AAAA,MACA,IAAI;AAAA,QACF,mBAAmB;AAAA,QACnB,GAAG;AAAA,MACL;AAAA,MACA,IAAI,KAAK;AAAA,MACT,gBAAgB;AAAA,MAChB;AAAA,MACA,iBAAc;AAAA,MACd,SAAS,YAAY,SAAS;AAAA,MAC9B,oBAAoB;AAAA,MACnB,GAAG;AAAA,MAEJ;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,cAAc;AAAA,UACd,cAAc;AAAA,UACd,OACE,YACI;AAAA,YACE,UAAU;AAAA,YACV,cAAc;AAAA,YACd,YAAY;AAAA,UACd,IACA,CAAC;AAAA,UAGN,UAAM,qBAAe,QAAQ,IACpB,mBAAa,UAAgC;AAAA;AAAA,YAEjD,iBAAiB,aAAa,UAAU,eAAe,IACnD,SAAS,MAAM,eAAe,IAC9B;AAAA,YACJ,oBAAoB;AAAA,UACtB,CAAC,IACD;AAAA;AAAA,MACN;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,kBAAQ;;;AEnLf,IAAAC,SAAuB;;;AHEvB,IAAO,cAAQ;","names":["styled","Box","MOTION","Popout","React"]}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sproutsocial/seeds-react-tooltip",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seeds React Tooltip",
|
|
5
|
+
"author": "Sprout Social, Inc.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/esm/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup --dts",
|
|
12
|
+
"build:debug": "tsup --dts --metafile",
|
|
13
|
+
"dev": "tsup --watch --dts",
|
|
14
|
+
"clean": "rm -rf .turbo dist",
|
|
15
|
+
"clean:modules": "rm -rf node_modules",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch --coverage=false"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@sproutsocial/seeds-react-theme": "*",
|
|
22
|
+
"@sproutsocial/seeds-react-system-props": "*",
|
|
23
|
+
"@sproutsocial/seeds-react-popout": "*",
|
|
24
|
+
"@sproutsocial/seeds-react-box": "*",
|
|
25
|
+
"@sproutsocial/seeds-motion": "*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^18.0.0",
|
|
29
|
+
"@types/styled-components": "^5.1.26",
|
|
30
|
+
"@sproutsocial/eslint-config-seeds": "*",
|
|
31
|
+
"react": "^18.0.0",
|
|
32
|
+
"styled-components": "^5.2.3",
|
|
33
|
+
"tsup": "^8.0.2",
|
|
34
|
+
"typescript": "^5.6.2",
|
|
35
|
+
"@sproutsocial/seeds-tsconfig": "*",
|
|
36
|
+
"@sproutsocial/seeds-testing": "*",
|
|
37
|
+
"@sproutsocial/seeds-react-testing-library": "*",
|
|
38
|
+
"@sproutsocial/seeds-react-icon": "*",
|
|
39
|
+
"@sproutsocial/seeds-react-banner": "*",
|
|
40
|
+
"@sproutsocial/seeds-react-button": "*",
|
|
41
|
+
"@sproutsocial/seeds-react-text": "*",
|
|
42
|
+
"@sproutsocial/seeds-react-portal": "*"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"styled-components": "^5.2.3"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Banner from "@sproutsocial/seeds-react-banner";
|
|
3
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
5
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
6
|
+
import Tooltip from "./Tooltip";
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof Tooltip> = {
|
|
9
|
+
title: "Components/Tooltip",
|
|
10
|
+
component: Tooltip,
|
|
11
|
+
};
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Tooltip>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<>
|
|
19
|
+
<Box mb={400}>
|
|
20
|
+
<Banner text="Tooltips should be applied only to interactive elements, allowing users to launch them via focus or mouse hover." />
|
|
21
|
+
</Box>
|
|
22
|
+
<Box
|
|
23
|
+
width="100%"
|
|
24
|
+
p={600}
|
|
25
|
+
alignItems="center"
|
|
26
|
+
justifyContent="center"
|
|
27
|
+
display="flex"
|
|
28
|
+
height="200px"
|
|
29
|
+
>
|
|
30
|
+
<Tooltip content="This is a tooltip. It is a small piece of text that is shown when a user hovers or focuses over an element.">
|
|
31
|
+
<Button appearance="primary">show tooltip</Button>
|
|
32
|
+
</Tooltip>
|
|
33
|
+
</Box>
|
|
34
|
+
</>
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Focus: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<Box
|
|
41
|
+
width="100%"
|
|
42
|
+
p={600}
|
|
43
|
+
alignItems="center"
|
|
44
|
+
justifyContent="center"
|
|
45
|
+
display="flex"
|
|
46
|
+
height="200px"
|
|
47
|
+
color="text.body"
|
|
48
|
+
>
|
|
49
|
+
We've got some text and then we've got a focusable
|
|
50
|
+
<Tooltip content="I'm a tooltip">
|
|
51
|
+
<Button mx={300} appearance="primary">
|
|
52
|
+
button
|
|
53
|
+
</Button>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
and then some more text
|
|
56
|
+
</Box>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const WithNonStringContent: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<Box
|
|
63
|
+
width="100%"
|
|
64
|
+
p={600}
|
|
65
|
+
alignItems="center"
|
|
66
|
+
justifyContent="center"
|
|
67
|
+
display="flex"
|
|
68
|
+
height="200px"
|
|
69
|
+
>
|
|
70
|
+
<Tooltip
|
|
71
|
+
content={
|
|
72
|
+
<img
|
|
73
|
+
src="https://pixel.nymag.com/imgs/daily/vulture/2016/11/03/03-steven-seagal.w330.h330.jpg"
|
|
74
|
+
alt="a cool guy"
|
|
75
|
+
width="200"
|
|
76
|
+
/>
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
<Button appearance="primary">show tooltip</Button>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
</Box>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const WithIcon: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<Box
|
|
88
|
+
width="100%"
|
|
89
|
+
p={600}
|
|
90
|
+
alignItems="center"
|
|
91
|
+
justifyContent="center"
|
|
92
|
+
display="flex"
|
|
93
|
+
height="200px"
|
|
94
|
+
>
|
|
95
|
+
<Tooltip content="This is a button's tooltip">
|
|
96
|
+
<Button appearance="pill" ariaLabel="This is a button's label">
|
|
97
|
+
<Icon name="circle-check-outline" aria-hidden />
|
|
98
|
+
</Button>
|
|
99
|
+
</Tooltip>
|
|
100
|
+
</Box>
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const WithFocusableContent: Story = {
|
|
105
|
+
render: () => (
|
|
106
|
+
<Box
|
|
107
|
+
width="100%"
|
|
108
|
+
p={600}
|
|
109
|
+
alignItems="center"
|
|
110
|
+
justifyContent="center"
|
|
111
|
+
display="flex"
|
|
112
|
+
height="200px"
|
|
113
|
+
>
|
|
114
|
+
<Tooltip
|
|
115
|
+
content={
|
|
116
|
+
<Button
|
|
117
|
+
appearance="secondary"
|
|
118
|
+
width={1}
|
|
119
|
+
onClick={() => alert("clicked!")}
|
|
120
|
+
>
|
|
121
|
+
Tab to me
|
|
122
|
+
</Button>
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<Button appearance="primary">show tooltip</Button>
|
|
126
|
+
</Tooltip>
|
|
127
|
+
</Box>
|
|
128
|
+
),
|
|
129
|
+
};
|
package/src/Tooltip.tsx
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import MOTION from "@sproutsocial/seeds-motion/unitless";
|
|
4
|
+
import Popout from "@sproutsocial/seeds-react-popout";
|
|
5
|
+
import { StyledTooltipContent } from "./styles";
|
|
6
|
+
import type { TypeTooltipProps, TypeTooltipContent } from "./TooltipTypes";
|
|
7
|
+
|
|
8
|
+
let idCounter = 0;
|
|
9
|
+
|
|
10
|
+
const hasAttribute = (child: React.ReactNode, attribute: string) => {
|
|
11
|
+
return React.isValidElement(child) && child.props[attribute] !== undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Tooltip Styled Popout wrapper for handling events */
|
|
15
|
+
const TooltipBubble = ({
|
|
16
|
+
appearance = "pill",
|
|
17
|
+
children,
|
|
18
|
+
onFocus,
|
|
19
|
+
onBlur,
|
|
20
|
+
...rest
|
|
21
|
+
}: TypeTooltipContent) => {
|
|
22
|
+
// @ts-ignore Will fix during refactor
|
|
23
|
+
const handleFocus = (e) => {
|
|
24
|
+
onFocus(e);
|
|
25
|
+
};
|
|
26
|
+
// @ts-ignore Will fix during refactor
|
|
27
|
+
const handleBlur = (e) => {
|
|
28
|
+
onBlur(e);
|
|
29
|
+
};
|
|
30
|
+
return (
|
|
31
|
+
<StyledTooltipContent
|
|
32
|
+
role="tooltip"
|
|
33
|
+
appearance={appearance}
|
|
34
|
+
borderRadius={appearance === "box" ? 500 : "5000em"}
|
|
35
|
+
px={400}
|
|
36
|
+
py={appearance === "box" ? 400 : 200}
|
|
37
|
+
m={200}
|
|
38
|
+
color="text.body"
|
|
39
|
+
bg="container.background.base"
|
|
40
|
+
boxShadow="medium"
|
|
41
|
+
border={500}
|
|
42
|
+
borderColor="container.border.base"
|
|
43
|
+
onFocus={handleFocus}
|
|
44
|
+
onBlur={handleBlur}
|
|
45
|
+
onMouseEnter={handleFocus}
|
|
46
|
+
onMouseLeave={handleBlur}
|
|
47
|
+
tabIndex={0}
|
|
48
|
+
{...rest}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</StyledTooltipContent>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Core component */
|
|
56
|
+
const Tooltip = ({
|
|
57
|
+
content,
|
|
58
|
+
children,
|
|
59
|
+
enterDelay = MOTION.MOTION_DURATION_FAST * 1000,
|
|
60
|
+
placement = "auto",
|
|
61
|
+
appearance,
|
|
62
|
+
zIndex = 7,
|
|
63
|
+
qa,
|
|
64
|
+
popoutProps,
|
|
65
|
+
truncated = false,
|
|
66
|
+
onFocus,
|
|
67
|
+
onBlur,
|
|
68
|
+
...rest
|
|
69
|
+
}: TypeTooltipProps) => {
|
|
70
|
+
const [shouldShow, setShouldShow] = useState(false);
|
|
71
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
72
|
+
const [id] = useState(`Racine-tooltip-${idCounter++}`);
|
|
73
|
+
const isInvalidContent = content === null || content === undefined;
|
|
74
|
+
|
|
75
|
+
// @ts-ignore Will fix during refactor
|
|
76
|
+
const show = (e) => {
|
|
77
|
+
onFocus?.(e);
|
|
78
|
+
setShouldShow(true);
|
|
79
|
+
};
|
|
80
|
+
// @ts-ignore Will fix during refactor
|
|
81
|
+
const hide = (e) => {
|
|
82
|
+
onBlur?.(e);
|
|
83
|
+
setShouldShow(false);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const exitDelay = MOTION.MOTION_DURATION_FAST * 1000;
|
|
87
|
+
const defaultAppearance =
|
|
88
|
+
appearance || (typeof content === "object" ? "box" : "pill");
|
|
89
|
+
|
|
90
|
+
/** Handles all the logic around whether to display/not display */
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const documentBody = document.body;
|
|
93
|
+
let timeout;
|
|
94
|
+
const onEsc = (e: KeyboardEvent): void => {
|
|
95
|
+
// older browsers use "Esc"
|
|
96
|
+
if (["Escape", "Esc"].includes(e.key)) {
|
|
97
|
+
setIsOpen(false);
|
|
98
|
+
setShouldShow(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (shouldShow) {
|
|
103
|
+
timeout = setTimeout(() => setIsOpen(true), enterDelay);
|
|
104
|
+
} else {
|
|
105
|
+
timeout = setTimeout(() => {
|
|
106
|
+
setIsOpen(false);
|
|
107
|
+
}, exitDelay);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// We only want listeners from the tooltip if its open in the first place
|
|
111
|
+
if (isOpen) {
|
|
112
|
+
documentBody.addEventListener("keydown", onEsc, { capture: true });
|
|
113
|
+
}
|
|
114
|
+
return () => {
|
|
115
|
+
documentBody.removeEventListener("keydown", onEsc, { capture: true });
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
};
|
|
118
|
+
}, [isOpen, setShouldShow, shouldShow, enterDelay, exitDelay]);
|
|
119
|
+
|
|
120
|
+
/** The wrapped content of whats inside the Tooltip */
|
|
121
|
+
const TooltipContent = () => (
|
|
122
|
+
<TooltipBubble
|
|
123
|
+
appearance={defaultAppearance}
|
|
124
|
+
onFocus={show}
|
|
125
|
+
onBlur={hide}
|
|
126
|
+
aria-expanded={isOpen}
|
|
127
|
+
id={id}
|
|
128
|
+
{...rest}
|
|
129
|
+
>
|
|
130
|
+
{content}
|
|
131
|
+
</TooltipBubble>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Popout
|
|
136
|
+
content={!isInvalidContent ? TooltipContent : undefined}
|
|
137
|
+
isOpen={isOpen}
|
|
138
|
+
placement={placement}
|
|
139
|
+
qa={{
|
|
140
|
+
"data-qa-tooltip": id,
|
|
141
|
+
...qa,
|
|
142
|
+
}}
|
|
143
|
+
id={id + "-wrapper"}
|
|
144
|
+
focusOnContent={false}
|
|
145
|
+
zIndex={zIndex}
|
|
146
|
+
aria-haspopup="false"
|
|
147
|
+
display={truncated ? "flex" : undefined}
|
|
148
|
+
disableWrapperAria={true} // required so that the child span doesnt take in redundant aria props
|
|
149
|
+
{...popoutProps}
|
|
150
|
+
>
|
|
151
|
+
<span
|
|
152
|
+
onBlur={hide}
|
|
153
|
+
onFocus={show}
|
|
154
|
+
onMouseEnter={show}
|
|
155
|
+
onMouseLeave={hide}
|
|
156
|
+
style={
|
|
157
|
+
truncated
|
|
158
|
+
? {
|
|
159
|
+
overflow: "hidden",
|
|
160
|
+
textOverflow: "ellipsis",
|
|
161
|
+
whiteSpace: "nowrap",
|
|
162
|
+
}
|
|
163
|
+
: {}
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
{React.isValidElement(children)
|
|
167
|
+
? React.cloneElement(children as React.ReactElement, {
|
|
168
|
+
//** There may be cases where the Tooltip's child needs to properly describe its role as expanding a drawer, in which case that propery takes priority */
|
|
169
|
+
"aria-expanded": hasAttribute(children, "aria-expanded")
|
|
170
|
+
? children.props["aria-expanded"]
|
|
171
|
+
: isOpen,
|
|
172
|
+
"aria-describedby": id,
|
|
173
|
+
})
|
|
174
|
+
: children}
|
|
175
|
+
</span>
|
|
176
|
+
</Popout>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export default Tooltip;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { TypePopoutProps } from "@sproutsocial/seeds-react-popout";
|
|
3
|
+
import type { TypeBoxProps } from "@sproutsocial/seeds-react-box";
|
|
4
|
+
|
|
5
|
+
export interface TypeTooltipProps
|
|
6
|
+
extends Omit<
|
|
7
|
+
TypeBoxProps,
|
|
8
|
+
"children" | "content" | "onMouseEnter" | "onMouseLeave"
|
|
9
|
+
> {
|
|
10
|
+
/** The content that the tooltip should be attached to. Hovering or focusing this element will cause the tooltip to appear */
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
|
|
13
|
+
/** The content to be displayed within the tooltip. If there is no content, just the children are rendered */
|
|
14
|
+
content: React.ReactNode;
|
|
15
|
+
|
|
16
|
+
/** The placement of the tooltip in relation to the children */
|
|
17
|
+
placement?: TypePopoutProps["placement"];
|
|
18
|
+
|
|
19
|
+
/** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */
|
|
20
|
+
enterDelay?: number;
|
|
21
|
+
|
|
22
|
+
/** Used to override the appearance of the Tooltip content. By default, strings will have the 'pill' appearance, and more complex content will have the 'box' appearance. You can change those defaults by setting this prop. */
|
|
23
|
+
appearance?: "pill" | "box";
|
|
24
|
+
qa?: object;
|
|
25
|
+
zIndex?: number;
|
|
26
|
+
|
|
27
|
+
/** Props to be spread onto the underlying Popout component */
|
|
28
|
+
popoutProps?: Partial<TypePopoutProps>;
|
|
29
|
+
|
|
30
|
+
/** Truncates text into a single line with ellipsis */
|
|
31
|
+
truncated?: boolean;
|
|
32
|
+
|
|
33
|
+
ariaProps?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
37
|
+
export interface TypeTooltipContent
|
|
38
|
+
extends Pick<TypeTooltipProps, "appearance" | "children"> {
|
|
39
|
+
onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|
|
40
|
+
onBlur: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* eslint-disable testing-library/no-unnecessary-act */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
fireEvent,
|
|
7
|
+
act,
|
|
8
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
9
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
10
|
+
import Tooltip from "../Tooltip";
|
|
11
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
12
|
+
|
|
13
|
+
jest.mock("popper.js", () => {
|
|
14
|
+
const PopperJS = jest.requireActual("popper.js");
|
|
15
|
+
const mockDestroy = jest.fn();
|
|
16
|
+
const mockScheduleUpdate = jest.fn();
|
|
17
|
+
|
|
18
|
+
return class Popper {
|
|
19
|
+
static placements = PopperJS.placements;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
return {
|
|
23
|
+
destroy: mockDestroy,
|
|
24
|
+
scheduleUpdate: mockScheduleUpdate,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
jest.mock(
|
|
31
|
+
"@sproutsocial/seeds-react-portal",
|
|
32
|
+
() =>
|
|
33
|
+
({ children }: { children: React.ReactNode }) =>
|
|
34
|
+
<div>{children}</div>
|
|
35
|
+
);
|
|
36
|
+
describe("Tooltip", () => {
|
|
37
|
+
describe("rendering all valid `React.Node`s", () => {
|
|
38
|
+
it("should render string content", async () => {
|
|
39
|
+
const contentText = "hey";
|
|
40
|
+
render(<Tooltip content={contentText}>hi</Tooltip>);
|
|
41
|
+
// This will cause a console error (but still pass) until react 16.9
|
|
42
|
+
act(() => {
|
|
43
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
44
|
+
});
|
|
45
|
+
await screen.findByText(contentText);
|
|
46
|
+
expect(screen.getByText(contentText)).toBeInTheDocument();
|
|
47
|
+
|
|
48
|
+
// TODO
|
|
49
|
+
// await runA11yCheck();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should render number content", async () => {
|
|
53
|
+
const contentText = 123;
|
|
54
|
+
render(<Tooltip content={contentText}>hi</Tooltip>);
|
|
55
|
+
act(() => {
|
|
56
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
57
|
+
});
|
|
58
|
+
await screen.findByDataQaLabel({
|
|
59
|
+
"popout-isopen": "true",
|
|
60
|
+
});
|
|
61
|
+
expect(screen.getByText(`${contentText}`)).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should render React Element content", async () => {
|
|
65
|
+
render(<Tooltip content={<p>hey</p>}>hi</Tooltip>);
|
|
66
|
+
act(() => {
|
|
67
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
68
|
+
});
|
|
69
|
+
await screen.findByDataQaLabel({
|
|
70
|
+
"popout-isopen": "true",
|
|
71
|
+
});
|
|
72
|
+
expect(screen.getByText("hey")).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should render $Iterable<string> content", async () => {
|
|
76
|
+
render(<Tooltip content={["hello", "world"]}>hi</Tooltip>);
|
|
77
|
+
act(() => {
|
|
78
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
79
|
+
});
|
|
80
|
+
await screen.findByText(/hello/i);
|
|
81
|
+
expect(screen.getByText(/hello/i)).toBeInTheDocument();
|
|
82
|
+
await screen.findByText(/world/i);
|
|
83
|
+
expect(screen.getByText(/world/i)).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should render $Iterable<React$Element<any>> content", async () => {
|
|
87
|
+
render(
|
|
88
|
+
<Tooltip content={[<p key="1">hello</p>, <p key="2">world</p>]}>
|
|
89
|
+
hi
|
|
90
|
+
</Tooltip>
|
|
91
|
+
);
|
|
92
|
+
act(() => {
|
|
93
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
94
|
+
});
|
|
95
|
+
await screen.findByText(/hello/i);
|
|
96
|
+
expect(screen.getByText(/hello/i)).toBeInTheDocument();
|
|
97
|
+
await screen.findByText(/world/i);
|
|
98
|
+
expect(screen.getByText(/world/i)).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should not render null content", async () => {
|
|
102
|
+
expect.assertions(1);
|
|
103
|
+
render(<Tooltip content={null}>hi</Tooltip>);
|
|
104
|
+
act(() => {
|
|
105
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
106
|
+
});
|
|
107
|
+
// @see https://jestjs.io/docs/tutorial-async#rejects
|
|
108
|
+
await expect(
|
|
109
|
+
screen.findByDataQaLabel({
|
|
110
|
+
"popout-isopen": "true",
|
|
111
|
+
})
|
|
112
|
+
).rejects.toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should not render undefined content", async () => {
|
|
116
|
+
expect.assertions(1);
|
|
117
|
+
render(<Tooltip content={undefined}>hi</Tooltip>);
|
|
118
|
+
act(() => {
|
|
119
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
120
|
+
});
|
|
121
|
+
// @see https://jestjs.io/docs/tutorial-async#rejects
|
|
122
|
+
await expect(
|
|
123
|
+
screen.findByDataQaLabel({
|
|
124
|
+
"popout-isopen": "true",
|
|
125
|
+
})
|
|
126
|
+
).rejects.toBeTruthy();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should render as aria-expanded if tooltip is hovered", async () => {
|
|
130
|
+
render(
|
|
131
|
+
<Tooltip content="hello">
|
|
132
|
+
<Button role="button" onClick={() => {}}>
|
|
133
|
+
hi
|
|
134
|
+
</Button>
|
|
135
|
+
</Tooltip>
|
|
136
|
+
);
|
|
137
|
+
// Hover over
|
|
138
|
+
act(() => {
|
|
139
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
140
|
+
});
|
|
141
|
+
// Wait for tooltip
|
|
142
|
+
await screen.findByText(/hello/i);
|
|
143
|
+
// check if expanded
|
|
144
|
+
expect(
|
|
145
|
+
screen.getByRole("button", {
|
|
146
|
+
expanded: true,
|
|
147
|
+
})
|
|
148
|
+
).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should not render as aria-expanded if child already defines one", async () => {
|
|
152
|
+
render(
|
|
153
|
+
<Tooltip content="hello">
|
|
154
|
+
<Button aria-expanded="false" role="button" onClick={() => {}}>
|
|
155
|
+
hi
|
|
156
|
+
</Button>
|
|
157
|
+
</Tooltip>
|
|
158
|
+
);
|
|
159
|
+
// Hover over
|
|
160
|
+
act(() => {
|
|
161
|
+
fireEvent.mouseOver(screen.getByText("hi"));
|
|
162
|
+
});
|
|
163
|
+
// Wait for tooltip
|
|
164
|
+
await screen.findByText(/hello/i);
|
|
165
|
+
// check if expanded
|
|
166
|
+
expect(
|
|
167
|
+
screen.getByRole("button", {
|
|
168
|
+
expanded: false,
|
|
169
|
+
})
|
|
170
|
+
).toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should render when wrapped around an icon", async () => {
|
|
175
|
+
render(
|
|
176
|
+
<Tooltip content="hey">
|
|
177
|
+
<Icon
|
|
178
|
+
data-testid="icon"
|
|
179
|
+
name="globe-outline"
|
|
180
|
+
aria-label="Open Tooltip"
|
|
181
|
+
/>
|
|
182
|
+
</Tooltip>
|
|
183
|
+
);
|
|
184
|
+
// This will cause a console error (but still pass) until react 16.9
|
|
185
|
+
act(() => {
|
|
186
|
+
fireEvent.mouseOver(screen.getByTestId("icon"));
|
|
187
|
+
});
|
|
188
|
+
await expect(
|
|
189
|
+
screen.findByDataQaLabel({
|
|
190
|
+
"popout-isopen": "true",
|
|
191
|
+
})
|
|
192
|
+
).resolves.toBeTruthy();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Tooltip from "../Tooltip";
|
|
3
|
+
import { Text } from "@sproutsocial/seeds-react-text";
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6
|
+
function TooltipTypes() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<Tooltip content="This is a tooltip on a text element">
|
|
10
|
+
<Text>Hello World</Text>
|
|
11
|
+
</Tooltip>
|
|
12
|
+
<Tooltip onFocus={() => {}} content="This is a tooltip on a text element">
|
|
13
|
+
<Text>Hello World</Text>
|
|
14
|
+
</Tooltip>
|
|
15
|
+
<Tooltip
|
|
16
|
+
content="foo"
|
|
17
|
+
ariaProps={{
|
|
18
|
+
role: "link",
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<Text>Yo</Text>
|
|
22
|
+
</Tooltip>
|
|
23
|
+
{/* @ts-expect-error - test that invalid props are rejected */}
|
|
24
|
+
<Tooltip />
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
}
|
package/src/index.ts
ADDED
package/src/styled.d.ts
ADDED
package/src/styles.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
3
|
+
import type { TypeTooltipContent } from "./TooltipTypes";
|
|
4
|
+
|
|
5
|
+
export const StyledTooltipContent = styled(Box)<
|
|
6
|
+
Pick<TypeTooltipContent, "appearance">
|
|
7
|
+
>`
|
|
8
|
+
font-family: ${(props) => props.theme.fontFamily};
|
|
9
|
+
${(props) => props.theme.typography[200]}
|
|
10
|
+
text-align: ${(props) => (props.appearance === "box" ? "left" : "center")};
|
|
11
|
+
`;
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig((options) => ({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["cjs", "esm"],
|
|
6
|
+
clean: true,
|
|
7
|
+
legacyOutput: true,
|
|
8
|
+
dts: options.dts,
|
|
9
|
+
external: ["react"],
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
metafile: options.metafile,
|
|
12
|
+
}));
|