Skip to content

feat: add v4's float effect (follow mouse position) #861

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
merged 20 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4fb2db1
feat: effect float POC
gabrieljablonski Dec 15, 2022
08775e9
feat[wip]: add type of tooltip as prop
danielbarion Dec 19, 2022
cc3f201
refactor: update tooltip position feature implementation
danielbarion Dec 20, 2022
eed1200
test: add tests to the new feature
danielbarion Dec 20, 2022
f028426
chore: add example into app.tsx about position feature
danielbarion Dec 20, 2022
8815eed
docs: update project readme
danielbarion Dec 20, 2022
dc4d404
fix: pad arrow position to avoid overflow
gabrieljablonski Dec 20, 2022
c2c222a
refactor: avoid eslint `no-empty-function` warning
gabrieljablonski Dec 20, 2022
9f5311a
chore: remove manual `float` example from dev demo
gabrieljablonski Dec 20, 2022
88465f8
feat: close `click` tooltip on click outside
gabrieljablonski Dec 20, 2022
fae8796
feat: use `delayHide` on `click` tooltip
gabrieljablonski Dec 20, 2022
7859ae3
refactor: `position` must have `x` and `y`
gabrieljablonski Dec 20, 2022
49fdb26
feat: add `float` mode (follow mouse)
gabrieljablonski Dec 20, 2022
7f290e4
test: account for added arrow padding
gabrieljablonski Dec 20, 2022
1e9ddd0
refactor: var naming
gabrieljablonski Dec 20, 2022
e8058a6
docs: `position`/`float` props and general improvements
gabrieljablonski Dec 20, 2022
f579913
Merge branch 'master' into tooltip-effect-float
gabrieljablonski Dec 20, 2022
c1d9ca3
refactor: use let instead of state for lastFloatPosition variable
danielbarion Dec 21, 2022
6a9a01d
refactor: `useRef()` for `lastFloatPosition`
gabrieljablonski Dec 21, 2022
c875241
chore: bump project version and docs package
gabrieljablonski Dec 21, 2022
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ import ReactDOMServer from 'react-dom/server';

