Skip to content

Track window resize to reposition tooltip #1075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions docs/docs/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,35 +64,6 @@ If you've imported the default styling and the tooltip is still not showing up w

If `data-tooltip-content` and `data-tooltip-html` are both unset (or they have empty values) on the anchor element, and also the `content`, `render`, and `children` props on the tooltip are unset (or have empty values), the tooltip is not shown by default.


## The tooltip doesn't move when scrolling

If your anchor element is inside a scrolling element, your tooltip might get "stuck" in place when scrolling.
There are two ways to avoid this.

### Change your CSS (recommended)

For the tooltip to be properly placed inside a scrolling element, the following conditions must be met:

1. The tooltip component has to be inside the scrolling element (placing it as a direct child is **not** required)
2. The `positionStrategy` tooltip prop must be unset, or set to the default (`absolute`)
3. The scrolling element should have the CSS attribute `position: relative`

:::info

The `position: relative` attribute can be set on any element on the DOM structure between the scrolling element and the tooltip.
This means the tooltip component doesn't have to be a direct child of the scrolling element.

:::

### Use `closeOnScroll` prop

```tsx
<Tooltip closeOnScroll={true} />
```

When `closeOnScroll` is set, scrolling will immediately close the tooltip (`closeOnResize` also exists for closing when resizing the window).

## Bad performance

