From 7fd39abb2b119e2356a48d7cd4a247174d727ce2 Mon Sep 17 00:00:00 2001 From: akcodez Date: Tue, 13 Jan 2026 10:05:02 -0800 Subject: [PATCH] Integrate search functionality into Navbar and MobileMenu components, utilizing Redocly's search dialog. Update AlertBanner to prevent hydration mismatch with null initial state for displayDate. Adjust NavControls and NetworkSubmenu to handle theme detection more effectively. --- @theme/components/Navbar/Navbar.tsx | 11 +++++++++-- .../Navbar/components/AlertBanner.tsx | 5 +++-- .../components/Navbar/controls/NavControls.tsx | 17 ++++++----------- .../Navbar/mobile-menu/MobileMenu.tsx | 8 ++++---- @theme/components/Navbar/submenus/Submenu.tsx | 9 ++++++--- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/@theme/components/Navbar/Navbar.tsx b/@theme/components/Navbar/Navbar.tsx index ea2c067ae1..c91faaff13 100644 --- a/@theme/components/Navbar/Navbar.tsx +++ b/@theme/components/Navbar/Navbar.tsx @@ -1,4 +1,6 @@ import * as React from "react"; +import { useSearchDialog } from "@redocly/theme/core/hooks"; +import { SearchDialog } from "@redocly/theme/components/Search/SearchDialog"; // Import from modular components import { AlertBanner } from "./components/AlertBanner"; @@ -33,6 +35,9 @@ export function Navbar(_props: NavbarProps = {}) { const submenuTimeoutRef = React.useRef(null); const closingTimeoutRef = React.useRef(null); + // Use Redocly's search dialog hook - shared across navbar and mobile menu + const { isOpen: isSearchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useSearchDialog(); + const handleHamburgerClick = () => { setMobileMenuOpen(true); }; @@ -115,7 +120,7 @@ export function Navbar(_props: NavbarProps = {}) {
- +
{/* Submenus positioned relative to navbar */} @@ -126,7 +131,9 @@ export function Navbar(_props: NavbarProps = {}) { - + + {/* Render SearchDialog when open - this is the actual search modal */} + {isSearchOpen && } ); } diff --git a/@theme/components/Navbar/components/AlertBanner.tsx b/@theme/components/Navbar/components/AlertBanner.tsx index e98df172f8..38f6f9b2d4 100644 --- a/@theme/components/Navbar/components/AlertBanner.tsx +++ b/@theme/components/Navbar/components/AlertBanner.tsx @@ -18,7 +18,8 @@ export function AlertBanner({ message, button, link, show }: AlertBannerProps) { const { useTranslate } = useThemeHooks(); const { translate } = useTranslate(); const bannerRef = React.useRef(null); - const [displayDate, setDisplayDate] = React.useState("JUNE 10-12"); + // Use null initial state to avoid hydration mismatch - server and client both render null initially + const [displayDate, setDisplayDate] = React.useState(null); React.useEffect(() => { const calculateCountdown = () => { @@ -69,7 +70,7 @@ export function AlertBanner({ message, button, link, show }: AlertBannerProps) { >
{translate(message)}
-
{displayDate}
+
{displayDate ?? "JUNE 10-12"}
{translate(button)}
diff --git a/@theme/components/Navbar/controls/NavControls.tsx b/@theme/components/Navbar/controls/NavControls.tsx index fa715b95c4..dad75a4e30 100644 --- a/@theme/components/Navbar/controls/NavControls.tsx +++ b/@theme/components/Navbar/controls/NavControls.tsx @@ -1,21 +1,16 @@ -import * as React from "react"; import { SearchButton } from "./SearchButton"; import { ModeToggleButton } from "./ModeToggleButton"; import { LanguagePill } from "./LanguagePill"; +interface NavControlsProps { + onSearch?: () => void; +} + /** * Nav Controls Component. * Right side of the navbar containing search, mode toggle, and language selector. */ -export function NavControls() { - const handleSearch = () => { - // Trigger the search modal - const searchTrigger = document.querySelector('[data-component-name="Search/SearchTrigger"]') as HTMLElement; - if (searchTrigger) { - searchTrigger.click(); - } - }; - +export function NavControls({ onSearch }: NavControlsProps) { const handleModeToggle = () => { // Toggle between light and dark theme const newTheme = document.documentElement.classList.contains("dark") ? "light" : "dark"; @@ -32,7 +27,7 @@ export function NavControls() { return (
- +
diff --git a/@theme/components/Navbar/mobile-menu/MobileMenu.tsx b/@theme/components/Navbar/mobile-menu/MobileMenu.tsx index a85c1f126f..16f855ee33 100644 --- a/@theme/components/Navbar/mobile-menu/MobileMenu.tsx +++ b/@theme/components/Navbar/mobile-menu/MobileMenu.tsx @@ -9,13 +9,14 @@ import { MobileMenuContent, type MobileMenuKey } from "./MobileMenuContent"; interface MobileMenuProps { isOpen: boolean; onClose: () => void; + onSearch?: () => void; } /** * Mobile Menu Component. * Full-screen slide-out menu for mobile devices. */ -export function MobileMenu({ isOpen, onClose }: MobileMenuProps) { +export function MobileMenu({ isOpen, onClose, onSearch }: MobileMenuProps) { const { useTranslate } = useThemeHooks(); const { translate } = useTranslate(); const [expandedItem, setExpandedItem] = React.useState("Develop"); @@ -37,9 +38,8 @@ export function MobileMenu({ isOpen, onClose }: MobileMenuProps) { }; const handleSearch = () => { - const searchTrigger = document.querySelector('[data-component-name="Search/SearchTrigger"]') as HTMLElement; - if (searchTrigger) { - searchTrigger.click(); + if (onSearch) { + onSearch(); } onClose(); }; diff --git a/@theme/components/Navbar/submenus/Submenu.tsx b/@theme/components/Navbar/submenus/Submenu.tsx index 4762a906a4..9430d7cf92 100644 --- a/@theme/components/Navbar/submenus/Submenu.tsx +++ b/@theme/components/Navbar/submenus/Submenu.tsx @@ -72,7 +72,9 @@ export function Submenu({ variant, isActive, isClosing }: SubmenuProps) { /** Network submenu with theme-aware pattern images */ function NetworkSubmenuContent({ isActive, isClosing }: { isActive: boolean; isClosing: boolean }) { - const [isDarkMode, setIsDarkMode] = React.useState(false); + // Start with null to indicate "not yet determined" - avoids hydration mismatch + // by ensuring server and client both render the same initial state + const [isDarkMode, setIsDarkMode] = React.useState(null); React.useEffect(() => { const checkTheme = () => { @@ -84,9 +86,10 @@ function NetworkSubmenuContent({ isActive, isClosing }: { isActive: boolean; isC return () => observer.disconnect(); }, []); + // Default to light mode patterns until client-side detection runs const patternImages = React.useMemo(() => ({ - lilac: isDarkMode ? darkLilacPattern : resourcesPurplePattern, - green: isDarkMode ? darkInsightsGreenPattern : insightsGreenPattern, + lilac: isDarkMode === true ? darkLilacPattern : resourcesPurplePattern, + green: isDarkMode === true ? darkInsightsGreenPattern : insightsGreenPattern, }), [isDarkMode]); const classNames = [