[gabrieljablonski](https://github.com/gabrieljablonski) Maintainer.

[aronhelser](https://github.com/aronhelser) Passive maintainer - accepting PRs and doing minor testing, but not fixing issues or doing active development.
[aronhelser](https://github.com/aronhelser) (inactive).

[alexgurr](https://github.com/alexgurr) (inactive).

Expand Down
69 changes: 36 additions & 33 deletions docs/docs/options.mdx

Large diffs are not rendered by default.

98 changes: 50 additions & 48 deletions docs/docs/upgrade-guide/changelog-v4-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,76 @@ If you are using V4 and want to use V5, please read this doc.

## From V4 to V5

V4 was a great react tooltip component but was built a few years ago, he was built with react class components and it's hard to maintain and to the community give contributions, so, with this in mind, we build a new version of react tooltip using [float-ui](https://floating-ui.com/) behind the scenes. This gives a great improvement in performance and in a new and easier code to let the community contribute to the project.
V4 was a great react tooltip component but was built a few years ago, he was built with react class components and it's hard to maintain and to the community give contributions, so, with this in mind, we build a new version of react tooltip using [floating-ui](https://floating-ui.com/) behind the scenes. This gives a great improvement in performance and in a new and easier code to let the community contribute to the project.

## Improvements

- Dropped package dependency `uuid`
- - Using React `useId` - [Docs](https://reactjs.org/docs/hooks-reference.html#useid)
- - - Unfortunately `useId` was introduced only into React v18, so, that will be the minimum necessary version of React to V5
- - - Unfortunately `useId` was introduced only into React v18, so that is the minimum required React version to use V5
- Dropped package dependency `prop-types`
- V5 is written in TypeScript
- V5 has minified and unminified files available to be used as you want

## Break Changes
## Breaking Changes

- All data attributes now has `tooltip` into his name
- All data attributes are now prefixed with `data-tooltip-`
- Default Padding changed from `padding: 8px 21px;` to `padding: 8px 16px;`
- Exported module now is `Tooltip` instead of `ReactTooltip`
- - If you already have a `Tooltip` component in your application and want to explicitly declare this is `ReactTooltip`, just `import { Tooltip as ReactTooltip } from "react-tooltip"`
- CSS import is now optional, so, you can modify and/or add any styling to your floating tooltip element
- CSS import is now optional, so you can modify and/or add any styling to your floating tooltip element
- `data-tip` attribute now is `data-tooltip-content`
- `getContent` prop was removed. Instead, you can directly pass dynamic content to the `content` tooltip prop, or to `data-tooltip-content`
- default behavior of tooltip now is `solid` instead of `float`
- `getContent` prop was removed. Instead, you can directly pass dynamic content to the `content` tooltip prop, or to `data-tooltip-content` in the anchor element
- Default behavior of tooltip now is equivalent to V4's `solid` effect, instead of `float`. The new `float` prop can be set to achieve V4's `effect="float"`. See [Options](../options.mdx) for more details.

## New Props

- [x] classNameArrow
- [x] events - `data-tooltip-events` -`['hover', 'click']` - default: `['hover']` (always an array when using as prop, even with only one option, when using as data attribute: `data-tooltip-events="hover click"`)
- [x] isOpen - `boolean` (to control tooltip state) - if not used, internal state of tooltip will handle the show state
- [x] setIsOpen - `function` (to control tooltip state) - if not used, internal state of tooltip will handle the show state
- [x] `classNameArrow`
- [x] `events` (or `data-tooltip-events` on anchor element) - `['hover', 'click']` - default: `['hover']` (always an array when using as prop, even with only one option, when using as data attribute: `data-tooltip-events="hover click"`)
- [x] `isOpen` - `boolean` (to control tooltip state) - if not used, tooltip state will be handled internally
- [x] `setIsOpen` - `function` (to control tooltip state) - if not used, tooltip state will be handled internally
- [x] `position` - `{ x: number; y: number }` - similar to V4's `overridePosition`
- [x] `float` - `boolean` - used to achieve V4's `effect="float"`

## `V4` props available in `V5`

- [x] children
- [x] place - `data-tooltip-place`
- [x] type - **Deprecated** | in V5 -> `variant` - `data-tooltip-variant`
- [ ] effect - not implemented yet, if many users need this feature, we will work on this one.
- [x] offset - `data-tooltip-offset`
- [ ] padding - **Deprecated** | in V5 -> can be easy updated by className prop
- [ ] multiline - **Deprecated** | in V5 -> this is already supported as default by `content` and `html` props
- [ ] border - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] borderClass - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] textColor - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] backgroundColor - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] borderColor - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] arrowColor - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] arrowRadius - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] tooltipRadius - **Deprecated** | in V5 -> can be easy updated by `className` prop
- [ ] insecure - **Deprecated** | in V5 -> CSS will be a separate file and can be imported or not
- [x] className
- [x] id
- [x] html
- [x] delayHide - `data-tooltip-delay-hide`
- [ ] delayUpdate - **Deprecated** | if requested, can be implemented later
- [x] delayShow - `data-tooltip-delay-show`
- [ ] event - not implemented yet, if many users need this feature, we will work on this one.
- [ ] eventOff - **Deprecated**
- [ ] isCapture - **Deprecated**
- [ ] globalEventOff - **Deprecated**
- [ ] getContent - use dynamic `content` instead
- [ ] afterShow - not implemented yet, if many users need this feature, we will work on this one.
- [ ] afterHide - not implemented yet, if many users need this feature, we will work on this one.
- [ ] overridePosition - **Deprecated**
- [ ] disable - **Deprecated** | in V5 -> state can be controlled or uncontrolled
- [ ] scrollHide - not implemented yet, if many users need this feature, we will validate if we wrok on this one.
- [ ] resizeHide - not implemented yet, if many users need this feature, we will validate if we wrok on this one.
- [x] wrapper - `data-tooltip-wrapper`
- [ ] bodyMode - **Deprecated**
- [ ] clickable - **Deprecated** | Supported by default in V5
- [ ] disableInternalStyle - **Deprecated** | in V5 -> CSS will be a separate file and can be imported or not
- [x] `children`
- [x] `place` - also available on anchor element as `data-tooltip-place`
- [ ] `type` - use `variant`. also available on anchor element as `data-tooltip-variant`
Copy link
Member Author

Choose a reason for hiding this comment

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

Having both the empty brackets [ ] and **Deprecated** seemed redundant.

