Skip to content

Commit 2a3a1fe

Browse files
committed
feat: add autoClose prop and logic
1 parent 7d3bed0 commit 2a3a1fe

8 files changed

Lines changed: 110 additions & 0 deletions

File tree

docs/docs/options.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { Tooltip } from 'react-tooltip';
6666
| data-tooltip-position-strategy | string | false | `absolute` | `absolute` `fixed` | The position strategy used for the tooltip. Set to `fixed` if you run into issues with `overflow: hidden` on the tooltip parent container |
6767
| data-tooltip-delay-show | number | false | | any `number` | The delay (in ms) before showing the tooltip |
6868
| data-tooltip-delay-hide | number | false | | any `number` | The delay (in ms) before hiding the tooltip |
69+
| data-tooltip-auto-close | number | false | | any positive `number` | Automatically closes the tooltip after the given time (in ms), even if the anchor is still hovered |
6970
| data-tooltip-float | boolean | false | `false` | `true` `false` | Tooltip will follow the mouse position when it moves inside the anchor element (same as V4's `effect="float"`) |
7071
| data-tooltip-hidden | boolean | false | `false` | `true` `false` | Tooltip will not be shown |
7172
| data-tooltip-class-name | string | false | | | Classnames for the tooltip container |
@@ -107,6 +108,7 @@ import { Tooltip } from 'react-tooltip';
107108
| `positionStrategy` | `string` | no | `absolute` | `absolute` `fixed` | The position strategy used for the tooltip. Set to `fixed` if you run into issues with `overflow: hidden` on the tooltip parent container |
108109
| `delayShow` | `number` | no | | any `number` | The delay (in ms) before showing the tooltip |
109110
| `delayHide` | `number` | no | | any `number` | The delay (in ms) before hiding the tooltip |
111+
| `autoClose` | `number` | no | | any positive `number` | Automatically closes the tooltip after the given time (in ms). When the active anchor changes, the timer starts again for the new anchor |
110112
| `float` | `boolean` | no | `false` | `true` `false` | Tooltip will follow the mouse position when it moves inside the anchor element (same as V4's `effect="float"`) |
111113
| `hidden` | `boolean` | no | `false` | `true` `false` | Tooltip will not be shown |
112114
| `noArrow` | `boolean` | no | `false` | `true` `false` | Tooltip arrow will not be shown |

docs/docs/upgrade-guide/changelog-v5-v6.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ V6 keeps the core tooltip behavior from V5, but updates the implementation and A
1414
- runtime behavior is lighter and more scalable in larger interfaces
1515
- React 19 is supported while React 16.14+ remains compatible
1616
- optional `portalRoot` support lets you render the tooltip into a custom DOM container when you need tighter control over clipping and overlay layout
17+
- optional `autoClose` support lets a tooltip dismiss itself after a fixed visible duration without custom timeout wiring
1718

1819
## Breaking Changes
1920

@@ -25,6 +26,7 @@ V6 keeps the core tooltip behavior from V5, but updates the implementation and A
2526

2627
- `children` and `render` are the preferred way to render rich tooltip content in v6
2728
- `portalRoot` is available when the tooltip should render into a specific DOM node, such as `document.body`
29+
- `autoClose` is available when a tooltip should close after a fixed delay, including cases where the pointer is still over the anchor
2830
- v6 includes internal runtime improvements that reduce mount cost, memory retention, and shipped bundle size relative to v5
2931

3032
## `portalRoot`
@@ -44,6 +46,16 @@ When a layout clips overlays or makes stacking difficult, you can render the too
4446

4547
When portaling to `document.body`, `positionStrategy="fixed"` is the safest default because it avoids most coordinate-space and overflow issues.
4648

49+
## `autoClose`
50+
51+
If a tooltip should remain visible only for a fixed amount of time, you can let the component close itself:
52+
53+
```jsx
54+
<Tooltip id="my-tooltip" content="Hello" autoClose={5000} />
55+
```
56+
57+
This is useful when the tooltip should dismiss after a short reading window even if the user keeps hovering the same anchor. When the active anchor changes, the timer starts again for the newly active anchor.
58+
4759
## What should I use instead?
4860

4961
### Replace `data-tooltip-html` with tooltip `children`
@@ -97,5 +109,6 @@ If you previously used HTML strings for multiline tooltips, render JSX instead:
97109
- `content` is still supported for plain string content
98110
- `children` and `render` are now the recommended way to display rich tooltip content
99111
- `portalRoot` is optional and only needed when you want the tooltip rendered outside its default location
112+
- `autoClose` is optional and only needed when the tooltip should dismiss itself after a fixed visible duration
100113

101114
Check the current examples for [children](../examples/children.mdx), [render](../examples/render.mdx), and [multiline content](../examples/multiline.mdx).

docs/versioned_docs/version-5.x/upgrade-guide/changelog-v5-v6.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ V6 keeps the core tooltip behavior from V5, but updates the implementation and A
1414
- runtime behavior is lighter and more scalable in larger interfaces
1515
- React 19 is supported while React 16.14+ remains compatible
1616
- optional `portalRoot` support lets you render the tooltip into a custom DOM container when you need tighter control over clipping and overlay layout
17+
- optional `autoClose` support lets a tooltip dismiss itself after a fixed visible duration without custom timeout wiring
1718

1819
## Breaking Changes
1920

@@ -25,6 +26,7 @@ V6 keeps the core tooltip behavior from V5, but updates the implementation and A
2526

2627
- `children` and `render` are the preferred way to render rich tooltip content in v6
2728
- `portalRoot` is available when the tooltip should render into a specific DOM node, such as `document.body`
29+
- `autoClose` is available when a tooltip should close after a fixed delay, including cases where the pointer is still over the anchor
2830
- v6 includes internal runtime improvements that reduce mount cost, memory retention, and shipped bundle size relative to v5
2931

3032
## `portalRoot`
@@ -44,6 +46,16 @@ When a layout clips overlays or makes stacking difficult, you can render the too
4446

4547
When portaling to `document.body`, `positionStrategy="fixed"` is the safest default because it avoids most coordinate-space and overflow issues.
4648

49+
## `autoClose`
50+
51+
If a tooltip should remain visible only for a fixed amount of time, you can let the component close itself:
52+
53+
```jsx
54+
<Tooltip id="my-tooltip" content="Hello" autoClose={5000} />
55+
```
56+
57+
This is useful when the tooltip should dismiss after a short reading window even if the user keeps hovering the same anchor. When the active anchor changes, the timer starts again for the newly active anchor.
58+
4759
## What should I use instead?
4860

4961
### Replace `data-tooltip-html` with tooltip `children`
@@ -97,5 +109,6 @@ If you previously used HTML strings for multiline tooltips, render JSX instead:
97109
- `content` is still supported for plain string content
98110
- `children` and `render` are now the recommended way to display rich tooltip content
99111
- `portalRoot` is optional and only needed when you want the tooltip rendered outside its default location
112+
- `autoClose` is optional and only needed when the tooltip should dismiss itself after a fixed visible duration
100113

101114
Check the current examples for [children](../examples/children.mdx), [render](../examples/render.mdx), and [multiline content](../examples/multiline.mdx).

src/components/Tooltip/Tooltip.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const Tooltip = ({
3232
wrapper: WrapperElement,
3333
delayShow = 0,
3434
delayHide = 0,
35+
autoClose,
3536
float = false,
3637
hidden = false,
3738
noArrow = false,
@@ -64,6 +65,7 @@ const Tooltip = ({
6465
const tooltipArrowRef = useRef<HTMLElement>(null)
6566
const tooltipShowDelayTimerRef = useRef<NodeJS.Timeout | null>(null)
6667
const tooltipHideDelayTimerRef = useRef<NodeJS.Timeout | null>(null)
68+
const tooltipAutoCloseTimerRef = useRef<NodeJS.Timeout | null>(null)
6769
const missedTransitionTimerRef = useRef<NodeJS.Timeout | null>(null)
6870
const [computedPosition, setComputedPosition] = useState<IComputedPosition>({
6971
tooltipStyles: {},
@@ -199,6 +201,24 @@ const Tooltip = ({
199201
}
200202
}, [afterHide, afterShow, show])
201203

204+
useEffect(() => {
205+
clearTimeoutRef(tooltipAutoCloseTimerRef)
206+
207+
if (!show || !autoClose || autoClose <= 0) {
208+
return () => {
209+
clearTimeoutRef(tooltipAutoCloseTimerRef)
210+
}
211+
}
212+
213+
tooltipAutoCloseTimerRef.current = setTimeout(() => {
214+
handleShow(false)
215+
}, autoClose)
216+
217+
return () => {
218+
clearTimeoutRef(tooltipAutoCloseTimerRef)
219+
}
220+
}, [activeAnchor, autoClose, handleShow, show])
221+
202222
const handleComputedPosition = useCallback((newComputedPosition: IComputedPosition) => {
203223
if (!mounted.current) {
204224
return
@@ -353,6 +373,7 @@ const Tooltip = ({
353373
setActiveAnchor(null)
354374
clearTimeoutRef(tooltipShowDelayTimerRef)
355375
clearTimeoutRef(tooltipHideDelayTimerRef)
376+
clearTimeoutRef(tooltipAutoCloseTimerRef)
356377
}, [handleShow, setActiveAnchor])
357378

358379
const anchorElements = useTooltipAnchors({
@@ -456,6 +477,7 @@ const Tooltip = ({
456477
return () => {
457478
clearTimeoutRef(tooltipShowDelayTimerRef)
458479
clearTimeoutRef(tooltipHideDelayTimerRef)
480+
clearTimeoutRef(tooltipAutoCloseTimerRef)
459481
clearTimeoutRef(missedTransitionTimerRef)
460482
}
461483
}, [defaultIsOpen, handleShow])
@@ -519,6 +541,7 @@ const Tooltip = ({
519541
// Final cleanup to ensure no memory leaks
520542
clearTimeoutRef(tooltipShowDelayTimerRef)
521543
clearTimeoutRef(tooltipHideDelayTimerRef)
544+
clearTimeoutRef(tooltipAutoCloseTimerRef)
522545
clearTimeoutRef(missedTransitionTimerRef)
523546
}
524547
}, [])

src/components/Tooltip/TooltipTypes.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type DataAttribute =
2929
| 'position-strategy'
3030
| 'delay-show'
3131
| 'delay-hide'
32+
| 'auto-close'
3233
| 'float'
3334
| 'hidden'
3435
| 'class-name'
@@ -112,6 +113,7 @@ export interface ITooltip {
112113
middlewares?: Middleware[]
113114
delayShow?: number
114115
delayHide?: number
116+
autoClose?: number
115117
float?: boolean
116118
hidden?: boolean
117119
noArrow?: boolean

src/components/TooltipController/TooltipController.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const TooltipController = React.forwardRef<TooltipRefProps, ITooltipController>(
3232
middlewares,
3333
delayShow = 0,
3434
delayHide = 0,
35+
autoClose,
3536
float = false,
3637
hidden = false,
3738
noArrow = false,
@@ -182,6 +183,10 @@ const TooltipController = React.forwardRef<TooltipRefProps, ITooltipController>(
182183
anchorDataAttributes['delay-hide'] == null
183184
? delayHide
184185
: Number(anchorDataAttributes['delay-hide'])
186+
const tooltipAutoClose =
187+
anchorDataAttributes['auto-close'] == null
188+
? autoClose
189+
: Number(anchorDataAttributes['auto-close'])
185190
const tooltipFloat =
186191
anchorDataAttributes.float == null ? float : anchorDataAttributes.float === 'true'
187192
const tooltipHidden =
@@ -220,6 +225,7 @@ const TooltipController = React.forwardRef<TooltipRefProps, ITooltipController>(
220225
middlewares,
221226
delayShow: tooltipDelayShow,
222227
delayHide: tooltipDelayHide,
228+
autoClose: tooltipAutoClose,
223229
float: tooltipFloat,
224230
hidden: tooltipHidden,
225231
noArrow,

src/components/TooltipController/TooltipControllerTypes.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface ITooltipController {
3030
middlewares?: Middleware[]
3131
delayShow?: number
3232
delayHide?: number
33+
autoClose?: number
3334
float?: boolean
3435
hidden?: boolean
3536
noArrow?: boolean
@@ -84,6 +85,7 @@ declare module 'react' {
8485
'data-tooltip-position-strategy'?: PositionStrategy
8586
'data-tooltip-delay-show'?: number
8687
'data-tooltip-delay-hide'?: number
88+
'data-tooltip-auto-close'?: number
8789
'data-tooltip-float'?: boolean
8890
'data-tooltip-hidden'?: boolean
8991
'data-tooltip-class-name'?: string

src/test/tooltip-close-and-delay-behavior.spec.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,55 @@ describe('tooltip close and delay behavior', () => {
260260
expect(document.getElementById('rehydrated-hide-delay-test')).toBeInTheDocument()
261261
})
262262

263+
test('closes automatically after the configured time while still hovering the anchor', async () => {
264+
render(
265+
<>
266+
<span data-tooltip-id="auto-close-test">Hover Me</span>
267+
<TooltipController id="auto-close-test" content="Auto Close Test" autoClose={5000} />
268+
</>,
269+
)
270+
271+
const anchor = screen.getByText('Hover Me')
272+
273+
hoverAnchor(anchor, 100)
274+
await waitForTooltip('auto-close-test')
275+
276+
advanceTimers(4900)
277+
expect(document.getElementById('auto-close-test')).toBeInTheDocument()
278+
279+
advanceTimers(200)
280+
await waitForTooltipToClose('auto-close-test')
281+
})
282+
283+
test('restarts the auto-close timer when the active anchor changes', async () => {
284+
render(
285+
<>
286+
<span data-tooltip-id="auto-close-reset-test">First Anchor</span>
287+
<span data-tooltip-id="auto-close-reset-test">Second Anchor</span>
288+
<TooltipController
289+
id="auto-close-reset-test"
290+
content="Auto Close Reset Test"
291+
autoClose={5000}
292+
/>
293+
</>,
294+
)
295+
296+
const firstAnchor = screen.getByText('First Anchor')
297+
const secondAnchor = screen.getByText('Second Anchor')
298+
299+
hoverAnchor(firstAnchor, 100)
300+
await waitForTooltip('auto-close-reset-test')
301+
302+
advanceTimers(3000)
303+
hoverAnchor(secondAnchor, 100)
304+
305+
advanceTimers(3000)
306+
expect(document.getElementById('auto-close-reset-test')).toBeInTheDocument()
307+
308+
advanceTimers(2200)
309+
await waitForTooltipToClose('auto-close-reset-test')
310+
})
311+
263312
test('cancels a pending show timer when the anchor hides first', () => {
264313
render(
265314
<>

0 commit comments

Comments
 (0)