Skip to content
3 changes: 2 additions & 1 deletion docs/docs/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,14 @@ import { Tooltip } from 'react-tooltip';
| `closeOnEsc` | `boolean` | no | `false` | `true` `false` | Pressing escape key will close the tooltip |
| `closeOnScroll` | `boolean` | no | `false` | `true` `false` | Scrolling will close the tooltip (for this to work, scroll element must be either the root html tag, the tooltip parent, or the anchor parent) |
| `closeOnEsc` | `boolean` | no | `false` | `true` `false` | Resizing the window will close the tooltip |
| `style` | `CSSProperties` | no | | a React inline style | Add inline styles directly to the tooltip |
| `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 | handled by internal state | `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`) |
| `setIsOpen` | `function` | no | | | The tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip |
| `afterShow` | `function` | no | | | A function to be called after the tooltip is shown |
| `afterHide` | `function` | no | | | A function to be called after the tooltip is hidden |
| `middlewares` | `Middleware[]` | no | | array of valid `floating-ui` middlewares | Allows for advanced customization. Check the [`floating-ui` docs](https://floating-ui.com/docs/middleware) for more information |
| `border` | `CSSProperties['border']` | no | | a CSS border style | Change the style of the tooltip border (including the arrow) |

### Envs

Expand Down
3 changes: 3 additions & 0 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Tooltip = ({
setIsOpen,
activeAnchor,
setActiveAnchor,
border,
}: ITooltip) => {
const tooltipRef = useRef<HTMLElement>(null)
const tooltipArrowRef = useRef<HTMLElement>(null)
Expand Down Expand Up @@ -237,6 +238,7 @@ const Tooltip = ({
tooltipArrowReference: tooltipArrowRef.current,
strategy: positionStrategy,
middlewares,
border,
}).then((computedStylesData) => {
if (Object.keys(computedStylesData.tooltipStyles).length) {
setInlineStyles(computedStylesData.tooltipStyles)
Expand Down Expand Up @@ -503,6 +505,7 @@ const Tooltip = ({
tooltipArrowReference: tooltipArrowRef.current,
strategy: positionStrategy,
middlewares,
border,
}).then((computedStylesData) => {
if (!mounted.current) {
// invalidate computed positions after remount
Expand Down
1 change: 1 addition & 0 deletions src/components/Tooltip/TooltipTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ export interface ITooltip {
afterHide?: () => void
activeAnchor: HTMLElement | null
setActiveAnchor: (anchor: HTMLElement | null) => void
border?: CSSProperties['border']
}
19 changes: 19 additions & 0 deletions src/components/TooltipController/TooltipController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from 'components/Tooltip/TooltipTypes'
import { useTooltip } from 'components/TooltipProvider'
import { TooltipContent } from 'components/TooltipContent'
import { cssAttrIsValid } from 'utils/css-attr-is-valid'
import type { ITooltipController } from './TooltipControllerTypes'

const TooltipController = ({
Expand Down Expand Up @@ -44,6 +45,7 @@ const TooltipController = ({
style,
position,
isOpen,
border,
setIsOpen,
afterShow,
afterHide,
Expand Down Expand Up @@ -235,6 +237,22 @@ const TooltipController = ({
}
}, [anchorRefs, providerActiveAnchor, activeAnchor, anchorId, anchorSelect])

useEffect(() => {
if (process.env.NODE_ENV === 'production') {
return
}
if (style?.border) {
// eslint-disable-next-line no-console
console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.')
}
if (border && !cssAttrIsValid('border', border)) {
// eslint-disable-next-line no-console
console.warn(
`[react-tooltip] "${border}" is not a valid \`border\`. See https://developer.mozilla.org/en-US/docs/Web/CSS/border`,
)
}
}, [])

/**
* content priority: children < render or content < html
* children should be lower priority so that it can be used as the "default" content
Expand Down Expand Up @@ -283,6 +301,7 @@ const TooltipController = ({
style,
position,
isOpen,
border,
setIsOpen,
afterShow,
afterHide,
Expand Down
7 changes: 7 additions & 0 deletions src/components/TooltipController/TooltipControllerTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export interface ITooltipController {
style?: CSSProperties
position?: IPosition
isOpen?: boolean
/**
* @description see https://developer.mozilla.org/en-US/docs/Web/CSS/border.
*
* Adding a border with width > 3px, or with `em/cm/rem/...` instead of `px`
* might break the tooltip arrow positioning.
*/
border?: CSSProperties['border']
setIsOpen?: (value: boolean) => void
afterShow?: () => void
afterHide?: () => void
Expand Down
2 changes: 2 additions & 0 deletions src/utils/compute-positions-types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CSSProperties } from 'react'
import type { Middleware } from '../components/Tooltip/TooltipTypes'

export interface IComputePositions {
Expand All @@ -20,4 +21,5 @@ export interface IComputePositions {
offset?: number
strategy?: 'absolute' | 'fixed'
middlewares?: Middleware[]
border?: CSSProperties['border']
}
28 changes: 26 additions & 2 deletions src/utils/compute-positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const computeTooltipPosition = async ({
offset: offsetValue = 10,
strategy = 'absolute',
middlewares = [offset(Number(offsetValue)), flip(), shift({ padding: 5 })],
border,
}: IComputePositions) => {
if (!elementReference) {
// elementReference can be null or undefined and we will not compute the position
Expand All @@ -31,7 +32,7 @@ export const computeTooltipPosition = async ({
strategy,
middleware,
}).then(({ x, y, placement, middlewareData }) => {
const styles = { left: `${x}px`, top: `${y}px` }
const styles = { left: `${x}px`, top: `${y}px`, border }

const { x: arrowX, y: arrowY } = middlewareData.arrow ?? { x: 0, y: 0 }

Expand All @@ -43,12 +44,35 @@ export const computeTooltipPosition = async ({
left: 'right',
}[placement.split('-')[0]] ?? 'bottom'

const borderSide =
border &&
{
top: { borderBottom: border, borderRight: border },
right: { borderBottom: border, borderLeft: border },
bottom: { borderTop: border, borderLeft: border },
left: { borderTop: border, borderRight: border },
}[placement.split('-')[0]]

let borderWidth = 0
if (border) {
const match = `${border}`.match(/(\d+)px/)
if (match?.[1]) {
borderWidth = Number(match[1])
} else {
/**
* this means `border` was set without `width`, or non-px value
*/
borderWidth = 1
}
}

const arrowStyle = {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '-4px',
...borderSide,
[staticSide]: `-${4 + borderWidth}px`,
}

return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement }
Expand Down
25 changes: 25 additions & 0 deletions src/utils/css-attr-is-valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const cssAttrIsValid = (attr: string, value: unknown) => {
const iframe = document.createElement('iframe')
Object.apply(iframe.style, {
display: 'none',
// in case `display: none` not supported
width: '0px',
height: '0px',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
document.body.appendChild(iframe)
if (!iframe.contentDocument) {
return true
}
const style = iframe.contentDocument.createElement('style')
style.innerHTML = `.test-css { ${attr}: ${value}; }`
iframe.contentDocument.head.appendChild(style)
const { sheet } = style
if (!sheet) {
return true
}
const result = sheet.cssRules[0].cssText
iframe.remove()
const match = result.match(new RegExp(`${attr}:`))
return !!match
}