diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index d3cf91e7..c45b0f09 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -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 diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index f5ded4f3..9d5da714 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -44,6 +44,7 @@ const Tooltip = ({ setIsOpen, activeAnchor, setActiveAnchor, + border, }: ITooltip) => { const tooltipRef = useRef(null) const tooltipArrowRef = useRef(null) @@ -237,6 +238,7 @@ const Tooltip = ({ tooltipArrowReference: tooltipArrowRef.current, strategy: positionStrategy, middlewares, + border, }).then((computedStylesData) => { if (Object.keys(computedStylesData.tooltipStyles).length) { setInlineStyles(computedStylesData.tooltipStyles) @@ -503,6 +505,7 @@ const Tooltip = ({ tooltipArrowReference: tooltipArrowRef.current, strategy: positionStrategy, middlewares, + border, }).then((computedStylesData) => { if (!mounted.current) { // invalidate computed positions after remount diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 23f5c0b0..f75520d7 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -89,4 +89,5 @@ export interface ITooltip { afterHide?: () => void activeAnchor: HTMLElement | null setActiveAnchor: (anchor: HTMLElement | null) => void + border?: CSSProperties['border'] } diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 0bdb3e72..01e2785f 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -44,6 +44,7 @@ const TooltipController = ({ style, position, isOpen, + border, setIsOpen, afterShow, afterHide, @@ -235,6 +236,20 @@ 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 && !CSS.supports('border', `${border}`)) { + // eslint-disable-next-line no-console + console.warn(`[react-tooltip] "${border}" is not a valid \`border\`.`) + } + }, []) + /** * content priority: children < render or content < html * children should be lower priority so that it can be used as the "default" content @@ -283,6 +298,7 @@ const TooltipController = ({ style, position, isOpen, + border, setIsOpen, afterShow, afterHide, diff --git a/src/components/TooltipController/TooltipControllerTypes.d.ts b/src/components/TooltipController/TooltipControllerTypes.d.ts index 674225e1..3e9a161d 100644 --- a/src/components/TooltipController/TooltipControllerTypes.d.ts +++ b/src/components/TooltipController/TooltipControllerTypes.d.ts @@ -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 @@ -69,8 +76,8 @@ declare module 'react' { interface HTMLAttributes extends AriaAttributes, DOMAttributes { 'data-tooltip-id'?: string 'data-tooltip-place'?: PlacesType - 'data-tooltip-content'?: string - 'data-tooltip-html'?: string + 'data-tooltip-content'?: string | null + 'data-tooltip-html'?: string | null 'data-tooltip-variant'?: VariantType 'data-tooltip-offset'?: number 'data-tooltip-wrapper'?: WrapperType diff --git a/src/utils/compute-positions-types.d.ts b/src/utils/compute-positions-types.d.ts index fc960570..000f34c4 100644 --- a/src/utils/compute-positions-types.d.ts +++ b/src/utils/compute-positions-types.d.ts @@ -1,3 +1,4 @@ +import { CSSProperties } from 'react' import type { Middleware } from '../components/Tooltip/TooltipTypes' export interface IComputePositions { @@ -20,4 +21,5 @@ export interface IComputePositions { offset?: number strategy?: 'absolute' | 'fixed' middlewares?: Middleware[] + border?: CSSProperties['border'] } diff --git a/src/utils/compute-positions.ts b/src/utils/compute-positions.ts index 78c71138..8e0048ae 100644 --- a/src/utils/compute-positions.ts +++ b/src/utils/compute-positions.ts @@ -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 @@ -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 } @@ -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 }