- [ ] `effect` - use `float` prop
- [x] `offset` - also available on anchor element as `data-tooltip-offset`
- [ ] `padding` - use `className` and custom CSS
- [ ] `multiline` - supported by default in `content` and `html` props
- [ ] `border` - use `className` and custom CSS
- [ ] `borderClass` - use `className` and custom CSS
- [ ] `textColor` - use `className` and custom CSS
- [ ] `backgroundColor` - use `className` and custom CSS
- [ ] `borderColor` - use `className` and custom CSS
- [ ] `arrowColor` - use `className` and custom CSS
- [ ] `arrowRadius` - use `className` and custom CSS
- [ ] `tooltipRadius` - use `className` and custom CSS
- [ ] `insecure` - CSS will be a separate file and can be imported or not
- [x] `className`
- [x] `id`
- [x] `html`
- [x] `delayHide` - also available on anchor element as `data-delay-hide`
- [ ] `delayUpdate` - if requested, can be implemented later
- [x] `delayShow` - also available on anchor element as `data-delay-show`
- [ ] `event`
- [ ] `eventOff`
- [ ] `isCapture`
- [ ] `globalEventOff`
- [ ] `getContent` - use dynamic `content`
- [ ] `afterShow` - if requested, can be implemented later
- [ ] `afterHide` - if requested, can be implemented later
- [ ] `overridePosition` - use `position`
- [ ] `disable` - state can be controlled or uncontrolled
- [ ] `scrollHide` - if requested, can be implemented later
- [ ] `resizeHide` - if requested, can be implemented later
- [x] `wrapper` - also available on anchor element as `data-tooltip-wrapper`
- [ ] `bodyMode`
- [ ] `clickable` - use controlled state to keep tooltip open
- [ ] `disableInternalStyle` - CSS will be a separate file and can be imported or not

### Detailed informations

Expand Down
54 changes: 52 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { TooltipController as Tooltip } from 'components/TooltipController'
import { TooltipProvider, TooltipWrapper } from 'components/TooltipProvider'
import { IPosition } from 'components/Tooltip/TooltipTypes.d'
import { useState } from 'react'
import styles from './styles.module.css'

