@sproutsocial/seeds-react-tooltip 1.1.32 → 1.1.34

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.
@@ -1,312 +0,0 @@
1
- import React, { useState } from "react";
2
- import type { Meta, StoryObj } from "@storybook/react";
3
- import Banner from "@sproutsocial/seeds-react-banner";
4
- import Box from "@sproutsocial/seeds-react-box";
5
- import Button from "@sproutsocial/seeds-react-button";
6
- import Icon from "@sproutsocial/seeds-react-icon";
7
- import Text from "@sproutsocial/seeds-react-text";
8
- import Tooltip from "./Tooltip";
9
-
10
- const meta: Meta<typeof Tooltip> = {
11
- title: "Components/Tooltip",
12
- component: Tooltip,
13
- };
14
- export default meta;
15
-
16
- type Story = StoryObj<typeof Tooltip>;
17
-
18
- export const Default: Story = {
19
- render: () => (
20
- <>
21
- <Box mb={400}>
22
- <Banner text="Tooltips should be applied only to interactive elements, allowing users to launch them via focus or mouse hover." />
23
- </Box>
24
- <Box
25
- width="100%"
26
- p={600}
27
- alignItems="center"
28
- justifyContent="center"
29
- display="flex"
30
- height="200px"
31
- >
32
- <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.">
33
- <Button appearance="primary">show tooltip</Button>
34
- </Tooltip>
35
- </Box>
36
- </>
37
- ),
38
- };
39
-
40
- export const Focus: Story = {
41
- render: () => (
42
- <Box
43
- width="100%"
44
- p={600}
45
- alignItems="center"
46
- justifyContent="center"
47
- display="flex"
48
- height="200px"
49
- color="text.body"
50
- >
51
- We've got some text and then we've got a focusable
52
- <Tooltip content="I'm a tooltip">
53
- <Button mx={300} appearance="primary">
54
- button
55
- </Button>
56
- </Tooltip>
57
- and then some more text
58
- </Box>
59
- ),
60
- };
61
-
62
- export const WithNonStringContent: Story = {
63
- render: () => (
64
- <Box
65
- width="100%"
66
- p={600}
67
- alignItems="center"
68
- justifyContent="center"
69
- display="flex"
70
- height="200px"
71
- >
72
- <Tooltip
73
- content={
74
- <img
75
- src="https://pixel.nymag.com/imgs/daily/vulture/2016/11/03/03-steven-seagal.w330.h330.jpg"
76
- alt="a cool guy"
77
- width="200"
78
- />
79
- }
80
- >
81
- <Button appearance="primary">show tooltip</Button>
82
- </Tooltip>
83
- </Box>
84
- ),
85
- };
86
-
87
- export const WithIcon: Story = {
88
- render: () => (
89
- <Box
90
- width="100%"
91
- p={600}
92
- alignItems="center"
93
- justifyContent="center"
94
- display="flex"
95
- height="200px"
96
- >
97
- <Tooltip content="This is a button's tooltip">
98
- <Button appearance="pill" ariaLabel="This is a button's label">
99
- <Icon name="circle-check-outline" aria-hidden />
100
- </Button>
101
- </Tooltip>
102
- </Box>
103
- ),
104
- };
105
-
106
- export const WithFocusableContent: Story = {
107
- render: () => (
108
- <Box
109
- width="100%"
110
- p={600}
111
- alignItems="center"
112
- justifyContent="center"
113
- display="flex"
114
- height="200px"
115
- >
116
- <Tooltip
117
- content={
118
- <Button
119
- appearance="secondary"
120
- width={1}
121
- onClick={() => alert("clicked!")}
122
- >
123
- Tab to me
124
- </Button>
125
- }
126
- >
127
- <Button appearance="primary">show tooltip</Button>
128
- </Tooltip>
129
- </Box>
130
- ),
131
- };
132
-
133
- export const WithLegacyMouseInteraction: Story = {
134
- render: () => (
135
- <Box
136
- width="100%"
137
- p={600}
138
- alignItems="center"
139
- justifyContent="center"
140
- display="flex"
141
- flexDirection="column"
142
- gap={400}
143
- height="400px"
144
- >
145
- <Box display="flex" flexDirection="column" alignItems="center" gap={200}>
146
- <Tooltip
147
- content={
148
- <Box>
149
- <div>Hover into this content!</div>
150
- <Button
151
- appearance="secondary"
152
- size="small"
153
- mt={200}
154
- onClick={() => alert("Clicked!")}
155
- >
156
- Interactive button
157
- </Button>
158
- </Box>
159
- }
160
- legacyMouseInteraction={true}
161
- >
162
- <Button appearance="primary">With legacyMouseInteraction</Button>
163
- </Tooltip>
164
- <Text fontSize={300} color="text.body.secondary">
165
- Tooltip stays open when you hover into it (exitDelay: ~150ms)
166
- </Text>
167
- </Box>
168
-
169
- <Box display="flex" flexDirection="column" alignItems="center" gap={200}>
170
- <Tooltip
171
- content={
172
- <Box>
173
- <div>Try to hover into this content</div>
174
- <Button appearance="secondary" size="small" mt={200}>
175
- Can't interact
176
- </Button>
177
- </Box>
178
- }
179
- >
180
- <Button appearance="primary">Without legacyMouseInteraction</Button>
181
- </Tooltip>
182
- <Text fontSize={300} color="text.body.secondary">
183
- Tooltip closes immediately when you leave (exitDelay: 0ms)
184
- </Text>
185
- </Box>
186
- </Box>
187
- ),
188
- };
189
-
190
- const InModalV1Template = () => {
191
- const [isOpen, setIsOpen] = useState(true);
192
- // Lazy-load Modal V1 to avoid circular deps in storybook
193
- // eslint-disable-next-line @typescript-eslint/no-var-requires
194
- const Modal = require("@sproutsocial/seeds-react-modal").default;
195
-
196
- return (
197
- <Box>
198
- <Button appearance="primary" onClick={() => setIsOpen(true)}>
199
- Open Modal
200
- </Button>
201
- <Modal
202
- appElementSelector="#storybook-root"
203
- isOpen={isOpen}
204
- onClose={() => setIsOpen(false)}
205
- closeButtonLabel="Close modal"
206
- label="Modal with Tooltip"
207
- >
208
- <Modal.Header title="Tooltip in Modal V1" />
209
- <Modal.Content>
210
- <Box p={400} display="flex" justifyContent="center">
211
- <Tooltip content="This tooltip renders inside the Modal V1">
212
- <Button appearance="primary">Hover me</Button>
213
- </Tooltip>
214
- </Box>
215
- </Modal.Content>
216
- </Modal>
217
- </Box>
218
- );
219
- };
220
-
221
- export const InModal: Story = {
222
- render: () => <InModalV1Template />,
223
- };
224
-
225
- const InModalV2Template = () => {
226
- // eslint-disable-next-line @typescript-eslint/no-var-requires
227
- const { ModalV2, ModalHeader, ModalBody } = require("@sproutsocial/seeds-react-modal");
228
-
229
- return (
230
- <ModalV2
231
- aria-label="Modal V2 with Tooltip"
232
- closeButtonAriaLabel="Close Modal"
233
- modalTrigger={<Button appearance="primary">Open Modal V2</Button>}
234
- >
235
- <ModalHeader title="Tooltip in Modal V2" />
236
- <ModalBody>
237
- <Box p={400} display="flex" justifyContent="center">
238
- <Tooltip content="This tooltip renders inside Modal V2">
239
- <Button appearance="primary">Hover me</Button>
240
- </Tooltip>
241
- </Box>
242
- </ModalBody>
243
- </ModalV2>
244
- );
245
- };
246
-
247
- export const InModalV2: Story = {
248
- render: () => <InModalV2Template />,
249
- };
250
-
251
- const InPopoutTemplate = () => {
252
- // eslint-disable-next-line @typescript-eslint/no-var-requires
253
- const Popout = require("@sproutsocial/seeds-react-popout").default;
254
- const [isOpen, setIsOpen] = useState(true);
255
-
256
- return (
257
- <Box p={600} display="flex" justifyContent="center">
258
- <Popout
259
- content={() => (
260
- <Box p={400}>
261
- <Tooltip content="This tooltip renders inside a Popout">
262
- <Button appearance="secondary">Hover me</Button>
263
- </Tooltip>
264
- </Box>
265
- )}
266
- isOpen={isOpen}
267
- placement="bottom"
268
- >
269
- <Button appearance="primary" onClick={() => setIsOpen(!isOpen)}>
270
- Toggle Popout
271
- </Button>
272
- </Popout>
273
- </Box>
274
- );
275
- };
276
-
277
- export const InPopout: Story = {
278
- render: () => <InPopoutTemplate />,
279
- };
280
-
281
- const InDrawerTemplate = () => {
282
- // eslint-disable-next-line @typescript-eslint/no-var-requires
283
- const Drawer = require("@sproutsocial/seeds-react-drawer").default;
284
- const [isOpen, setIsOpen] = useState(true);
285
-
286
- return (
287
- <Box>
288
- <Button appearance="primary" onClick={() => setIsOpen(true)}>
289
- Open Drawer
290
- </Button>
291
- <Drawer
292
- isOpen={isOpen}
293
- onClose={() => setIsOpen(false)}
294
- closeButtonLabel="Close drawer"
295
- id="tooltip-drawer"
296
- >
297
- <Box p={400}>
298
- <Text as="h3" fontSize={400} fontWeight="bold" mb={400}>
299
- Tooltip in Drawer
300
- </Text>
301
- <Tooltip content="This tooltip renders inside a Drawer">
302
- <Button appearance="primary">Hover me</Button>
303
- </Tooltip>
304
- </Box>
305
- </Drawer>
306
- </Box>
307
- );
308
- };
309
-
310
- export const InDrawer: Story = {
311
- render: () => <InDrawerTemplate />,
312
- };
package/src/Tooltip.tsx DELETED
@@ -1,206 +0,0 @@
1
- import * as React from "react";
2
- import { useState, useRef, useEffect, useContext } from "react";
3
- import MOTION from "@sproutsocial/seeds-motion/unitless";
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";
7
- import { StyledTooltipContent } from "./styles";
8
- import type { TypeTooltipProps } from "./TooltipTypes";
9
-
10
- let idCounter = 0;
11
-
12
- const hasAttribute = (child: React.ReactNode, attribute: string) => {
13
- return React.isValidElement(child) && child.props[attribute] !== undefined;
14
- };
15
-
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
- }
53
-
54
- const Tooltip = ({
55
- content,
56
- children,
57
- enterDelay = MOTION.MOTION_DURATION_FAST * 1000,
58
- exitDelay,
59
- placement = "auto",
60
- appearance,
61
- zIndex = 7,
62
- qa,
63
- popoutProps,
64
- triggerProps,
65
- truncated = false,
66
- onFocus,
67
- onBlur,
68
- legacyMouseInteraction,
69
- ...rest
70
- }: TypeTooltipProps) => {
71
- const [open, setOpen] = useState(false);
72
- const [id] = useState(`Racine-tooltip-${idCounter++}`);
73
- const triggerRef = useRef<HTMLSpanElement>(null);
74
- const isInvalidContent = content === null || content === undefined;
75
- const disablePortalToBody = useContext(DisablePortalToBodyContext);
76
- const [portalContainer, setPortalContainer] = useState<
77
- HTMLElement | undefined
78
- >(undefined);
79
-
80
- const resolvedExitDelay =
81
- exitDelay !== undefined
82
- ? exitDelay
83
- : legacyMouseInteraction
84
- ? MOTION.MOTION_DURATION_FAST * 1000
85
- : 0;
86
-
87
- const defaultAppearance =
88
- appearance || (typeof content === "object" ? "box" : "pill");
89
-
90
- const { side, align } = mapPlacement(placement);
91
-
92
- const mergedWrapperProps = {
93
- ...popoutProps,
94
- ...triggerProps,
95
- };
96
-
97
- const hasWrapperProps = Object.keys(mergedWrapperProps).length > 0;
98
-
99
- useEffect(() => {
100
- if (disablePortalToBody && triggerRef.current) {
101
- const dialogContent =
102
- triggerRef.current.closest<HTMLElement>("[role='dialog']");
103
- setPortalContainer(dialogContent ?? undefined);
104
- }
105
- }, [disablePortalToBody]);
106
-
107
- if (isInvalidContent) {
108
- return (
109
- <span
110
- style={
111
- truncated
112
- ? {
113
- overflow: "hidden",
114
- textOverflow: "ellipsis",
115
- whiteSpace: "nowrap",
116
- }
117
- : {}
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
- }
155
- >
156
- {React.isValidElement(children)
157
- ? React.cloneElement(children as React.ReactElement, {
158
- "aria-expanded": hasAttribute(children, "aria-expanded")
159
- ? children.props["aria-expanded"]
160
- : undefined,
161
- "aria-describedby": open ? id : undefined,
162
- })
163
- : children}
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
203
- );
204
- };
205
-
206
- export default Tooltip;
@@ -1,77 +0,0 @@
1
- import * as React from "react";
2
- import type { TypeBoxProps } from "@sproutsocial/seeds-react-box";
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
-
21
- export interface TypeTooltipProps
22
- extends Omit<
23
- TypeBoxProps,
24
- "children" | "content" | "onMouseEnter" | "onMouseLeave"
25
- > {
26
- /** The content that the tooltip should be attached to. Hovering or focusing this element will cause the tooltip to appear */
27
- children: React.ReactNode;
28
-
29
- /** The content to be displayed within the tooltip. If there is no content, just the children are rendered */
30
- content: React.ReactNode;
31
-
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;
40
-
41
- /** The time (in ms) that a user has to be hovered/focused before the tooltip will appear */
42
- enterDelay?: number;
43
-
44
- /** The time (in ms) that a user has to leave hovered/focused before the tooltip will disappear */
45
- exitDelay?: number;
46
-
47
- /**
48
- * When true, enables mouse interaction on tooltip content and sets exitDelay
49
- * to MOTION_DURATION_FAST * 1000 by default (instead of 0).
50
- */
51
- legacyMouseInteraction?: boolean;
52
-
53
- /** 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. */
54
- appearance?: "pill" | "box";
55
- qa?: object;
56
- zIndex?: number;
57
-
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;
65
-
66
- /** Truncates text into a single line with ellipsis */
67
- truncated?: boolean;
68
-
69
- ariaProps?: Record<string, string>;
70
- }
71
-
72
- export interface TypeTooltipContent
73
- extends Pick<TypeTooltipProps, "appearance" | "children"> {
74
- onFocus: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
75
- onBlur: (e: React.FocusEvent<HTMLDivElement, FocusEvent>) => void;
76
- legacyMouseInteraction?: boolean;
77
- }