Skip to content

Commit 5b64278

Browse files
feat: expose tooltip ref (imperative mode)
1 parent d7f98e8 commit 5b64278

File tree

5 files changed

+411
-309
lines changed

5 files changed

+411
-309
lines changed

src/App.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* eslint-disable jsx-a11y/no-static-element-interactions */
22
/* eslint-disable jsx-a11y/click-events-have-key-events */
33
import { TooltipController as Tooltip } from 'components/TooltipController'
4-
import { IPosition } from 'components/Tooltip/TooltipTypes.d'
5-
import React, { useState } from 'react'
4+
import { IPosition, TooltipImperativeProps } from 'components/Tooltip/TooltipTypes.d'
5+
import React, { useEffect, useRef, useState } from 'react'
66
import { inline, offset } from '@floating-ui/dom'
77
import styles from './styles.module.css'
88

@@ -11,6 +11,7 @@ function App() {
1111
const [isDarkOpen, setIsDarkOpen] = useState(false)
1212
const [position, setPosition] = useState<IPosition>({ x: 0, y: 0 })
1313
const [toggle, setToggle] = useState(false)
14+
const tooltipRef = useRef<TooltipImperativeProps>(null)
1415

1516
const handlePositionClick: React.MouseEventHandler<HTMLDivElement> = (event) => {
1617
const x = event.clientX
@@ -23,6 +24,19 @@ function App() {
2324
setAnchorId(target.id)
2425
}
2526

27+
useEffect(() => {
28+
const handleQ = (event: KeyboardEvent) => {
29+
if (event.key === 'q') {
30+
// q
31+
tooltipRef.current?.close()
32+
}
33+
}
34+
window.addEventListener('keydown', handleQ)
35+
return () => {
36+
window.removeEventListener('keydown', handleQ)
37+
}
38+
})
39+
2640
return (
2741
<main className={styles['main']}>
2842
<button
@@ -86,6 +100,7 @@ function App() {
86100
</p>
87101
<Tooltip id="anchor-select">Tooltip content</Tooltip>
88102
<Tooltip
103+
ref={tooltipRef}
89104
anchorSelect="section[id='section-anchor-select'] > p > button"
90105
place="bottom"
91106
events={['click']}
@@ -140,6 +155,25 @@ function App() {
140155
positionStrategy="fixed"
141156
/>
142157
</div>
158+
<button
159+
id="imperativeTooltipButton"
160+
style={{ height: 40, marginLeft: 100 }}
161+
onClick={() => {
162+
tooltipRef.current?.open({
163+
anchorSelect: '#imperativeTooltipButton',
164+
content: (
165+
<div style={{ fontSize: 32 }}>
166+
Opened imperatively!
167+
<br />
168+
<br />
169+
Press Q to close imperatively too!
170+
</div>
171+
),
172+
})
173+
}}
174+
>
175+
imperative tooltip
176+
</button>
143177
</div>
144178

145179
<div style={{ marginTop: '1rem' }}>

src/components/Tooltip/Tooltip.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useRef, useCallback } from 'react'
1+
import React, { useEffect, useState, useRef, useCallback, useImperativeHandle } from 'react'
22
import { autoUpdate } from '@floating-ui/dom'
33
import classNames from 'classnames'
44
import debounce from 'utils/debounce'
@@ -8,10 +8,11 @@ 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 { IPosition, ITooltip, PlacesType, TooltipImperativeOpenOptions } from './TooltipTypes'
1212

1313
const Tooltip = ({
1414
// props
15+
forwardRef,
1516
id,
1617
className,
1718
classNameArrow,
@@ -58,6 +59,9 @@ const Tooltip = ({
5859
const [inlineArrowStyles, setInlineArrowStyles] = useState({})
5960
const [show, setShow] = useState(false)
6061
const [rendered, setRendered] = useState(false)
62+
const [imperativeOptions, setImperativeOptions] = useState<TooltipImperativeOpenOptions | null>(
63+
null,
64+
)
6165
const wasShowing = useRef(false)
6266
const lastFloatPosition = useRef<IPosition | null>(null)
6367
/**
@@ -149,6 +153,7 @@ const Tooltip = ({
149153
if (show) {
150154
afterShow?.()
151155
} else {
156+
setImperativeOptions(null)
152157
afterHide?.()
153158
}
154159
}, [show])
@@ -274,6 +279,9 @@ const Tooltip = ({
274279
}
275280

276281
const handleClickOutsideAnchors = (event: MouseEvent) => {
282+
if (!show) {
283+
return
284+
}
277285
const anchorById = document.querySelector<HTMLElement>(`[id='${anchorId}']`)
278286
const anchors = [anchorById, ...anchorsBySelect]
279287
if (anchors.some((anchor) => anchor?.contains(event.target as HTMLElement))) {
@@ -293,9 +301,10 @@ const Tooltip = ({
293301
const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50, true)
294302
const debouncedHandleHideTooltip = debounce(handleHideTooltip, 50, true)
295303
const updateTooltipPosition = useCallback(() => {
296-
if (position) {
304+
const actualPosition = imperativeOptions?.position ?? position
305+
if (actualPosition) {
297306
// if `position` is set, override regular and `float` positioning
298-
handleTooltipPosition(position)
307+
handleTooltipPosition(actualPosition)
299308
return
300309
}
301310

@@ -349,6 +358,7 @@ const Tooltip = ({
349358
offset,
350359
positionStrategy,
351360
position,
361+
imperativeOptions?.position,
352362
float,
353363
])
354364

@@ -484,7 +494,7 @@ const Tooltip = ({
484494
])
485495

486496
useEffect(() => {
487-
let selector = anchorSelect ?? ''
497+
let selector = imperativeOptions?.anchorSelect ?? anchorSelect ?? ''
488498
if (!selector && id) {
489499
selector = `[data-tooltip-id='${id}']`
490500
}
@@ -584,7 +594,7 @@ const Tooltip = ({
584594
return () => {
585595
documentObserver.disconnect()
586596
}
587-
}, [id, anchorSelect, activeAnchor])
597+
}, [id, anchorSelect, imperativeOptions?.anchorSelect, activeAnchor])
588598

589599
useEffect(() => {
590600
updateTooltipPosition()
@@ -628,7 +638,7 @@ const Tooltip = ({
628638
}, [])
629639

630640
useEffect(() => {
631-
let selector = anchorSelect
641+
let selector = imperativeOptions?.anchorSelect ?? anchorSelect
632642
if (!selector && id) {
633643
selector = `[data-tooltip-id='${id}']`
634644
}
@@ -642,9 +652,34 @@ const Tooltip = ({
642652
// warning was already issued in the controller
643653
setAnchorsBySelect([])
644654
}
645-
}, [id, anchorSelect])
655+
}, [id, anchorSelect, imperativeOptions?.anchorSelect])
656+
657+
const actualContent = imperativeOptions?.content ?? content
658+
const canShow = Boolean(!hidden && actualContent && show && Object.keys(inlineStyles).length > 0)
646659

647-
const canShow = !hidden && content && show && Object.keys(inlineStyles).length > 0
660+
useImperativeHandle(forwardRef, () => ({
661+
open: (options) => {
662+
if (options?.anchorSelect) {
663+
try {
664+
document.querySelector(options.anchorSelect)
665+
} catch {
666+
if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') {
667+
// eslint-disable-next-line no-console
668+
console.warn(`[react-tooltip] "${options.anchorSelect}" is not a valid CSS selector`)
669+
}
670+
return
671+
}
672+
}
673+
setImperativeOptions(options ?? null)
674+
handleShow(true)
675+
},
676+
close: () => {
677+
handleShow(false)
678+
},
679+
activeAnchor,
680+
place: actualPlacement,
681+
isOpen: rendered && canShow,
682+
}))
648683

649684
return rendered ? (
650685
<WrapperElement
@@ -671,7 +706,7 @@ const Tooltip = ({
671706
}}
672707
ref={tooltipRef}
673708
>
674-
{content}
709+
{actualContent}
675710
<WrapperElement
676711
className={classNames(
677712
'react-tooltip-arrow',

src/components/Tooltip/TooltipTypes.d.ts

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

52+
export interface TooltipImperativeOpenOptions {
53+
anchorSelect?: string
54+
position?: IPosition
55+
content?: ChildrenType
56+
}
57+
58+
export interface TooltipImperativeProps {
59+
open: (options?: TooltipImperativeOpenOptions) => void
60+
close: () => void
61+
/**
62+
* @readonly
63+
*/
64+
activeAnchor: HTMLElement | null
65+
/**
66+
* @readonly
67+
*/
68+
place: PlacesType
69+
/**
70+
* @readonly
71+
*/
72+
isOpen: boolean
73+
}
74+
5275
export interface ITooltip {
76+
forwardRef?: React.ForwardedRef<TooltipImperativeProps>
5377
className?: string
5478
classNameArrow?: string
5579
content?: ChildrenType

0 commit comments

Comments
 (0)