function WithProviderMinimal() {
return (
<section style={{ marginTop: '100px' }}>
<section style={{ marginTop: '50px' }}>
<p>
<TooltipWrapper place="bottom" content="Shared Global Tooltip">
<button>Minimal 1</button>
Expand All @@ -21,7 +24,7 @@ function WithProviderMinimal() {

function WithProviderMultiple() {
return (
<section style={{ marginTop: '100px' }}>
<section style={{ marginTop: '50px' }}>
<p>
<TooltipWrapper tooltipId="tooltip-1" place="bottom">
<button>Multiple 1</button>
Expand All @@ -39,6 +42,14 @@ function WithProviderMultiple() {
function App() {
const [anchorId, setAnchorId] = useState('button')
const [isDarkOpen, setIsDarkOpen] = useState(false)
const [position, setPosition] = useState<IPosition>({ x: 0, y: 0 })
const [toggle, setToggle] = useState(false)

const handlePositionClick: React.MouseEventHandler<HTMLDivElement> = (event) => {
const x = event.clientX
const y = event.clientY
setPosition({ x, y })
}

return (
<main className={styles['main']}>
Expand Down Expand Up @@ -112,6 +123,45 @@ function App() {
<TooltipProvider>
<WithProviderMultiple />
</TooltipProvider>
<div style={{ display: 'flex', gap: '12px', flexDirection: 'row' }}>
<div>
<div
id="floatAnchor"
className={styles['big-anchor']}
onClick={() => {
setToggle((t) => !t)
}}
>
Hover me!
</div>
<Tooltip
anchorId="floatAnchor"
content={
toggle
? 'This is a float tooltip with a very very large content string'
: 'This is a float tooltip'
}
float
/>
</div>
<div>
<div
id="onClickAnchor"
className={styles['big-anchor']}
onClick={(event) => {
handlePositionClick(event)
}}
>
Click me!
</div>
<Tooltip
anchorId="onClickAnchor"
content={`This is an on click tooltip (x:${position.x},y:${position.y})`}
events={['click']}
position={position}
/>
</div>
</div>
</main>
)
}
Expand Down
103 changes: 96 additions & 7 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { TooltipContent } from 'components/TooltipContent'
import { useTooltip } from 'components/TooltipProvider'
import { computeTooltipPosition } from '../../utils/compute-positions'
import styles from './styles.module.css'
import type { ITooltip } from './TooltipTypes'
import type { IPosition, ITooltip } from './TooltipTypes'

let lastFloatPosition: IPosition | null = null

const Tooltip = ({
// props
Expand All @@ -22,8 +24,10 @@ const Tooltip = ({
children = null,
delayShow = 0,
delayHide = 0,
float = false,
noArrow,
style: externalStyles,
position,
// props handled by controller
isHtmlContent = false,
content,
Expand Down Expand Up @@ -100,14 +104,71 @@ const Tooltip = ({
}
}

const handleTooltipPosition = ({ x, y }: IPosition) => {
const virtualElement = {
getBoundingClientRect() {
return {
x,
y,
width: 0,
height: 0,
top: y,
left: x,
right: x,
bottom: y,
}
},
} as Element
setCalculatingPosition(true)
computeTooltipPosition({
place,
offset,
elementReference: virtualElement,
tooltipReference: tooltipRef.current,
tooltipArrowReference: tooltipArrowRef.current,
strategy: positionStrategy,
}).then((computedStylesData) => {
setCalculatingPosition(false)
if (Object.keys(computedStylesData.tooltipStyles).length) {
setInlineStyles(computedStylesData.tooltipStyles)
}
if (Object.keys(computedStylesData.tooltipArrowStyles).length) {
setInlineArrowStyles(computedStylesData.tooltipArrowStyles)
}
})
}

const handleMouseMove = (event?: Event) => {
if (!event) {
return
}
const mouseEvent = event as MouseEvent
const mousePosition = {
x: mouseEvent.clientX,
y: mouseEvent.clientY,
}
handleTooltipPosition(mousePosition)
lastFloatPosition = mousePosition
}

const handleClickTooltipAnchor = () => {
if (setIsOpen) {
setIsOpen(!isOpen)
} else if (isOpen === undefined) {
setShow((currentValue) => !currentValue)
} else if (!setIsOpen && isOpen === undefined) {
setShow(true)
if (delayHide) {
handleHideTooltipDelayed()
}
}
}

const handleClickOutsideAnchor = (event: MouseEvent) => {
if (event.target === activeAnchor.current) {
return
}
setShow(false)
}

// debounce handler to prevent call twice when
// mouse enter and focus events being triggered toggether
const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50)
Expand All @@ -125,13 +186,13 @@ const Tooltip = ({
}

if (!elementRefs.size) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {}
return () => null
}

const enabledEvents: { event: string; listener: (event?: Event) => void }[] = []

if (events.find((event: string) => event === 'click')) {
window.addEventListener('click', handleClickOutsideAnchor)
enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor })
}

Expand All @@ -142,6 +203,12 @@ const Tooltip = ({
{ event: 'focus', listener: debouncedHandleShowTooltip },
{ event: 'blur', listener: debouncedHandleHideTooltip },
)
if (float) {
enabledEvents.push({
event: 'mousemove',
listener: handleMouseMove,
})
}
}

enabledEvents.forEach(({ event, listener }) => {
Expand All @@ -151,15 +218,37 @@ const Tooltip = ({
})

return () => {
window.removeEventListener('click', handleClickOutsideAnchor)
enabledEvents.forEach(({ event, listener }) => {
elementRefs.forEach((ref) => {
ref.current?.removeEventListener(event, listener)
})
})
}
}, [anchorRefs, anchorId, events, delayHide, delayShow])
}, [anchorRefs, activeAnchor, anchorId, events, delayHide, delayShow])

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

if (float) {
if (lastFloatPosition) {
/*
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)
}
// if `float` is set, override regular positioning
return () => null
}
Copy link
Member Author

Choose a reason for hiding this comment

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

For future reference!


let elementReference = activeAnchor.current
if (anchorId) {
// `anchorId` element takes precedence
Expand Down Expand Up @@ -190,7 +279,7 @@ const Tooltip = ({
return () => {
mounted = false
}
}, [show, isOpen, anchorId, activeAnchor, content, place, offset, positionStrategy])
}, [show, isOpen, anchorId, activeAnchor, content, place, offset, positionStrategy, position])

useEffect(() => {
return () => {
Expand Down
Loading