@intility/bifrost-react
Version:
React library for Intility's design system, Bifrost.
161 lines (160 loc) • 6.8 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { forwardRef, useEffect, useState } from "react";
import { FocusTrap } from "focus-trap-react";
import classNames from "classnames";
import { faBars } from "@fortawesome/free-solid-svg-icons/faBars";
import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
import NavTop from "./Nav.Top.js";
import NavSide from "./Nav.Side.js";
import NavMobile from "./Nav.Mobile.js";
import NavItem from "./Nav.Item.js";
import NavLogo from "./Nav.Logo.js";
import NavProvider from "./NavContext.internal.js";
import Button from "../Button/Button.js";
import useLocale from "../../hooks/useLocale.js";
import useBreakpoint from "../../hooks/useBreakpoint.js";
/**
* Responsive component for Bifrost navigation menus
*/ const Nav = /*#__PURE__*/ forwardRef(({ children, top, topProps, side, sideProps, mobile, mobileProps, logo, hideBranding = false, noMain = false, ...props }, ref)=>{
const locale = useLocale();
const toXL = useBreakpoint(null, "xl");
const [mobileOpen, setMobileStateOpen] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const setMobileOpen = (open)=>{
if (mobileOpen === open) return;
setIsAnimating(true);
setMobileStateOpen(open);
};
useEffect(()=>{
if (typeof document === "undefined") return;
function closeMobileNavOnEsc(e) {
if (e.key === "Escape") setMobileOpen(false);
}
document.addEventListener("keydown", closeMobileNavOnEsc);
return ()=>document.removeEventListener("keydown", closeMobileNavOnEsc);
}, [
setMobileOpen
]);
// hide root scrollbar while nav mobile is open
useEffect(()=>{
if (typeof window === "undefined") return;
if (mobileOpen) {
document.documentElement.classList.add("bf-nav-mobile-is-open");
return ()=>{
document.documentElement.classList.remove("bf-nav-mobile-is-open");
};
} else {
document.documentElement.classList.remove("bf-nav-mobile-is-open");
}
}, [
mobileOpen
]);
// make sure the <main id> is a non-empty string
const mainId = props.id || "bf-nav-main";
const MainElement = noMain ? "div" : "main";
logo = typeof logo === "string" ? /*#__PURE__*/ _jsx(NavLogo, {
children: logo
}) : logo;
return /*#__PURE__*/ _jsxs(NavProvider, {
sideCollapsed: sideProps?.collapsed ?? false,
setSideCollapsed: sideProps?.onCollapsedChange,
mobileOpen: mobileOpen,
setMobileOpen: setMobileOpen,
children: [
/*#__PURE__*/ _jsx("a", {
href: "#" + mainId,
className: "bf-hide-until-focus bf-button bf-button-small",
children: locale.skipToMain
}),
side && /*#__PURE__*/ _jsx(NavSide, {
logo: top ? /*#__PURE__*/ _jsx(_Fragment, {}) : logo,
hideBranding: hideBranding,
...sideProps,
className: classNames("from-xl", sideProps?.className),
children: side
}),
/*#__PURE__*/ _jsx(NavTop, {
preLogo: (mobile || side) && /*#__PURE__*/ _jsx("button", {
type: "button",
className: "to-xl bf-nav-mobile-toggle",
"aria-label": locale.mainNav,
"aria-expanded": mobileOpen,
onClick: (e)=>{
setMobileOpen(!mobileOpen);
e.stopPropagation();
},
children: /*#__PURE__*/ _jsx(NavItem, {
icon: mobileOpen ? faXmark : faBars
})
}),
logo: logo,
...topProps,
onClick: (e)=>{
if (mobileOpen) setMobileOpen(false);
topProps?.onClick?.(e);
},
className: classNames(topProps?.className, {
"to-xl": !top
}),
children: top
}),
(mobile || side) && /*#__PURE__*/ _jsx(FocusTrap, {
focusTrapOptions: {
clickOutsideDeactivates: true,
initialFocus: false
},
active: mobileOpen && toXL,
children: /*#__PURE__*/ _jsxs(NavMobile, {
hideBranding: hideBranding,
...mobileProps,
onClick: (e)=>{
// automatically close mobile nav on link clicks
if (e.target.closest("a")) {
setMobileOpen(false);
}
mobileProps?.onClick?.(e);
},
onOverlayClick: (e)=>{
setMobileOpen(false);
e.stopPropagation();
},
className: classNames(mobileProps?.className, "to-xl", "bf-scrollbar-small", {
"bf-nav-mobile-open": mobileOpen,
"bf-nav-mobile-close": !mobileOpen,
// prevent keyboard focus (display: none; applied after animation end)
// this will also prevent close animation from playing on first load
"bf-nav-mobile-gone": !mobileOpen && !isAnimating
}),
onAnimationEnd: ()=>{
setIsAnimating(false);
},
// always hide content from screen readers when closed
"aria-hidden": !mobileOpen,
children: [
mobile ?? side,
/*#__PURE__*/ _jsx("div", {
children: /*#__PURE__*/ _jsx(Button, {
small: true,
className: "bf-hide-until-focus",
onClick: ()=>setMobileOpen(false),
children: locale.closeMenu
})
})
]
})
}),
/*#__PURE__*/ _jsx(MainElement, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ref: ref,
...props,
className: classNames(props.className, "bf-nav-main"),
id: mainId,
children: children
})
]
});
});
Nav.displayName = "Nav";
export default Nav;