@intility/bifrost-react
Version:
React library for Intility's design system, Bifrost.
141 lines (138 loc) • 4.23 kB
JavaScript
"use client";
/* eslint-disable @typescript-eslint/ban-ts-comment */
import classNames from "classnames";
import { arrow, autoUpdate, flip, FloatingArrow, offset, shift, useDismiss, useFloating, useFocus, useHover, useInteractions } from "@floating-ui/react";
import React, { cloneElement, useState } from "react";
import reactMajor from "../../utils/reactMajor.js";
import setRef from "../../utils/setRef.js";
import Popover from "../internal/Popover.internal.js";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
const refIsProp = reactMajor >= 19;
/**
* Display a tooltip on hover
* The nested content of `<Tooltip>` needs to be a single element able to hold
* a `ref`, unless an element reference is passed to the `reference` prop.
*
* @see https://bifrost.intility.com/react/tooltip
*
* @example
* <Tooltip content="Good job hovering!" placement="right">
* <span>Hover me</span>
* </Tooltip>
*/
export default function Tooltip({
reference,
children,
className,
content,
visible,
onShow,
onHide,
state = "default",
variant = "basic",
placement = "top",
strategy = "absolute",
disabled = false,
offset: offsetProp = [0, 10]
}) {
const element = reference && "current" in reference ? reference.current : reference;
const [open, setOpen] = useState(false);
const [arrowElement, setArrowElement] = useState(null);
const isControlled = visible !== undefined;
const isOpen = visible ?? open;
const {
refs,
floatingStyles,
context
} = useFloating({
whileElementsMounted: autoUpdate,
strategy,
open: isOpen,
elements: {
reference: element
},
placement,
onOpenChange: newOpen => {
setOpen(newOpen);
if (newOpen) {
onShow?.();
} else {
onHide?.();
}
},
middleware: [offset({
mainAxis: offsetProp[1],
// gap between target and popup
crossAxis: offsetProp[0] // skidding
}), shift({
padding: 10 // space around edge of viewport
}), flip({
fallbackAxisSideDirection: "start"
}),
// make sure arrow is placed *after* shift
arrow({
element: arrowElement,
// Prevent arrow from overlapping box border
padding: 8
})]
});
const hover = useHover(context, {
enabled: !isControlled
});
const dismiss = useDismiss(context, {
outsidePressEvent: "click"
});
const focus = useFocus(context, {
enabled: !isControlled
});
const {
getReferenceProps,
getFloatingProps
} = useInteractions([hover, dismiss, focus]);
if (disabled || !content) return children;
// https://github.com/vercel/next.js/discussions/81876
// https://github.com/facebook/react/issues/32392
let resolvedChildren = children;
if (
// @ts-ignore
resolvedChildren?.$$typeof === Symbol.for("react.lazy") && typeof React.use === "function" && "_payload" in resolvedChildren) {
// @ts-ignore
resolvedChildren = React.use(resolvedChildren._payload);
}
return /*#__PURE__*/_jsxs(_Fragment, {
children: [resolvedChildren &&
/*#__PURE__*/
// floating-ui's refs.setReference is a stable callback, not a ref read
// eslint-disable-next-line react-hooks/refs
cloneElement(resolvedChildren, {
...(!isControlled && getReferenceProps(resolvedChildren.props)),
ref: node => {
refs.setReference(node);
setRef(
// @ts-ignore
refIsProp ? resolvedChildren.props.ref : resolvedChildren.ref, node);
}
}), context.open && /*#__PURE__*/_jsxs(Popover, {
open: isOpen
// eslint-disable-next-line react-hooks/refs
,
ref: refs.setFloating,
style: floatingStyles,
className: classNames(className, "bf-tooltip", "bf-open-sans", {
"bf-tooltip-neutral": state === "neutral",
"bf-tooltip-compact": variant === "compact"
}),
...getFloatingProps(),
children: [/*#__PURE__*/_jsx("span", {
className: "bf-tooltip-content",
children: content
}), /*#__PURE__*/_jsx(FloatingArrow, {
ref: setArrowElement,
context: context,
className: "bf-tooltip-arrow",
height: 7,
width: 16
})]
})]
});
}