import React, { FormEvent } from "react"; import { DesktopLinkGroups } from "./desktop-link-groups.tsx"; import { MobileLinkGroups } from "./mobile-link-groups.tsx"; import { NavigationProps } from "./types"; import { CallButton } from "@shared/components/call-button"; import { Input } from "@shared/components/input"; import { Link } from "@shared/components/link"; import { MaterialIcon } from "@shared/components/material-icon"; import { NextImage } from "@shared/components/next-image"; import { Text } from "@shared/components/text"; import { Button as ContentfulButton } from "@shared/contentful/blocks/button"; import { cx } from "@shared/utils"; export const Navigation: React.FC = props => { const { primaryNavigationLinks, utilityNavigationLinks, checkPlansJSX, primaryNavigationLogo, accountNavigationLinks, supportNavigationLinks, searchBarIcon, invocaPhoneNumberLink, invocaPhoneNumberDisplayText, callNowCtaText, displayCartIcon, cartHref, cartHasRetention, cartIconAriaLabel = "Cart", onCallClickDesktop, onCallClickMobile, onCartClick, onSearch = () => {}, utilityNavActiveIndex, primaryNavigationLogoWidth = 76.5, primaryNavigationLogoHeight = 24, hideMobileCallButton = false, displayUtilityNavigation = true, displaySearchBar = true, displayCallNowCta = true, showCallButton = true, showCallNowCtaInMainNav = false, showMobileSliderMenu = true, callNowCtaIcon, navigationBackgroundColor, navigationLinkColor, } = props; const logoUrl = typeof primaryNavigationLogo === "string" ? primaryNavigationLogo : primaryNavigationLogo?.url || ""; // Background token convention matches the legacy ResidentialNavbar: // when CMS provides a token like "navy500" we map it to // `bg-secondary-navy500`. Raw Tailwind classes (containing a `-`) // are passed through untouched. Defaults to `bg-secondary-navy500` // to preserve legacy behavior when CMS leaves the field empty. const mainNavBgClass = navigationBackgroundColor ? navigationBackgroundColor.includes("-") || navigationBackgroundColor.startsWith("bg-") ? navigationBackgroundColor : `bg-secondary-${navigationBackgroundColor}` : "bg-secondary-navy500"; const callNowCtaIconUrl = typeof callNowCtaIcon === "string" ? callNowCtaIcon : callNowCtaIcon?.url || ""; // Styled "Call Now" pill used in the main nav rows when CMS opts in via // `showCallNowCtaInMainNav` (desktop) or when the mobile slider menu is // disabled (`showMobileSliderMenu: false`). Mirrors the legacy // ResidentialNavbar styling so existing CMS entries render identically. const renderMainNavCallCta = ( onClick?: (event: React.MouseEvent) => void ) => ( {callNowCtaIconUrl ? ( ) : null} {invocaPhoneNumberDisplayText} ); const hasMobileMenuItems = (primaryNavigationLinks?.length ?? 0) > 0 || (supportNavigationLinks?.length ?? 0) > 0 || (accountNavigationLinks?.length ?? 0) > 0; return (
{checkPlansJSX &&
{checkPlansJSX}
}
); }; const MobileMenu = (props: NavigationProps) => { const { primaryNavigationLinks, utilityNavigationLinks, supportNavigationLinks, accountNavigationLinks, showCallButton, displayCallNowCta, displaySearchBar, } = props; const [isOpen, setIsOpen] = React.useState(false); React.useEffect(() => { if (typeof window === "undefined") return; if (isOpen) document.body.style.overflowY = "hidden"; else document.body.style.overflowY = "unset"; const element = document.getElementById("drawer-items"); if (!element) return; const focusableEls = element.querySelectorAll(".focus-item"); const firstFocusableEl = focusableEls[0]; const lastFocusableEl = focusableEls[focusableEls.length - 1]; const handleKeyDown = (e: { key: string; keyCode: number; shiftKey: any; preventDefault: () => void; }) => { const isTabPressed = e.key === "Tab" || e.keyCode === 9; if (!isTabPressed) return; if (e.shiftKey) { if (document.activeElement === firstFocusableEl) { (lastFocusableEl as HTMLButtonElement).focus?.(); e.preventDefault(); } } else { if (document.activeElement === lastFocusableEl) { (firstFocusableEl as HTMLButtonElement).focus?.(); e.preventDefault(); } } }; window.addEventListener("keydown", handleKeyDown); return () => { document.body.style.overflowY = "unset"; window.removeEventListener("keydown", handleKeyDown); }; }, [isOpen]); const closeMenu = () => { setIsOpen(false); }; return (
setIsOpen(true)} > {isOpen ? (
) : null}
{showCallButton !== false || displayCallNowCta !== false ? (
{props.invocaPhoneNumberDisplayText}
) : null}
{displaySearchBar !== false ? ( {})} /> ) : null}
{ // Close the drawer when a link (anchor) is clicked. // Group-toggle buttons render as
); }; const MobileSearchInput = (props: { closeMenu: () => void; onSearch: (query: string) => void; isMenuOpen: boolean; searchBarIconURL: string; }) => { const { closeMenu, onSearch, isMenuOpen, searchBarIconURL } = props; const [searchValue, setSearchValue] = React.useState(""); const searchInputRef = React.useRef(null); const redirectToSearchResults = (e: FormEvent | any) => { closeMenu(); e.preventDefault(); onSearch(searchValue); }; React.useEffect(() => { if (!isMenuOpen) { setSearchValue(""); } }, [isMenuOpen]); return (
setSearchValue(e.target.value)} autoComplete="off" containerClassName="h-[46px] px-4 pl-0 rounded-none flex-grow border-none" />
); }; const DesktopSearchInput = (props: { searchBarIconURL: string; onSearch: (query: string) => void; }) => { const { searchBarIconURL, onSearch } = props; const [searchValue, setSearchValue] = React.useState(""); const searchInputRef = React.useRef(null); const redirectToSearchResults = (e: FormEvent | any) => { e.preventDefault(); onSearch(searchValue); }; return (
{/* Search Icon */} {/* Input */} setSearchValue(e.target.value)} className={"body3 rounded-full px-3 text-text"} containerClassName="px-0 h-full border-none rounded-full" autoComplete="off" /> ); };