From 0755f8663803bb06a6c5c9fd7dd972ade4fb232f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 03:21:56 +0000 Subject: [PATCH 1/2] feat(docs): navigate to section pages and route "All" to home + global search Make the docs side nav behave like navigation rather than an in-menu expander. - Clicking a top level section (Components, Get started, Foundations) now lands on that section's index page instead of only swapping the in-menu view. The destination page derives and shows the matching submenu from the URL. - A submenu's "All" item now navigates to the home page and auto-opens global search, reinforcing search as a primary way to navigate, instead of silently collapsing back to the parent menu. Auto-open is driven by an openSearch=1 marker that SearchModal checks on mount and then strips from the URL. The shared "All" item is extracted into AllHomeItem, and the now unused in-menu state handlers (onSelectSection, onBack) are removed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014KqafCHdPSm7GsgVWwAVHN --- docs/src/components/SiteNav.tsx | 39 ++------- docs/src/components/nav/AllHomeItem.tsx | 32 +++++++ docs/src/components/nav/ComponentsSubMenu.tsx | 18 +--- .../src/components/nav/FoundationsSubMenu.tsx | 16 +--- docs/src/components/nav/GetStartedSubMenu.tsx | 17 +--- docs/src/components/nav/ParentMenu.tsx | 87 +++---------------- docs/src/components/search/SearchModal.tsx | 20 ++++- docs/src/components/search/search-utils.ts | 13 +++ 8 files changed, 96 insertions(+), 146 deletions(-) create mode 100644 docs/src/components/nav/AllHomeItem.tsx diff --git a/docs/src/components/SiteNav.tsx b/docs/src/components/SiteNav.tsx index 8959250e0b..3f8080a0f1 100644 --- a/docs/src/components/SiteNav.tsx +++ b/docs/src/components/SiteNav.tsx @@ -1,16 +1,14 @@ /** * SiteNav.tsx * - * Unified site navigation with parent/sub-menu state switching. + * Unified site navigation. Every top-level section links directly to its index + * page; which menu renders (parent vs. a submenu) is derived from the current + * URL, not from in-menu click state. * - * Navigation behavior: - * - Components, Get started, Foundations: has submenu (many pages) - * - Tokens: Direct link, stays on parent menu, highlights "Tokens" - * - Examples: Direct link, stays on parent menu, highlights "Examples" - * - * Menu level is managed via React state: - * - Parent view: Shows all sections - * - Sub-menu view: Shows section-specific navigation (components, get-started, foundations) + * - Components, Get started, Foundations: land on their index page, which shows + * the matching submenu. + * - Tokens, Examples: single pages, stay on the parent menu and highlight. + * - A submenu's "All" item navigates home and opens global search. */ import { useState, useEffect, useCallback } from "react"; @@ -176,25 +174,6 @@ export function SiteNav({ }); }, [menuLevel]); - const handleSelectSection = useCallback( - (section: MenuSection) => { - // Submenu sections switch the menu into their dedicated view. - if (SUBMENU_SECTIONS.includes(section)) { - setMenuLevel(section); - // Auto-expand when entering submenu while collapsed (icons aren't descriptive enough) - if (!isOpen) { - setIsOpen(true); - } - } - // Other sections navigate directly via url prop, no state change needed - }, - [isOpen], - ); - - const handleBack = useCallback(() => { - setMenuLevel("parent"); - }, []); - const handleExpandMenu = useCallback(() => { setIsOpen(true); }, []); @@ -209,7 +188,6 @@ export function SiteNav({ @@ -246,7 +222,6 @@ export function SiteNav({ ); diff --git a/docs/src/components/nav/AllHomeItem.tsx b/docs/src/components/nav/AllHomeItem.tsx new file mode 100644 index 0000000000..d02ece93c6 --- /dev/null +++ b/docs/src/components/nav/AllHomeItem.tsx @@ -0,0 +1,32 @@ +/** + * AllHomeItem.tsx + * + * The "All" item rendered at the top of every submenu (Components, Get started, + * Foundations). It navigates to the home page and auto-opens global search, + * so "All" means "you're searching the whole design system" rather than just + * collapsing the menu. + * + * Wrapped-onClick + sentinel `url` mirrors the existing pattern in the submenus + * (e.g. handleAllComponentsClick): the menu's onNavigate ignores `/__` paths, so + * the onClick owns navigation. + */ + +import { type MouseEvent } from "react"; +import { GoabWorkSideMenuItem } from "@abgov/react-components"; +import { searchHomeUrl } from "../search/search-utils"; + +export function AllHomeItem() { + const handleClick = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + window.location.href = searchHomeUrl(); + }; + + return ( +
+ +
+ ); +} + +export default AllHomeItem; diff --git a/docs/src/components/nav/ComponentsSubMenu.tsx b/docs/src/components/nav/ComponentsSubMenu.tsx index ef9eb535e7..c37bd59e84 100644 --- a/docs/src/components/nav/ComponentsSubMenu.tsx +++ b/docs/src/components/nav/ComponentsSubMenu.tsx @@ -12,13 +12,13 @@ import { GoabWorkSideMenuGroup, } from "@abgov/react-components"; import { MenuSecondaryContent } from "./MenuSecondaryContent"; +import { AllHomeItem } from "./AllHomeItem"; import type { NavCategory } from "../../lib/nav-categories"; import { withBase } from "@/lib/base-url"; interface ComponentsSubMenuProps { isOpen: boolean; onToggle: () => void; - onBack: () => void; onExpandMenu?: () => void; currentSlug?: string; categories?: NavCategory[]; @@ -27,7 +27,6 @@ interface ComponentsSubMenuProps { export function ComponentsSubMenu({ isOpen, onToggle, - onBack, onExpandMenu, currentSlug, categories = [], @@ -35,13 +34,6 @@ export function ComponentsSubMenu({ // We're on the All Components page if there's no currentSlug const isAllComponentsPage = !currentSlug; - // Handle back button click - wrap prevents navigation, triggers state change - const handleBackClick = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onBack(); - }; - // Handle All Components click on detail pages - navigate without URL auto-matching const handleAllComponentsClick = (e: MouseEvent) => { e.preventDefault(); @@ -49,13 +41,11 @@ export function ComponentsSubMenu({ window.location.href = withBase("/components"); }; - // Primary content: Back button + All Components link + component categories + // Primary content: All (home + global search) + All Components link + component categories const primaryContent = ( <> - {/* Back to parent menu - wrapped div captures click since component doesn't expose onClick */} -
- -
+ {/* "All" navigates home and opens global search */} + {/* All Components page link - On All Components page: use url so auto-matching highlights it diff --git a/docs/src/components/nav/FoundationsSubMenu.tsx b/docs/src/components/nav/FoundationsSubMenu.tsx index 3b881630ce..79b9ad3159 100644 --- a/docs/src/components/nav/FoundationsSubMenu.tsx +++ b/docs/src/components/nav/FoundationsSubMenu.tsx @@ -5,13 +5,13 @@ * Uses GoabWorkSideMenuGroup for expandable Style guide sections. */ -import { type MouseEvent } from "react"; import { GoabWorkSideMenu, GoabWorkSideMenuItem, GoabWorkSideMenuGroup, } from "@abgov/react-components"; import { MenuSecondaryContent } from "./MenuSecondaryContent"; +import { AllHomeItem } from "./AllHomeItem"; import { withBase } from "@/lib/base-url"; // Top-level pages (not in a group) @@ -77,7 +77,6 @@ const ALL_URLS = [ interface FoundationsSubMenuProps { isOpen: boolean; onToggle: () => void; - onBack: () => void; onExpandMenu?: () => void; currentUrl?: string; } @@ -85,22 +84,13 @@ interface FoundationsSubMenuProps { export function FoundationsSubMenu({ isOpen, onToggle, - onBack, onExpandMenu, currentUrl, }: FoundationsSubMenuProps) { - const handleBackClick = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onBack(); - }; - const primaryContent = ( <> - {/* Back to parent menu */} -
- -
+ {/* "All" navigates home and opens global search */} + {/* Top-level pages */} {TOP_PAGES.map((page) => ( diff --git a/docs/src/components/nav/GetStartedSubMenu.tsx b/docs/src/components/nav/GetStartedSubMenu.tsx index 8faea4c8bb..9a3e6a1f8b 100644 --- a/docs/src/components/nav/GetStartedSubMenu.tsx +++ b/docs/src/components/nav/GetStartedSubMenu.tsx @@ -7,20 +7,20 @@ * `getGetStartedNav()` in lib/get-started-nav.ts. */ -import { Fragment, type MouseEvent } from "react"; +import { Fragment } from "react"; import { GoabWorkSideMenu, GoabWorkSideMenuItem, GoabWorkSideMenuGroup, } from "@abgov/react-components"; import { MenuSecondaryContent } from "./MenuSecondaryContent"; +import { AllHomeItem } from "./AllHomeItem"; import { withBase } from "@/lib/base-url"; import type { GetStartedNav, GetStartedNavSection } from "@/lib/get-started-nav"; interface GetStartedSubMenuProps { isOpen: boolean; onToggle: () => void; - onBack: () => void; onExpandMenu?: () => void; currentUrl?: string; items: GetStartedNav; @@ -29,17 +29,10 @@ interface GetStartedSubMenuProps { export function GetStartedSubMenu({ isOpen, onToggle, - onBack, onExpandMenu, currentUrl, items, }: GetStartedSubMenuProps) { - const handleBackClick = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onBack(); - }; - const renderSection = (section: GetStartedNavSection) => { if (section.type === "flat") { return ( @@ -72,10 +65,8 @@ export function GetStartedSubMenu({ const primaryContent = ( <> - {/* Back to parent menu */} -
- -
+ {/* "All" navigates home and opens global search */} + {items.sections.map(renderSection)} diff --git a/docs/src/components/nav/ParentMenu.tsx b/docs/src/components/nav/ParentMenu.tsx index 2e61b09358..93aabe78cc 100644 --- a/docs/src/components/nav/ParentMenu.tsx +++ b/docs/src/components/nav/ParentMenu.tsx @@ -1,12 +1,12 @@ /** * ParentMenu.tsx * - * Parent-level navigation showing all main sections. - * - Components, Get started, Foundations: opens submenu (has many pages) - * - Tokens, Examples: direct navigation (single page each) + * Parent-level navigation showing all main sections. Every section links + * directly to its index page. The destination page's SiteNav then shows the + * relevant submenu (Components, Get started, Foundations) based on the URL; + * Tokens and Examples are single pages. */ -import React from "react"; import { GoabWorkSideMenu, GoabWorkSideMenuItem } from "@abgov/react-components"; import type { GoabIconType } from "@abgov/ui-components-common"; import { MenuSecondaryContent } from "./MenuSecondaryContent"; @@ -23,7 +23,6 @@ export type MenuSection = interface ParentMenuProps { isOpen: boolean; onToggle: () => void; - onSelectSection: (section: MenuSection) => void; /** Currently active section (for highlighting) */ currentSection?: MenuSection; } @@ -34,15 +33,6 @@ interface TopLevelSection { icon: GoabIconType; } -// Sections that navigate directly to a page (no submenu) -const DIRECT_NAV_SECTIONS: Partial> = { - tokens: withBase("/tokens"), - examples: withBase("/examples"), -}; - -// Sections that open a submenu -const SUBMENU_SECTIONS = ["components", "get-started", "foundations"] as const; - // Main navigation sections const SECTIONS: TopLevelSection[] = [ { id: "get-started", label: "Get started", icon: "document-text" }, @@ -55,69 +45,20 @@ const SECTIONS: TopLevelSection[] = [ export function ParentMenu({ isOpen, onToggle, - onSelectSection, currentSection, }: ParentMenuProps) { - // Handle click on submenu item - prevent navigation, open submenu instead - const handleSubmenuClick = (sectionId: MenuSection) => (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onSelectSection(sectionId); - }; - - // Primary content: Main navigation sections + // Every section links directly to its index page. const primaryContent = ( <> - {SECTIONS.map((section) => { - const directUrl = DIRECT_NAV_SECTIONS[section.id]; - const hasSubmenu = SUBMENU_SECTIONS.includes( - section.id as (typeof SUBMENU_SECTIONS)[number], - ); - const isActive = currentSection === section.id; - - if (directUrl) { - // Direct navigation - use url prop - return ( - - ); - } - - if (hasSubmenu) { - // Opens submenu - use section URL when active for matching, - // otherwise use non-matching URL to prevent false positives on homepage - const submenuUrl = isActive ? withBase(`/${section.id}`) : "/__never_match__"; - return ( -
- -
- ); - } else { - // Placeholder sections - disabled for now - return ( - onSelectSection(section.id)} - /> - ); - } - })} + {SECTIONS.map((section) => ( + + ))} ); diff --git a/docs/src/components/search/SearchModal.tsx b/docs/src/components/search/SearchModal.tsx index 661d728f3a..bf8b4ac5d6 100644 --- a/docs/src/components/search/SearchModal.tsx +++ b/docs/src/components/search/SearchModal.tsx @@ -31,7 +31,7 @@ import { getFilteredOptions, type FilterOption, } from "./SearchFilterHints"; -import { getResultUrl } from "./search-utils"; +import { getResultUrl, SEARCH_OPEN_PARAM } from "./search-utils"; import "./search.css"; // ============================================================================ @@ -357,6 +357,24 @@ export function SearchModal() { return () => window.removeEventListener("goa-search-open", handleSearchOpen); }, [openModal]); + /** + * Auto-open when arriving with the `?openSearch=1` marker (set by the side + * menu's "All" item). Self-checking on mount avoids any race with this + * client:only island's hydration. The param is stripped afterwards so a + * refresh or back navigation doesn't re-trigger it. + */ + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get(SEARCH_OPEN_PARAM) === "1") { + openModal(); + params.delete(SEARCH_OPEN_PARAM); + const qs = params.toString(); + const cleanUrl = + window.location.pathname + (qs ? `?${qs}` : "") + window.location.hash; + window.history.replaceState({}, "", cleanUrl); + } + }, [openModal]); + // Only render content when open - this is where lazy loading happens if (!isOpen) { return null; diff --git a/docs/src/components/search/search-utils.ts b/docs/src/components/search/search-utils.ts index bc76bc00ad..3179015fa1 100644 --- a/docs/src/components/search/search-utils.ts +++ b/docs/src/components/search/search-utils.ts @@ -9,6 +9,19 @@ import type { SearchFilter } from "./useSearch"; import { withBase } from "@/lib/base-url"; +/** + * URL query param that, when present on any page load, auto-opens the global + * search modal. Used by the side menu's "All" item to land on the home page and + * drop the user straight into global search. Named distinctly from the tokens + * page's own `?search=` filter param to avoid collision. + */ +export const SEARCH_OPEN_PARAM = "openSearch"; + +/** Home URL that auto-opens global search on arrival. */ +export function searchHomeUrl(): string { + return withBase(`/?${SEARCH_OPEN_PARAM}=1`); +} + /** Build a URL for a search result or history item by type + slug. */ export function getResultUrl(type: string, slug: string): string { if (type === "page") { From 0c2d2941de3a86865ac4d1fb265d9ab572ad5b39 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 06:49:12 +0000 Subject: [PATCH 2/2] fix(docs): route nav "All" to the home page without auto-opening search Auto-opening the global search modal when clicking "All" felt intrusive, like it opened something the user did not intend. "All" now simply navigates to the home page, where the hero search field is already front and center, keeping search emphasized without forcing the modal open. Removes the openSearch marker, the SearchModal mount-time auto-open effect, and the now unused search-utils helpers. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014KqafCHdPSm7GsgVWwAVHN --- docs/src/components/nav/AllHomeItem.tsx | 9 ++++----- docs/src/components/search/SearchModal.tsx | 20 +------------------- docs/src/components/search/search-utils.ts | 13 ------------- 3 files changed, 5 insertions(+), 37 deletions(-) diff --git a/docs/src/components/nav/AllHomeItem.tsx b/docs/src/components/nav/AllHomeItem.tsx index d02ece93c6..29e8ebb85d 100644 --- a/docs/src/components/nav/AllHomeItem.tsx +++ b/docs/src/components/nav/AllHomeItem.tsx @@ -2,9 +2,8 @@ * AllHomeItem.tsx * * The "All" item rendered at the top of every submenu (Components, Get started, - * Foundations). It navigates to the home page and auto-opens global search, - * so "All" means "you're searching the whole design system" rather than just - * collapsing the menu. + * Foundations). It navigates back to the home page, where the hero search field + * is front and center, rather than just collapsing the menu. * * Wrapped-onClick + sentinel `url` mirrors the existing pattern in the submenus * (e.g. handleAllComponentsClick): the menu's onNavigate ignores `/__` paths, so @@ -13,13 +12,13 @@ import { type MouseEvent } from "react"; import { GoabWorkSideMenuItem } from "@abgov/react-components"; -import { searchHomeUrl } from "../search/search-utils"; +import { withBase } from "@/lib/base-url"; export function AllHomeItem() { const handleClick = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - window.location.href = searchHomeUrl(); + window.location.href = withBase("/"); }; return ( diff --git a/docs/src/components/search/SearchModal.tsx b/docs/src/components/search/SearchModal.tsx index bf8b4ac5d6..661d728f3a 100644 --- a/docs/src/components/search/SearchModal.tsx +++ b/docs/src/components/search/SearchModal.tsx @@ -31,7 +31,7 @@ import { getFilteredOptions, type FilterOption, } from "./SearchFilterHints"; -import { getResultUrl, SEARCH_OPEN_PARAM } from "./search-utils"; +import { getResultUrl } from "./search-utils"; import "./search.css"; // ============================================================================ @@ -357,24 +357,6 @@ export function SearchModal() { return () => window.removeEventListener("goa-search-open", handleSearchOpen); }, [openModal]); - /** - * Auto-open when arriving with the `?openSearch=1` marker (set by the side - * menu's "All" item). Self-checking on mount avoids any race with this - * client:only island's hydration. The param is stripped afterwards so a - * refresh or back navigation doesn't re-trigger it. - */ - useEffect(() => { - const params = new URLSearchParams(window.location.search); - if (params.get(SEARCH_OPEN_PARAM) === "1") { - openModal(); - params.delete(SEARCH_OPEN_PARAM); - const qs = params.toString(); - const cleanUrl = - window.location.pathname + (qs ? `?${qs}` : "") + window.location.hash; - window.history.replaceState({}, "", cleanUrl); - } - }, [openModal]); - // Only render content when open - this is where lazy loading happens if (!isOpen) { return null; diff --git a/docs/src/components/search/search-utils.ts b/docs/src/components/search/search-utils.ts index 3179015fa1..bc76bc00ad 100644 --- a/docs/src/components/search/search-utils.ts +++ b/docs/src/components/search/search-utils.ts @@ -9,19 +9,6 @@ import type { SearchFilter } from "./useSearch"; import { withBase } from "@/lib/base-url"; -/** - * URL query param that, when present on any page load, auto-opens the global - * search modal. Used by the side menu's "All" item to land on the home page and - * drop the user straight into global search. Named distinctly from the tokens - * page's own `?search=` filter param to avoid collision. - */ -export const SEARCH_OPEN_PARAM = "openSearch"; - -/** Home URL that auto-opens global search on arrival. */ -export function searchHomeUrl(): string { - return withBase(`/?${SEARCH_OPEN_PARAM}=1`); -} - /** Build a URL for a search result or history item by type + slug. */ export function getResultUrl(type: string, slug: string): string { if (type === "page") {