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

Expose the tooltip ref to allow for imperative control #1109

Merged
merged 19 commits into from
Nov 13, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions docs/docs/examples/imperative-mode.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
sidebar_position: 1
---

# Imperative mode (ref)

Using the ReactTooltip imperative mode to control the tooltip programmatically.

import { useRef } from 'react';
import { Tooltip } from 'react-tooltip'

export const TooltipAnchor = ({ children, id, ...rest }) => {
return (
<span
id={id}
style={{
display: 'flex',
justifyContent: 'center',
margin: 'auto',
alignItems: 'center',
width: '60px',
height: '60px',
borderRadius: '60px',
color: '#222',
background: 'rgba(255, 255, 255, 1)',
cursor: 'pointer',
boxShadow: '3px 4px 3px rgba(0, 0, 0, 0.5)',
border: '1px solid #333',
}}
{...rest}
>
{children}
</span>
)
}

### Basic usage

A ref object created with `React.useRef()` can be passed to the `ref` tooltip prop.
It allows you to expose internal state variables (read-only), and to also control the tooltip programmatically.

#### API

```ts
interface TooltipImperativeOpenOptions {
anchorSelect?: string
position?: IPosition
place?: PlacesType
/**
* In practice, `ChildrenType` -> `React.ReactNode`
*/
content?: ChildrenType
/**
* Delay (in ms) before opening the tooltip.
*/
delay?: number
}

interface TooltipImperativeCloseOptions {
/**
* Delay (in ms) before closing the tooltip.
*/
delay?: number
}

interface TooltipRefProps {
open: (options?: TooltipImperativeOpenOptions) => void
close: (options?: TooltipImperativeCloseOptions) => void
/**
* @readonly
*/
activeAnchor: HTMLElement | null
/**
* @readonly
*/
place: PlacesType
/**
* @readonly
*/
isOpen: boolean
}
```

#### Methods

:::info

The imperative methods <b>can</b> be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an HTTP request is finished.

If you intend to use the tooltip exclusively with these methods, setting the `imperativeModeOnly` prop to disable default behavior is recommended. Otherwise, you might face undesired behavior.

:::

- `open()` opens the tooltip programmatically. All arguments are optional
- `anchorSelect` overrides the current selector. Ideally, it should match only one element (e.g. `#my-element`)
- `position` overrides the `position` tooltip prop
- `place` overrides the `place` tooltip prop
- `content` overrides the tooltip content, whether it was set through `content`, `render`, or any other way
- `delay` indicates how long (in ms) before the tooltip actually opens
- `close()` closes the tooltip programmatically
- `delay` indicates how long (in ms) before the tooltip actually closes

#### Internal state

:::note

These are read-only. Updating their values has no effect on the tooltip.

:::

- `activeAnchor` is a reference to the current anchor element
- `place` is the current tooltip placement relative to the anchor element. Can differ from the `place` tooltip prop if the tooltip is close to the edges of its container
- `isOpen` indicates whether the tooltip is currently being shown or not

```jsx
import { useRef } from 'react';
import { Tooltip, TooltipRefProps } from 'react-tooltip';

const tooltipRef1 = useRef<TooltipRefProps>(null)
const tooltipRef2 = useRef<TooltipRefProps>(null)

<a id="my-element">
◕‿‿◕
</a>
<button
onClick={() => {
tooltipRef1.current?.open({
anchorSelect: '#my-element',
content: 'Hello world!',
})
tooltipRef2.current?.open({
position: {
x: Math.random() * 500,
y: Math.random() * 300,
},
place: 'bottom',
content: 'Where am I? 😕😕',
})
}}
>
Open
</button>
<button
onClick={() => {
tooltipRef1.current?.close()
tooltipRef2.current?.close()
}}
>
Close
</button>
<Tooltip ref={tooltipRef1} />
<Tooltip ref={tooltipRef2} />
```

:::caution

Notice the tooltip still closes when unhovering the anchor element. This might be undesired if you're using the imperative methods exclusively.

If that's the case, use the `imperativeModeOnly` tooltip prop to disable default tooltip behavior.

:::

export const ImperativeModeExample = () => {
const tooltipRef1 = useRef(null)
const tooltipRef2 = useRef(null)
return (
<>
<TooltipAnchor id="my-element">
◕‿‿◕
</TooltipAnchor>
<div style={{ display: 'flex', flexDirection: 'row', gap: '5px' }}>
<button
onClick={() => {
tooltipRef1.current?.open({
anchorSelect: '#my-element',
content: 'Hello world!',
})
tooltipRef2.current?.open({
position: {
x: 300 + Math.random() * 500,
y: 300 + Math.random() * 300,
},
place: 'bottom',
content: 'Where am I? 😕😕',
})
}}
>
Open
</button>
<button
onClick={() => {
tooltipRef1.current?.close()
tooltipRef2.current?.close()
}}
>
Close
</button>
</div>
<Tooltip ref={tooltipRef1} />
<Tooltip ref={tooltipRef2} />
</>
)
}

