Skip to content

Commit 92c38b1

Browse files
TheSonOfThompcharcoalyytsckshaneeza
authored
Exports Tooltip event handler utils (#2910)
* feat(tooltip): implement event handler utils for hover and click interactions * changesets * LG-5139: optional icon in chart titles (#2899) * rough implementation * optional icon to chart header title * documented use * changeset * avoid empty div Co-authored-by: Terrence Keane <terrence.keane@mongodb.com> --------- Co-authored-by: Terrence Keane <terrence.keane@mongodb.com> Co-authored-by: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> * Adds missing `./static` directory to SB addon (#2905) * adds static to addon exports * adds `static/*` to files * adds changesets * renames changesets * clean up handlers. mv types * simplifies hook * adds tests * adds DEFAULT_HOVER_DELAY * update triggerEventHandlers * tsdoc * only hook * Update useTooltipTriggerEventHandlers.spec.tsx * memoize * Update packages/tooltip/src/Tooltip/utils/useTooltipTriggerEventHandlers.spec.tsx Co-authored-by: Shaneeza <shaneeza.ali@mongodb.com> * SA feedback 👍 * Update useTooltipTriggerEventHandlers.spec.tsx * type names * rm delay escape hatch --------- Co-authored-by: alina 颖思 <106563068+charcoalyy@users.noreply.github.com> Co-authored-by: Terrence Keane <terrence.keane@mongodb.com> Co-authored-by: Shaneeza <shaneeza.ali@mongodb.com>
1 parent ca86113 commit 92c38b1

File tree

10 files changed

+418
-91
lines changed

10 files changed

+418
-91
lines changed

.changeset/tooltip-named-export.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/tooltip': minor
3+
---
4+
5+
Exports `Tooltip` as a named export (as well as default)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@leafygreen-ui/tooltip': minor
3+
---
4+
5+
Exports `useTooltipTriggerEventHandlers` to enable handling external tooltip triggers.
6+
7+
```tsx
8+
const [open, setOpen] = useState(false);
9+
const tooltipEventHandlers = useTooltipTriggerEventHandlers({
10+
triggerEvent: TriggerEvent.Hover,
11+
setState: setOpen,
12+
onFocus: (e) => { console.log(e) } // side effects called on focus of the trigger
13+
});
14+
15+
return (
16+
<>
17+
<Button ref={triggerRef} {...tooltipEventHandlers}>
18+
Button
19+
</Button>
20+
<Tooltip
21+
refEl={triggerRef}
22+
open={open}
23+
setOpen={setOpen}
24+
>Content</Tooltip>
25+
</>
26+
)
27+
```

packages/tooltip/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,30 @@ import Tooltip from '@leafygreen-ui/tooltip';
7878
| `popoverZIndex` | `number` | Sets the z-index CSS property for the popover. | |
7979
| `baseFontSize` | `13` \| `16` | font-size applied to typography element | default to value set by LeafyGreen Provider |
8080
| ... | native `div` attributes | Any other props will be spread on the root `div` element | |
81+
82+
### External tooltip triggers
83+
84+
When defining a tooltip trigger as a separate element, ensure the `refEl` is defined on the `<Tooltip>` element, and use the `useTooltipTriggerEventHandlers` hook to create tooltip event handlers.
85+
86+
```tsx
87+
const triggerRef = useRef();
88+
const [open, setOpen] = useState(false);
89+
const tooltipEventHandlers = useTooltipTriggerEventHandlers({
90+
triggerEvent: TriggerEvent.Hover,
91+
setState: setOpen,
92+
onFocus: e => {
93+
console.log(e);
94+
}, // side effects called on focus of the trigger
95+
});
96+
97+
return (
98+
<>
99+
<Button ref={triggerRef} {...tooltipEventHandlers}>
100+
Button
101+
</Button>
102+
<Tooltip refEl={triggerRef} open={open} setOpen={setOpen}>
103+
Content
104+
</Tooltip>
105+
</>
106+
);
107+
```

packages/tooltip/src/Tooltip/Tooltip.tsx

Lines changed: 16 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import React, {
55
useRef,
66
useState,
77
} from 'react';
8-
import { flushSync } from 'react-dom';
9-
import debounce from 'lodash/debounce';
108

119
import { css, cx } from '@leafygreen-ui/emotion';
1210
import {
@@ -26,6 +24,7 @@ import {
2624

2725
import SvgNotch from '../Notch';
2826

27+
import { useTooltipTriggerEventHandlers } from './utils/useTooltipTriggerEventHandlers';
2928
import {
3029
baseStyles,
3130
baseTypeStyle,
@@ -40,7 +39,6 @@ import {
4039
TooltipProps,
4140
TriggerEvent,
4241
} from './Tooltip.types';
43-
import { hoverDelay } from './tooltipConstants';
4442
import { notchPositionStyles } from './tooltipUtils';
4543

4644
const stopClickPropagation = (evt: React.MouseEvent) => {
@@ -111,7 +109,6 @@ function Tooltip({
111109
const setOpen =
112110
isControlled && controlledSetOpen ? controlledSetOpen : uncontrolledSetOpen;
113111

114-
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
115112
const tooltipRef = useRef<HTMLDivElement>(null);
116113

117114
const existingId = id ?? tooltipRef.current?.id;
@@ -127,13 +124,8 @@ function Tooltip({
127124
}
128125
}, [trigger]);
129126

130-
useEffect(() => {
131-
return () => {
132-
if (timeoutRef.current) {
133-
clearTimeout(timeoutRef.current);
134-
}
135-
};
136-
}, [timeoutRef]);
127+
const triggerComponent =
128+
typeof trigger === 'function' ? trigger({}) : trigger;
137129

138130
const handleClose = useCallback(() => {
139131
if (typeof shouldClose !== 'function' || shouldClose()) {
@@ -142,70 +134,20 @@ function Tooltip({
142134
}
143135
}, [setOpen, shouldClose, onClose]);
144136

145-
const createTriggerProps = useCallback(
146-
(triggerEvent: TriggerEvent, triggerProps?: any) => {
147-
switch (triggerEvent) {
148-
case TriggerEvent.Hover:
149-
return {
150-
onMouseEnter: debounce((e: MouseEvent) => {
151-
userTriggerHandler('onMouseEnter', e);
152-
// Without this the tooltip sometimes opens without a transition. flushSync prevents this state update from automatically batching. Instead updates are made synchronously.
153-
// https://react.dev/reference/react-dom/flushSync#flushing-updates-for-third-party-integrations
154-
flushSync(() => {
155-
timeoutRef.current = setTimeout(() => {
156-
setOpen(true);
157-
}, hoverDelay);
158-
});
159-
}, 35),
160-
onMouseLeave: debounce((e: MouseEvent) => {
161-
userTriggerHandler('onMouseLeave', e);
162-
if (timeoutRef.current) {
163-
clearTimeout(timeoutRef.current);
164-
timeoutRef.current = null;
165-
}
166-
handleClose();
167-
}, 35),
168-
onFocus: (e: MouseEvent) => {
169-
userTriggerHandler('onFocus', e);
170-
setOpen(true);
171-
},
172-
onBlur: (e: MouseEvent) => {
173-
userTriggerHandler('onBlur', e);
174-
handleClose();
175-
},
176-
};
177-
case TriggerEvent.Click:
178-
default:
179-
return {
180-
onClick: (e: MouseEvent) => {
181-
// ensure that we don't close the tooltip when content inside tooltip is clicked
182-
if (e.target !== tooltipRef.current) {
183-
userTriggerHandler('onClick', e);
184-
setOpen((curr: boolean) => !curr);
185-
}
186-
},
187-
};
188-
}
189-
190-
function userTriggerHandler(handler: string, e: MouseEvent): void {
191-
// call any click handlers already on the trigger
192-
if (
193-
triggerProps &&
194-
triggerProps[handler] &&
195-
typeof triggerProps[handler] == 'function'
196-
)
197-
triggerProps[handler](e);
198-
}
199-
},
200-
[handleClose, setOpen, tooltipRef],
201-
);
202-
203137
useEscapeKey(handleClose, { enabled: open });
204138

205139
useBackdropClick(handleClose, [tooltipRef], {
206140
enabled: open && triggerEvent === 'click',
207141
});
208142

143+
const triggerEventHandlers = useTooltipTriggerEventHandlers({
144+
setState: setOpen,
145+
triggerEvent,
146+
tooltipRef,
147+
isEnabled: enabled,
148+
...triggerComponent?.props,
149+
});
150+
209151
const popoverProps = {
210152
popoverZIndex,
211153
refEl,
@@ -291,20 +233,17 @@ function Tooltip({
291233
</Popover>
292234
);
293235

294-
if (trigger) {
295-
const originalTrigger =
296-
typeof trigger === 'function' ? trigger({}) : trigger;
297-
298-
return React.cloneElement(originalTrigger, {
299-
...createTriggerProps(triggerEvent, originalTrigger.props),
236+
if (triggerComponent) {
237+
return React.cloneElement(triggerComponent, {
238+
...triggerEventHandlers,
300239
'aria-describedby': active ? tooltipId : undefined,
301240
children: (
302241
<>
303-
{originalTrigger.props.children}
242+
{triggerComponent.props.children}
304243
{tooltip}
305244
</>
306245
),
307-
className: cx(positionRelative, originalTrigger.props.className),
246+
className: cx(positionRelative, triggerComponent.props.className),
308247
});
309248
}
310249

packages/tooltip/src/Tooltip/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
type TooltipProps,
88
TriggerEvent,
99
} from './Tooltip.types';
10-
import { hoverDelay } from './tooltipConstants';
10+
import { DEFAULT_HOVER_DELAY } from './tooltipConstants';
1111

1212
export {
1313
Align,
1414
DismissMode,
15-
hoverDelay,
15+
DEFAULT_HOVER_DELAY as hoverDelay,
1616
Justify,
1717
RenderMode,
1818
type TooltipProps,

packages/tooltip/src/Tooltip/tooltipConstants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ import { transitionDuration } from '@leafygreen-ui/tokens';
33
export const notchHeight = 8;
44
export const notchWidth = 26;
55
export const borderRadius = 16;
6-
export const hoverDelay = transitionDuration.slowest;
6+
7+
export const DEFAULT_HOVER_DELAY = transitionDuration.slowest;
8+
export const CALLBACK_DEBOUNCE = 35; // ms
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { FocusEventHandler, MouseEventHandler, RefObject } from 'react';
2+
3+
import type { TriggerEvent } from '../Tooltip.types';
4+
5+
export interface UseTooltipEventsBaseArgs {
6+
/**
7+
* The `useState` dispatch method to toggle the tooltip state
8+
*/
9+
setState: React.Dispatch<React.SetStateAction<boolean>>;
10+
11+
/**
12+
* Whether the tooltip event handlers should be enabled
13+
*/
14+
isEnabled?: boolean;
15+
}
16+
17+
export interface UseTooltipEventsArgsHover {
18+
/**
19+
* Whether the tooltip will open/close on `hover` or `click` events.
20+
* Note: must match the value passed onto `tooltip`
21+
*/
22+
triggerEvent: typeof TriggerEvent.Hover;
23+
24+
/**
25+
* A ref to the Tooltip element.
26+
* Optional when the trigger event is `hover`
27+
*/
28+
tooltipRef?: RefObject<HTMLElement>;
29+
30+
/** Additional side effects to run on this event */
31+
onMouseEnter?: MouseEventHandler<HTMLElement>;
32+
33+
/** Additional side effects to run on this event */
34+
onMouseLeave?: MouseEventHandler<HTMLElement>;
35+
36+
/** Additional side effects to run on this event */
37+
onFocus?: FocusEventHandler<HTMLElement>;
38+
39+
/** Additional side effects to run on this event */
40+
onBlur?: FocusEventHandler<HTMLElement>;
41+
}
42+
43+
export interface UseTooltipEventsArgsClick {
44+
/**
45+
* Whether the tooltip will open/close on `hover` or `click` events.
46+
* Note: must match the value passed onto `tooltip`
47+
*/
48+
triggerEvent: typeof TriggerEvent.Click;
49+
50+
/**
51+
* A ref to the Tooltip element.
52+
* Optional when the trigger event is `hover`
53+
*/
54+
tooltipRef: RefObject<HTMLElement>;
55+
56+
/**
57+
* Additional side effects to run on this event
58+
*/
59+
onClick?: MouseEventHandler<HTMLElement>;
60+
}
61+
62+
export type UseTooltipEventsArgs<Trigger extends TriggerEvent> =
63+
UseTooltipEventsBaseArgs &
64+
(Trigger extends 'hover'
65+
? UseTooltipEventsArgsHover
66+
: Trigger extends 'click'
67+
? UseTooltipEventsArgsClick
68+
: never);
69+
70+
export interface TooltipHoverEvents {
71+
onMouseEnter: MouseEventHandler<HTMLElement>;
72+
onMouseLeave: MouseEventHandler<HTMLElement>;
73+
onFocus: FocusEventHandler<HTMLElement>;
74+
onBlur: FocusEventHandler<HTMLElement>;
75+
onClick: undefined;
76+
}
77+
export interface TooltipClickEvents {
78+
onClick: MouseEventHandler<HTMLElement>;
79+
onMouseEnter: undefined;
80+
onMouseLeave: undefined;
81+
onFocus: undefined;
82+
onBlur: undefined;
83+
}
84+
85+
export type TooltipEventHandlers<Trigger extends TriggerEvent> =
86+
Trigger extends 'hover'
87+
? TooltipHoverEvents
88+
: Trigger extends 'click'
89+
? TooltipClickEvents
90+
: never;

0 commit comments

Comments
 (0)