Skip to content
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

New props for selecting events to open/close the tooltip #1108

Merged
merged 3 commits into from
Nov 1, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/docs/examples/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Events available in ReactTooltip component.

:::danger

This has been deprecated. Use the `openOnClick` tooltip prop instead.
This has been deprecated. Use `openOnClick`, or `openEvents`, `closeEvents`, and `globalCloseEvents` instead.

See the [options page](../options.mdx#available-props) for more details.

:::

Expand Down
25 changes: 14 additions & 11 deletions docs/docs/options.mdx

Large diffs are not rendered by default.

20 changes: 13 additions & 7 deletions docs/docs/upgrade-guide/changelog-v4-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ If you run into any problems with the tooltip not updating after changes are mad
- [x] `float` - `boolean` - used to achieve V4's `effect="float"`
- [x] `hidden` - `boolean` - when set, the tooltip will not show
- [x] `render` - `function` - can be used to render dynamic content based on the active anchor element (check [the examples](../examples/render.mdx) for more details)
- [x] `closeOnEsc` - `boolean` - when set, the tooltip will close after pressing the escape key
- [x] `closeOnScroll` - `boolean` - when set, the tooltip will close when scrolling (similar to V4's `scrollHide`)
- [x] `closeOnResize` - `boolean` - when set, the tooltip will close when resizing the window (same as V4's `resizeHide`)
- [x] `closeOnEsc` - **DEPRECATED** - ~~`boolean` - when set, the tooltip will close after pressing the escape key~~
- [x] `closeOnScroll` - **DEPRECATED** - ~~`boolean` - when set, the tooltip will close when scrolling (similar to V4's `scrollHide`)~~
- [x] `closeOnResize` - **DEPRECATED** - ~~`boolean` - when set, the tooltip will close when resizing the window (same as V4's `resizeHide`)~~

:::note

Use `globalCloseEvents` instead of `closeOnEsc`, `closeOnScroll`, and `closeOnResize`. See the [options page](../options.mdx#available-props) for more details.

:::

## `V4` props available in `V5`

Expand All @@ -78,10 +84,10 @@ If you run into any problems with the tooltip not updating after changes are mad
- [x] `delayHide` - also available on anchor element as `data-delay-hide`
- [ ] `delayUpdate` - can be implemented if requested
- [x] `delayShow` - also available on anchor element as `data-delay-show`
- [ ] `event`
- [ ] `eventOff`
- [x] `event` - functionality changed and renamed to `openEvents`
- [x] `eventOff` - functionality changed and renamed to `closeEvents`
- [ ] `isCapture`
- [ ] `globalEventOff`
- [x] `globalEventOff` - functionality changed and renamed to `globalCloseEvents`
- [ ] `getContent` - pass dynamic values to `content` instead
- [x] `afterShow`
- [x] `afterHide`
Expand All @@ -92,7 +98,7 @@ If you run into any problems with the tooltip not updating after changes are mad
- [x] `wrapper` - also available on anchor element as `data-tooltip-wrapper`
- [ ] `bodyMode`
- [x] `clickable`
- [ ] `disableInternalStyle`
- [x] `disableInternalStyle` - renamed to `disableStyleInjection`

### Detailed informations

Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ function App() {
<Tooltip
anchorSelect="section[id='section-anchor-select'] > p > button"
place="bottom"
events={['click']}
openEvents={{ click: true }}
closeEvents={{ click: true }}
globalCloseEvents={{ clickOutsideAnchor: true }}
>
Tooltip content
</Tooltip>
Expand Down
154 changes: 119 additions & 35 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import { getScrollParent } from 'utils/get-scroll-parent'
import { computeTooltipPosition } from 'utils/compute-positions'
import coreStyles from './core-styles.module.css'
import styles from './styles.module.css'
import type { IPosition, ITooltip, PlacesType } from './TooltipTypes'
import type {
AnchorCloseEvents,
AnchorOpenEvents,
GlobalCloseEvents,
IPosition,
ITooltip,
PlacesType,
} from './TooltipTypes'

const Tooltip = ({
// props
Expand All @@ -34,6 +41,9 @@ const Tooltip = ({
closeOnEsc = false,
closeOnScroll = false,
closeOnResize = false,
openEvents,
closeEvents,
globalCloseEvents,
style: externalStyles,
position,
afterShow,
Expand Down Expand Up @@ -68,7 +78,49 @@ const Tooltip = ({
const [anchorsBySelect, setAnchorsBySelect] = useState<HTMLElement[]>([])
const mounted = useRef(false)

/**
* @todo Update when deprecated stuff gets removed.
*/
const shouldOpenOnClick = openOnClick || events.includes('click')
const hasClickEvent =
shouldOpenOnClick || openEvents?.click || openEvents?.dblclick || openEvents?.mousedown
const actualOpenEvents: AnchorOpenEvents = openEvents
? { ...openEvents }
: {
mouseenter: true,
focus: true,
click: false,
dblclick: false,
mousedown: false,
}
if (!openEvents && shouldOpenOnClick) {
Object.assign(actualOpenEvents, {
mouseenter: false,
focus: false,
click: true,
})
}
const actualCloseEvents: AnchorCloseEvents = closeEvents
? { ...closeEvents }
: {
mouseleave: true,
blur: true,
click: false,
}
if (!closeEvents && shouldOpenOnClick) {
Object.assign(actualCloseEvents, {
mouseleave: false,
blur: false,
})
}
Comment on lines +110 to +115
Copy link
Member Author

Choose a reason for hiding this comment

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

openOnClick works as a shorthand for

<Tooltip
    openEvents={{ click: true }}
    closeEvents={{}}
/>

Without setting closeEvents, the tooltip would keep the default behavior of closing on mouseleave/blur, which feels cumbersome for this simple use-case. So deprecating openOnClick seems like a bad idea.

const actualGlobalCloseEvents: GlobalCloseEvents = globalCloseEvents
? { ...globalCloseEvents }
: {
escape: closeOnEsc || false,
scroll: closeOnScroll || false,
resize: closeOnResize || false,
clickOutsideAnchor: hasClickEvent || false,
}
Comment on lines 84 to +123
Copy link
Member Author

Choose a reason for hiding this comment

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

This may look a little confusing at first, but it ensures openOnClick and the deprecated events props still work exactly as before.


/**
* useLayoutEffect runs before useEffect,
Expand Down Expand Up @@ -266,13 +318,6 @@ const Tooltip = ({
lastFloatPosition.current = mousePosition
}

const handleClickTooltipAnchor = (event?: Event) => {
handleShowTooltip(event)
if (delayHide) {
handleHideTooltipDelayed()
}
}

const handleClickOutsideAnchors = (event: MouseEvent) => {
const anchorById = document.querySelector<HTMLElement>(`[id='${anchorId}']`)
const anchors = [anchorById, ...anchorsBySelect]
Expand Down Expand Up @@ -371,13 +416,13 @@ const Tooltip = ({
const anchorScrollParent = getScrollParent(activeAnchor)
const tooltipScrollParent = getScrollParent(tooltipRef.current)

if (closeOnScroll) {
if (actualGlobalCloseEvents.scroll) {
window.addEventListener('scroll', handleScrollResize)
anchorScrollParent?.addEventListener('scroll', handleScrollResize)
tooltipScrollParent?.addEventListener('scroll', handleScrollResize)
}
let updateTooltipCleanup: null | (() => void) = null
if (closeOnResize) {
if (actualGlobalCloseEvents.resize) {
window.addEventListener('resize', handleScrollResize)
} else if (activeAnchor && tooltipRef.current) {
updateTooltipCleanup = autoUpdate(
Expand All @@ -398,29 +443,63 @@ const Tooltip = ({
}
handleShow(false)
}

if (closeOnEsc) {
if (actualGlobalCloseEvents.escape) {
window.addEventListener('keydown', handleEsc)
}

if (actualGlobalCloseEvents.clickOutsideAnchor) {
window.addEventListener('click', handleClickOutsideAnchors)
}

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

if (shouldOpenOnClick) {
window.addEventListener('click', handleClickOutsideAnchors)
enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor })
} else {
enabledEvents.push(
{ event: 'mouseenter', listener: debouncedHandleShowTooltip },
{ event: 'mouseleave', listener: debouncedHandleHideTooltip },
{ event: 'focus', listener: debouncedHandleShowTooltip },
{ event: 'blur', listener: debouncedHandleHideTooltip },
)
if (float) {
enabledEvents.push({
event: 'mousemove',
listener: handleMouseMove,
})
const handleClickOpenTooltipAnchor = (event?: Event) => {
if (show) {
return
}
handleShowTooltip(event)
}
const handleClickCloseTooltipAnchor = () => {
if (!show) {
return
}
handleHideTooltip()
}

const regularEvents = ['mouseenter', 'mouseleave', 'focus', 'blur']
const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup']

Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
if (!enabled) {
return
}
if (regularEvents.includes(event)) {
enabledEvents.push({ event, listener: debouncedHandleShowTooltip })
} else if (clickEvents.includes(event)) {
enabledEvents.push({ event, listener: handleClickOpenTooltipAnchor })
} else {
// never happens
}
})

Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
if (!enabled) {
return
}
if (regularEvents.includes(event)) {
enabledEvents.push({ event, listener: debouncedHandleHideTooltip })
} else if (clickEvents.includes(event)) {
enabledEvents.push({ event, listener: handleClickCloseTooltipAnchor })
} else {
// never happens
}
})

if (float) {
enabledEvents.push({
event: 'mousemove',
listener: handleMouseMove,
})
}

const handleMouseEnterTooltip = () => {
Expand All @@ -431,7 +510,9 @@ const Tooltip = ({
handleHideTooltip()
}

if (clickable && !shouldOpenOnClick) {
if (clickable && !hasClickEvent) {
// used to keep the tooltip open when hovering content.
// not needed if using click events.
tooltipRef.current?.addEventListener('mouseenter', handleMouseEnterTooltip)
tooltipRef.current?.addEventListener('mouseleave', handleMouseLeaveTooltip)
}
Expand All @@ -443,23 +524,23 @@ const Tooltip = ({
})

return () => {
if (closeOnScroll) {
if (actualGlobalCloseEvents.scroll) {
window.removeEventListener('scroll', handleScrollResize)
anchorScrollParent?.removeEventListener('scroll', handleScrollResize)
tooltipScrollParent?.removeEventListener('scroll', handleScrollResize)
}
if (closeOnResize) {
if (actualGlobalCloseEvents.resize) {
window.removeEventListener('resize', handleScrollResize)
} else {
updateTooltipCleanup?.()
}
if (shouldOpenOnClick) {
if (actualGlobalCloseEvents.clickOutsideAnchor) {
window.removeEventListener('click', handleClickOutsideAnchors)
}
if (closeOnEsc) {
if (actualGlobalCloseEvents.escape) {
window.removeEventListener('keydown', handleEsc)
}
if (clickable && !shouldOpenOnClick) {
if (clickable && !hasClickEvent) {
tooltipRef.current?.removeEventListener('mouseenter', handleMouseEnterTooltip)
tooltipRef.current?.removeEventListener('mouseleave', handleMouseLeaveTooltip)
}
Expand All @@ -479,8 +560,11 @@ const Tooltip = ({
rendered,
anchorRefs,
anchorsBySelect,
closeOnEsc,
events,
// the effect uses the `actual*Events` objects, but this should work
openEvents,
closeEvents,
globalCloseEvents,
shouldOpenOnClick,
])

useEffect(() => {
Expand Down
24 changes: 24 additions & 0 deletions src/components/Tooltip/TooltipTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ export interface IPosition {
y: number
}

export type AnchorOpenEvents = {
mouseenter?: boolean
focus?: boolean
click?: boolean
dblclick?: boolean
mousedown?: boolean
}
export type AnchorCloseEvents = {
mouseleave?: boolean
blur?: boolean
click?: boolean
dblclick?: boolean
mouseup?: boolean
}
export type GlobalCloseEvents = {
escape?: boolean
scroll?: boolean
resize?: boolean
clickOutsideAnchor?: boolean
}

export interface ITooltip {
className?: string
classNameArrow?: string
Expand Down Expand Up @@ -81,6 +102,9 @@ export interface ITooltip {
closeOnEsc?: boolean
closeOnScroll?: boolean
closeOnResize?: boolean
openEvents?: AnchorOpenEvents
closeEvents?: AnchorCloseEvents
globalCloseEvents?: GlobalCloseEvents
style?: CSSProperties
position?: IPosition
isOpen?: boolean
Expand Down
6 changes: 6 additions & 0 deletions src/components/TooltipController/TooltipController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const TooltipController = ({
closeOnEsc = false,
closeOnScroll = false,
closeOnResize = false,
openEvents,
closeEvents,
globalCloseEvents,
style,
position,
isOpen,
Expand Down Expand Up @@ -330,6 +333,9 @@ const TooltipController = ({
closeOnEsc,
closeOnScroll,
closeOnResize,
openEvents,
closeEvents,
globalCloseEvents,
style,
position,
isOpen,
Expand Down