UNPKG

@intility/bifrost-react

Version:

React library for Intility's design system, Bifrost.

140 lines (139 loc) 5.2 kB
"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 }) ] }) ] }); }