@intility/bifrost-react
Version:
React library for Intility's design system, Bifrost.
140 lines (139 loc) • 5.2 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { arrow, autoUpdate, flip, FloatingArrow, offset, shift, useClick, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import classNames from "classnames";
import React, { cloneElement, useEffect, useRef, useState } from "react";
import reactMajor from "../../utils/reactMajor.js";
import setRef from "../../utils/setRef.js";
import Popover from "../internal/Popover.internal.js";
const refIsProp = reactMajor >= 19;
/**
* Display a dropdown on click.
* The nested content of `<Dropdown>` needs to be a single element able to hold
* a `ref`, unless an element reference is passed to the `reference` prop.
* @example
* <Dropdown content={<div>Dropdown content</div>}>
* <Button>Click to show dropdown</Button>
* </Dropdown>
*/ export default function Dropdown({ reference, children, className, content, placement = "bottom-start", onShow, onHide, noPadding, visible, strategy = "absolute", disabled = false, offset: offsetProp = [
0,
10
] }) {
const element = reference && "current" in reference ? reference.current : reference;
const [open, setOpen] = useState(false);
const arrowRef = useRef(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],
crossAxis: offsetProp[0]
}),
shift({
padding: 10
}),
flip(),
// make sure arrow is placed *after* shift
arrow({
element: arrowRef,
// Prevent arrow from overlapping box border
padding: 8
})
]
});
const click = useClick(context, {
enabled: !isControlled
});
const dismiss = useDismiss(context, {
outsidePressEvent: "click"
});
const { getReferenceProps, getFloatingProps } = useInteractions([
click,
dismiss
]);
// close on focus outside
useEffect(()=>{
if (!isOpen || !refs.floating.current || !refs.reference.current) {
return;
}
const anchorElement = refs.reference.current;
const popoverElement = refs.floating.current;
const handleFocus = (e)=>{
if (anchorElement.contains?.(e.target)) return;
if (!popoverElement.contains(e.target)) {
// focus in event happened outside dropdown
if (isControlled) {
onHide?.();
} else {
setOpen(false);
}
}
};
document.addEventListener("focusin", handleFocus);
return ()=>document.removeEventListener("focusin", handleFocus);
}, [
isOpen,
refs.floating.current,
refs.reference.current,
onHide,
setOpen
]);
// if people want to pass a reference, without controlled mode
// this would let them access the interaction reference props
// implement only if no other solution works for consumer
// useImperativeHandle(ref, () => ({ getReferenceProps }), [getReferenceProps]);
if (disabled || !content) return children;
return /*#__PURE__*/ _jsxs(_Fragment, {
children: [
children && /*#__PURE__*/ cloneElement(children, {
ref: (node)=>{
refs.setReference(node);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setRef(refIsProp ? children.props.ref : children.ref, node);
},
...!isControlled && getReferenceProps()
}),
context.open && /*#__PURE__*/ _jsxs(Popover, {
open: isOpen,
ref: refs.setFloating,
style: floatingStyles,
className: classNames(className, "bf-dropdown", "bf-open-sans", {
"bf-dropdown-nopadding": noPadding
}),
...getFloatingProps(),
children: [
/*#__PURE__*/ _jsx("div", {
className: "bf-dropdown-content",
children: content
}),
/*#__PURE__*/ _jsx(FloatingArrow, {
ref: arrowRef,
context: context,
fill: "var(--bfc-base-3)",
stroke: "var(--bfc-base-dimmed)",
strokeWidth: 1,
height: 7,
width: 16
})
]
})
]
});
}