Skip to content

Commit 8e786af

Browse files
feat: open/close events
1 parent bd5fddf commit 8e786af

File tree

5 files changed

+171
-40
lines changed

5 files changed

+171
-40
lines changed

src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ function App() {
8888
<Tooltip
8989
anchorSelect="section[id='section-anchor-select'] > p > button"
9090
place="bottom"
91-
events={['click']}
91+
openEvents={{ click: true }}
92+
closeEvents={{ click: true }}
93+
globalCloseEvents={{ clickOutsideAnchor: true }}
9294
>
9395
Tooltip content
9496
</Tooltip>

src/components/Tooltip/Tooltip.tsx

Lines changed: 119 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import { getScrollParent } from 'utils/get-scroll-parent'
88
import { computeTooltipPosition } from 'utils/compute-positions'
99
import coreStyles from './core-styles.module.css'
1010
import styles from './styles.module.css'
11-
import type { IPosition, ITooltip, PlacesType } from './TooltipTypes'
11+
import type {
12+
AnchorCloseEvents,
13+
AnchorOpenEvents,
14+
GlobalCloseEvents,
15+
IPosition,
16+
ITooltip,
17+
PlacesType,
18+
} from './TooltipTypes'
1219

1320
const Tooltip = ({
1421
// props
@@ -34,6 +41,9 @@ const Tooltip = ({
3441
closeOnEsc = false,
3542
closeOnScroll = false,
3643
closeOnResize = false,
44+
openEvents,
45+
closeEvents,
46+
globalCloseEvents,
3747
style: externalStyles,
3848
position,
3949
afterShow,
@@ -68,7 +78,49 @@ const Tooltip = ({
6878
const [anchorsBySelect, setAnchorsBySelect] = useState<HTMLElement[]>([])
6979
const mounted = useRef(false)
7080

81+
/**
82+
* @todo Update when deprecated stuff gets removed.
83+
*/
7184
const shouldOpenOnClick = openOnClick || events.includes('click')
85+
const hasClickEvent =
86+
shouldOpenOnClick || openEvents?.click || openEvents?.dblclick || openEvents?.mousedown
87+
const actualOpenEvents: AnchorOpenEvents = openEvents
88+
? { ...openEvents }
89+
: {
90+
mouseenter: true,
91+
focus: true,
92+
click: false,
93+
dblclick: false,
94+
mousedown: false,
95+
}
96+
if (!openEvents && shouldOpenOnClick) {
97+
Object.assign(actualOpenEvents, {
98+
mouseenter: false,
99+
focus: false,
100+
click: true,
101+
})
102+
}
103+
const actualCloseEvents: AnchorCloseEvents = closeEvents
104+
? { ...closeEvents }
105+
: {
106+
mouseleave: true,
107+
blur: true,
108+
click: false,
109+
}
110+
if (!closeEvents && shouldOpenOnClick) {
111+
Object.assign(actualCloseEvents, {
112+
mouseleave: false,
113+
blur: false,
114+
})
115+
}
116+
const actualGlobalCloseEvents: GlobalCloseEvents = globalCloseEvents
117+
? { ...globalCloseEvents }
118+
: {
119+
escape: closeOnEsc || false,
120+
scroll: closeOnScroll || false,
121+
resize: closeOnResize || false,
122+
clickOutsideAnchor: hasClickEvent || false,
123+
}
72124

73125
/**
74126
* useLayoutEffect runs before useEffect,
@@ -248,13 +300,6 @@ const Tooltip = ({
248300
lastFloatPosition.current = mousePosition
249301
}
250302

251-
const handleClickTooltipAnchor = (event?: Event) => {
252-
handleShowTooltip(event)
253-
if (delayHide) {
254-
handleHideTooltipDelayed()
255-
}
256-
}
257-
258303
const handleClickOutsideAnchors = (event: MouseEvent) => {
259304
const anchorById = document.querySelector<HTMLElement>(`[id='${anchorId}']`)
260305
const anchors = [anchorById, ...anchorsBySelect]
@@ -353,13 +398,13 @@ const Tooltip = ({
353398
const anchorScrollParent = getScrollParent(activeAnchor)
354399
const tooltipScrollParent = getScrollParent(tooltipRef.current)
355400

356-
if (closeOnScroll) {
401+
if (actualGlobalCloseEvents.scroll) {
357402
window.addEventListener('scroll', handleScrollResize)
358403
anchorScrollParent?.addEventListener('scroll', handleScrollResize)
359404
tooltipScrollParent?.addEventListener('scroll', handleScrollResize)
360405
}
361406
let updateTooltipCleanup: null | (() => void) = null
362-
if (closeOnResize) {
407+
if (actualGlobalCloseEvents.resize) {
363408
window.addEventListener('resize', handleScrollResize)
364409
} else if (activeAnchor && tooltipRef.current) {
365410
updateTooltipCleanup = autoUpdate(
@@ -380,29 +425,63 @@ const Tooltip = ({
380425
}
381426
handleShow(false)
382427
}
383-
384-
if (closeOnEsc) {
428+
if (actualGlobalCloseEvents.escape) {
385429
window.addEventListener('keydown', handleEsc)
386430
}
387431

432+
if (actualGlobalCloseEvents.clickOutsideAnchor) {
433+
window.addEventListener('click', handleClickOutsideAnchors)
434+
}
435+
388436
const enabledEvents: { event: string; listener: (event?: Event) => void }[] = []
389437

390-
if (shouldOpenOnClick) {
391-
window.addEventListener('click', handleClickOutsideAnchors)
392-
enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor })
393-
} else {
394-
enabledEvents.push(
395-
{ event: 'mouseenter', listener: debouncedHandleShowTooltip },
396-
{ event: 'mouseleave', listener: debouncedHandleHideTooltip },
397-
{ event: 'focus', listener: debouncedHandleShowTooltip },
398-
{ event: 'blur', listener: debouncedHandleHideTooltip },
399-
)
400-
if (float) {
401-
enabledEvents.push({
402-
event: 'mousemove',
403-
listener: handleMouseMove,
404-
})
438+
const handleClickOpenTooltipAnchor = (event?: Event) => {
439+
if (show) {
440+
return
405441
}
442+
handleShowTooltip(event)
443+
}
444+
const handleClickCloseTooltipAnchor = () => {
445+
if (!show) {
446+
return
447+
}
448+
handleHideTooltip()
449+
}
450+
451+
const regularEvents = ['mouseenter', 'mouseleave', 'focus', 'blur']
452+
const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup']
453+
454+
Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
455+
if (!enabled) {
456+
return
457+
}
458+
if (regularEvents.includes(event)) {
459+
enabledEvents.push({ event, listener: debouncedHandleShowTooltip })
460+
} else if (clickEvents.includes(event)) {
461+
enabledEvents.push({ event, listener: handleClickOpenTooltipAnchor })
462+
} else {
463+
// never happens
464+
}
465+
})
466+
467+
Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
468+
if (!enabled) {
469+
return
470+
}
471+
if (regularEvents.includes(event)) {
472+
enabledEvents.push({ event, listener: debouncedHandleHideTooltip })
473+
} else if (clickEvents.includes(event)) {
474+
enabledEvents.push({ event, listener: handleClickCloseTooltipAnchor })
475+
} else {
476+
// never happens
477+
}
478+
})
479+
480+
if (float) {
481+
enabledEvents.push({
482+
event: 'mousemove',
483+
listener: handleMouseMove,
484+
})
406485
}
407486

408487
const handleMouseEnterTooltip = () => {
@@ -413,7 +492,9 @@ const Tooltip = ({
413492
handleHideTooltip()
414493
}
415494

416-
if (clickable && !shouldOpenOnClick) {
495+
if (clickable && !hasClickEvent) {
496+
// used to keep the tooltip open when hovering content.
497+
// not needed if using click events.
417498
tooltipRef.current?.addEventListener('mouseenter', handleMouseEnterTooltip)
418499
tooltipRef.current?.addEventListener('mouseleave', handleMouseLeaveTooltip)
419500
}
@@ -425,23 +506,23 @@ const Tooltip = ({
425506
})
426507

427508
return () => {
428-
if (closeOnScroll) {
509+
if (actualGlobalCloseEvents.scroll) {
429510
window.removeEventListener('scroll', handleScrollResize)
430511
anchorScrollParent?.removeEventListener('scroll', handleScrollResize)
431512
tooltipScrollParent?.removeEventListener('scroll', handleScrollResize)
432513
}
433-
if (closeOnResize) {
514+
if (actualGlobalCloseEvents.resize) {
434515
window.removeEventListener('resize', handleScrollResize)
435516
} else {
436517
updateTooltipCleanup?.()
437518
}
438-
if (shouldOpenOnClick) {
519+
if (actualGlobalCloseEvents.clickOutsideAnchor) {
439520
window.removeEventListener('click', handleClickOutsideAnchors)
440521
}
441-
if (closeOnEsc) {
522+
if (actualGlobalCloseEvents.escape) {
442523
window.removeEventListener('keydown', handleEsc)
443524
}
444-
if (clickable && !shouldOpenOnClick) {
525+
if (clickable && !hasClickEvent) {
445526
tooltipRef.current?.removeEventListener('mouseenter', handleMouseEnterTooltip)
446527
tooltipRef.current?.removeEventListener('mouseleave', handleMouseLeaveTooltip)
447528
}
@@ -461,8 +542,11 @@ const Tooltip = ({
461542
rendered,
462543
anchorRefs,
463544
anchorsBySelect,
464-
closeOnEsc,
465-
events,
545+
// the effect uses the `actual*Events` objects, but this should work
546+
openEvents,
547+
closeEvents,
548+
globalCloseEvents,
549+
shouldOpenOnClick,
466550
])
467551

468552
useEffect(() => {

src/components/Tooltip/TooltipTypes.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ export interface IPosition {
4949
y: number
5050
}
5151

52+
export type AnchorOpenEvents = {
53+
mouseenter?: boolean
54+
focus?: boolean
55+
click?: boolean
56+
dblclick?: boolean
57+
mousedown?: boolean
58+
}
59+
export type AnchorCloseEvents = {
60+
mouseleave?: boolean
61+
blur?: boolean
62+
click?: boolean
63+
dblclick?: boolean
64+
mouseup?: boolean
65+
}
66+
export type GlobalCloseEvents = {
67+
escape?: boolean
68+
scroll?: boolean
69+
resize?: boolean
70+
clickOutsideAnchor?: boolean
71+
}
72+
5273
export interface ITooltip {
5374
className?: string
5475
classNameArrow?: string
@@ -81,6 +102,9 @@ export interface ITooltip {
81102
closeOnEsc?: boolean
82103
closeOnScroll?: boolean
83104
closeOnResize?: boolean
105+
openEvents?: AnchorOpenEvents
106+
closeEvents?: AnchorCloseEvents
107+
globalCloseEvents?: GlobalCloseEvents
84108
style?: CSSProperties
85109
position?: IPosition
86110
isOpen?: boolean

src/components/TooltipController/TooltipController.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ const TooltipController = ({
4141
closeOnEsc = false,
4242
closeOnScroll = false,
4343
closeOnResize = false,
44+
openEvents,
45+
closeEvents,
46+
globalCloseEvents,
4447
style,
4548
position,
4649
isOpen,
@@ -330,6 +333,9 @@ const TooltipController = ({
330333
closeOnEsc,
331334
closeOnScroll,
332335
closeOnResize,
336+
openEvents,
337+
closeEvents,
338+
globalCloseEvents,
333339
style,
334340
position,
335341
isOpen,

src/components/TooltipController/TooltipControllerTypes.d.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type {
99
PositionStrategy,
1010
IPosition,
1111
Middleware,
12+
AnchorOpenEvents,
13+
AnchorCloseEvents,
14+
GlobalCloseEvents,
1215
} from 'components/Tooltip/TooltipTypes'
1316

1417
export interface ITooltipController {
@@ -33,7 +36,7 @@ export interface ITooltipController {
3336
wrapper?: WrapperType
3437
children?: ChildrenType
3538
/**
36-
* @deprecated Use `openOnClick` instead.
39+
* @deprecated Use `openOnClick` or `openEvents`/`closeEvents` instead.
3740
*/
3841
events?: EventsType[]
3942
openOnClick?: boolean
@@ -46,17 +49,29 @@ export interface ITooltipController {
4649
noArrow?: boolean
4750
clickable?: boolean
4851
/**
49-
* @todo refactor to `hideOnEsc` for naming consistency
52+
* @deprecated Use `globalCloseEvents={{ escape: true }}` instead.
5053
*/
5154
closeOnEsc?: boolean
5255
/**
53-
* @todo refactor to `hideOnScroll` for naming consistency
56+
* @deprecated Use `globalCloseEvents={{ scroll: true }}` instead.
5457
*/
5558
closeOnScroll?: boolean
5659
/**
57-
* @todo refactor to `hideOnResize` for naming consistency
60+
* @deprecated Use `globalCloseEvents={{ resize: true }}` instead.
5861
*/
5962
closeOnResize?: boolean
63+
/**
64+
* @description The events to be listened on anchor elements to open the tooltip.
65+
*/
66+
openEvents?: AnchorOpenEvents
67+
/**
68+
* @description The events to be listened on anchor elements to close the tooltip.
69+
*/
70+
closeEvents?: AnchorCloseEvents
71+
/**
72+
* @description The global events listened to close the tooltip.
73+
*/
74+
globalCloseEvents?: GlobalCloseEvents
6075
style?: CSSProperties
6176
position?: IPosition
6277
isOpen?: boolean

0 commit comments

Comments
 (0)