If you're experiencing any kind of unexpected behavior or bad performance on your application when using ReactTooltip, here are a few things you can try.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@types/css-modules": "^1.0.2",
"@types/jest": "29.4.0",
"@types/node": "^18.15.3",
"@types/react": "18.0.28",
"@types/react": "^18.2.17",
"@types/react-dom": "18.0.11",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "5.54.0",
Expand Down
130 changes: 81 additions & 49 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useState, useRef } from 'react'
import React, { useEffect, useState, useRef, useCallback } from 'react'
import { autoUpdate } from '@floating-ui/dom'
import classNames from 'classnames'
import debounce from 'utils/debounce'
import { useTooltip } from 'components/TooltipProvider'
Expand Down Expand Up @@ -290,6 +291,61 @@ const Tooltip = ({
// mouse enter and focus events being triggered toggether
const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50, true)
const debouncedHandleHideTooltip = debounce(handleHideTooltip, 50, true)
const updateTooltipPosition = useCallback(() => {
if (position) {
// if `position` is set, override regular and `float` positioning
handleTooltipPosition(position)
return
}

if (float) {
if (lastFloatPosition.current) {
/*
Without this, changes to `content`, `place`, `offset`, ..., will only
trigger a position calculation after a `mousemove` event.

To see why this matters, comment this line, run `yarn dev` and click the
"Hover me!" anchor.
*/
handleTooltipPosition(lastFloatPosition.current)
}
// if `float` is set, override regular positioning
return
}

computeTooltipPosition({
place,
offset,
elementReference: activeAnchor,
tooltipReference: tooltipRef.current,
tooltipArrowReference: tooltipArrowRef.current,
strategy: positionStrategy,
middlewares,
border,
}).then((computedStylesData) => {
if (!mounted.current) {
// invalidate computed positions after remount
return
}
if (Object.keys(computedStylesData.tooltipStyles).length) {
setInlineStyles(computedStylesData.tooltipStyles)
}
if (Object.keys(computedStylesData.tooltipArrowStyles).length) {
setInlineArrowStyles(computedStylesData.tooltipArrowStyles)
}
setActualPlacement(computedStylesData.place as PlacesType)
})
}, [
show,
activeAnchor,
content,
externalStyles,
place,
offset,
positionStrategy,
position,
float,
])
Comment on lines +338 to +348
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to update dependencies here and on the main useEffect() further down to get some scenarios to work properly with autoUpdate().

After some testing it doesn't seem like anything broke, but there's a non-zero chance this will cause some random bugs we'll be running into soon.


useEffect(() => {
const elementRefs = new Set(anchorRefs)
Expand All @@ -315,8 +371,20 @@ const Tooltip = ({
anchorScrollParent?.addEventListener('scroll', handleScrollResize)
tooltipScrollParent?.addEventListener('scroll', handleScrollResize)
}
let updateTooltipCleanup: null | (() => void) = null
if (closeOnResize) {
window.addEventListener('resize', handleScrollResize)
} else if (activeAnchor && tooltipRef.current) {
updateTooltipCleanup = autoUpdate(
activeAnchor as HTMLElement,
tooltipRef.current as HTMLElement,
updateTooltipPosition,
{
ancestorResize: true,
elementResize: true,
layoutShift: true,
},
)
}

const handleEsc = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -377,6 +445,8 @@ const Tooltip = ({
}
if (closeOnResize) {
window.removeEventListener('resize', handleScrollResize)
} else {
updateTooltipCleanup?.()
}
if (shouldOpenOnClick) {
window.removeEventListener('click', handleClickOutsideAnchors)
Expand All @@ -398,7 +468,15 @@ const Tooltip = ({
* rendered is also a dependency to ensure anchor observers are re-registered
* since `tooltipRef` becomes stale after removing/adding the tooltip to the DOM
*/
}, [rendered, anchorRefs, anchorsBySelect, closeOnEsc, events])
}, [
activeAnchor,
updateTooltipPosition,
rendered,
anchorRefs,
anchorsBySelect,
closeOnEsc,
events,
])

useEffect(() => {
let selector = anchorSelect ?? ''
Expand Down Expand Up @@ -476,55 +554,9 @@ const Tooltip = ({
}
}, [id, anchorSelect, activeAnchor])

const updateTooltipPosition = () => {
if (position) {
// if `position` is set, override regular and `float` positioning
handleTooltipPosition(position)
return
}

if (float) {
if (lastFloatPosition.current) {
/*
Without this, changes to `content`, `place`, `offset`, ..., will only
trigger a position calculation after a `mousemove` event.

To see why this matters, comment this line, run `yarn dev` and click the
"Hover me!" anchor.
*/
handleTooltipPosition(lastFloatPosition.current)
}
// if `float` is set, override regular positioning
return
}

computeTooltipPosition({
place,
offset,
elementReference: activeAnchor,
tooltipReference: tooltipRef.current,
tooltipArrowReference: tooltipArrowRef.current,
strategy: positionStrategy,
middlewares,
border,
}).then((computedStylesData) => {
if (!mounted.current) {
// invalidate computed positions after remount
return
}
if (Object.keys(computedStylesData.tooltipStyles).length) {
setInlineStyles(computedStylesData.tooltipStyles)
}
if (Object.keys(computedStylesData.tooltipArrowStyles).length) {
setInlineArrowStyles(computedStylesData.tooltipArrowStyles)
}
setActualPlacement(computedStylesData.place as PlacesType)
})
}

useEffect(() => {
updateTooltipPosition()
}, [show, activeAnchor, content, externalStyles, place, offset, positionStrategy, position])
}, [updateTooltipPosition])

useEffect(() => {
if (!contentWrapperRef?.current) {
Expand Down
4 changes: 2 additions & 2 deletions src/test/__snapshots__/tooltip-attributes.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ exports[`tooltip attributes basic tooltip 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand All @@ -39,7 +39,7 @@ exports[`tooltip attributes tooltip with place 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: -4px; top: 5px;"
style="left: -4px; top: -1px;"
/>
</div>
</div>
Expand Down
14 changes: 7 additions & 7 deletions src/test/__snapshots__/tooltip-props.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ exports[`tooltip props basic tooltip 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand All @@ -38,7 +38,7 @@ exports[`tooltip props clickable tooltip 1`] = `
</button>
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand All @@ -59,7 +59,7 @@ exports[`tooltip props tooltip with custom position 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand Down Expand Up @@ -90,7 +90,7 @@ exports[`tooltip props tooltip with delay show 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand All @@ -111,7 +111,7 @@ exports[`tooltip props tooltip with float 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand All @@ -137,7 +137,7 @@ exports[`tooltip props tooltip with html 1`] = `
</span>
<div
class="react-tooltip-arrow"
style="left: 5px; bottom: -4px;"
style="left: -1px; bottom: -4px;"
/>
</div>
</div>
Expand All @@ -158,7 +158,7 @@ exports[`tooltip props tooltip with place 1`] = `
Hello World!
<div
class="react-tooltip-arrow"
style="left: -4px; top: 5px;"
style="left: -4px; top: -1px;"
/>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/test/__snapshots__/utils.spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`compute positions all reference elements 1`] = `
{
"place": "top",
"tooltipArrowStyles": {
"bottom": "-4px",
"left": "-1px",
"right": "",
"top": "",
},
"tooltipStyles": {
"border": undefined,
"left": "5px",
"top": "-10px",
},
}
`;
6 changes: 6 additions & 0 deletions src/test/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ import React from 'react'
// whatever else you need in here

global.React = React

global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}))
14 changes: 1 addition & 13 deletions src/test/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,7 @@ describe('compute positions', () => {
tooltipArrowReference: elementTooltipArrow,
})

expect(value).toEqual({
tooltipArrowStyles: {
bottom: '-4px',
left: '5px',
right: '',
top: '',
},
tooltipStyles: {
left: '5px',
top: '-10px',
},
place: 'top',
})
expect(value).toMatchSnapshot()
})
})

Expand Down
2 changes: 1 addition & 1 deletion src/utils/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @param { number } wait Time to wait before execut the function
* @param { boolean } immediate Param to define if the function will be executed immediately
*/
const debounce = (func: (...args: any[]) => void, wait?: number, immediate?: true) => {
const debounce = (func: (...args: any[]) => void, wait?: number, immediate?: boolean) => {
let timeout: NodeJS.Timeout | null = null

return function debounced(this: typeof func, ...args: any[]) {
Expand Down
Loading