<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: 'fit-content', margin: 'auto' }}>
<ImperativeModeExample />
</div>
2 changes: 2 additions & 0 deletions docs/docs/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { Tooltip } from 'react-tooltip';

| name | type | required | default | values | description |
| ----------------------- | -------------------------------------- | -------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref` | Tooltip reference | no | | `React.useRef` | Reference object which exposes internal state, and some methods for manually controlling the tooltip. See [the examples](./examples/imperative-mode.mdx). |
| `className` | `string` | no | | | Class name to customize tooltip element. You can also use the default class `react-tooltip` which is set internally |
| `classNameArrow` | `string` | no | | | Class name to customize tooltip arrow element. You can also use the default class `react-tooltip-arrow` which is set internally |
| `content` | `string` | no | | | Content to be displayed in tooltip (`html` prop is priorized over `content`) |
Expand Down Expand Up @@ -117,6 +118,7 @@ import { Tooltip } from 'react-tooltip';
| `openEvents` | `Record<string, boolean>` | no | `mouseenter` `focus` | `mouseenter` `focus` `click` `dblclick` `mousedown` | Events to be listened on the anchor elements to open the tooltip |
| `closeEvents` | `Record<string, boolean>` | no | `mouseleave` `blur` | `mouseleave` `blur` `click` `dblclick` `mouseup` | Events to be listened on the anchor elements to close the tooltip |
| `globalCloseEvents` | `Record<string, boolean>` | no | | `escape` `scroll` `resize` `clickOutsideAnchor` | Global events to be listened to close the tooltip (`escape` closes on pressing `ESC`, `clickOutsideAnchor` is useful with click events on `openEvents`) |
| `imperativeModeOnly` | `boolean` | no | `false` | `true` `false` | When enabled, default tooltip behavior is disabled. Check [the examples](./examples/imperative-mode.mdx) for more details |
| `style` | `CSSProperties` | no | | a CSS style object | Add inline styles directly to the tooltip |
| `position` | `{ x: number; y: number }` | no | | any `number` value for both `x` and `y` | Override the tooltip position on the DOM |
| `isOpen` | `boolean` | no | | `true` `false` | The tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip (can be used **without** `setIsOpen`) |
Expand Down
38 changes: 36 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* 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 { IPosition } from 'components/Tooltip/TooltipTypes.d'
import React, { useState } from 'react'
import { IPosition, TooltipRefProps } from 'components/Tooltip/TooltipTypes.d'
import React, { useEffect, useRef, useState } from 'react'
import { inline, offset } from '@floating-ui/dom'
import styles from './styles.module.css'

Expand All @@ -11,6 +11,7 @@ function App() {
const [isDarkOpen, setIsDarkOpen] = useState(false)
const [position, setPosition] = useState<IPosition>({ x: 0, y: 0 })
const [toggle, setToggle] = useState(false)
const tooltipRef = useRef<TooltipRefProps>(null)

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

useEffect(() => {
const handleQ = (event: KeyboardEvent) => {
if (event.key === 'q') {
// q
tooltipRef.current?.close()
}
}
window.addEventListener('keydown', handleQ)
return () => {
window.removeEventListener('keydown', handleQ)
}
})

return (
<main className={styles['main']}>
<button
Expand Down Expand Up @@ -86,6 +100,7 @@ function App() {
</p>
<Tooltip id="anchor-select">Tooltip content</Tooltip>
<Tooltip
ref={tooltipRef}
anchorSelect="section[id='section-anchor-select'] > p > button"
place="bottom"
openEvents={{ click: true }}
Expand Down Expand Up @@ -142,6 +157,25 @@ function App() {
positionStrategy="fixed"
/>
</div>
<button
id="imperativeTooltipButton"
style={{ height: 40, marginLeft: 100 }}
onClick={() => {
tooltipRef.current?.open({
anchorSelect: '#imperativeTooltipButton',
content: (
<div style={{ fontSize: 32 }}>
Opened imperatively!
<br />
<br />
Press Q to close imperatively too!
</div>
),
})
}}
>
imperative tooltip
</button>
</div>

<div style={{ marginTop: '1rem' }}>
Expand Down
Loading
Loading