mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-03 13:54:41 +00:00
Merge pull request #221 from MacRimi/hotfix/doc-nav-storage-share-anchors
Hotfix: doc-navigation skips sidebar anchor-only section headers
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
import { Link, usePathname } from "@/i18n/navigation"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { useEffect, useState } from "react"
|
||||
import { sidebarItems } from "@/components/DocSidebar"
|
||||
|
||||
interface DocNavigationProps {
|
||||
@@ -31,6 +30,21 @@ interface FlatPage {
|
||||
sectionI18nKey?: string
|
||||
}
|
||||
|
||||
// Sidebar entries whose href contains a fragment (`#host`, `#lxc-net`,
|
||||
// …) are visual section headers that group submenu items inside an
|
||||
// existing physical page (currently only Storage Share Manager uses
|
||||
// this pattern — `Host storage integration` and `LXC network sharing`
|
||||
// are headers for groups of subpages, but their href is the parent
|
||||
// Overview page with an anchor). They aren't standalone docs the
|
||||
// reader can advance to, so including them in the Previous/Next walk
|
||||
// produces two regressions: on the page they anchor (`#host`) Next
|
||||
// circles back to the same URL, and on a regular page that happens to
|
||||
// sit next to one of them in the flat list (`lxc-mount-points`) Next
|
||||
// jumps to the section header instead of skipping to the next real
|
||||
// subpage. Skip them at walk time so both cases collapse to "the next
|
||||
// real page in reading order".
|
||||
const isAnchorOnlyHref = (href: string) => href.includes("#")
|
||||
|
||||
function walkSubmenu(
|
||||
items: SubMenuItem[],
|
||||
section: string,
|
||||
@@ -38,13 +52,15 @@ function walkSubmenu(
|
||||
out: FlatPage[],
|
||||
) {
|
||||
items.forEach((sub) => {
|
||||
out.push({
|
||||
title: sub.title,
|
||||
i18nKey: sub.i18nKey,
|
||||
href: sub.href,
|
||||
section,
|
||||
sectionI18nKey,
|
||||
})
|
||||
if (!isAnchorOnlyHref(sub.href)) {
|
||||
out.push({
|
||||
title: sub.title,
|
||||
i18nKey: sub.i18nKey,
|
||||
href: sub.href,
|
||||
section,
|
||||
sectionI18nKey,
|
||||
})
|
||||
}
|
||||
if (sub.submenu && sub.submenu.length > 0) {
|
||||
walkSubmenu(sub.submenu, section, sectionI18nKey, out)
|
||||
}
|
||||
@@ -56,26 +72,6 @@ export function DocNavigation({ className }: DocNavigationProps) {
|
||||
const tNav = useTranslations("docNav")
|
||||
const tSidebar = useTranslations("docSidebar")
|
||||
|
||||
// Capture the URL hash (`#host`, `#lxc-net`, …) on the client so we
|
||||
// can disambiguate Previous/Next when a single doc page hosts several
|
||||
// sidebar entries via in-page anchors (Storage Share Manager is the
|
||||
// canonical case: /docs/storage-share + /docs/storage-share#host +
|
||||
// /docs/storage-share#lxc-net are three distinct sidebar items but a
|
||||
// single physical page; usePathname() returns the same string for
|
||||
// all three because the fragment is not part of the path).
|
||||
//
|
||||
// SSR can't see the hash, so we hydrate with an empty string and
|
||||
// refresh on mount + on hashchange. The brief render before
|
||||
// hydration just shows the navigation as if the user were at the
|
||||
// parent page — same behaviour as before this fix, so no regression.
|
||||
const [hash, setHash] = useState("")
|
||||
useEffect(() => {
|
||||
const sync = () => setHash(window.location.hash || "")
|
||||
sync()
|
||||
window.addEventListener("hashchange", sync)
|
||||
return () => window.removeEventListener("hashchange", sync)
|
||||
}, [])
|
||||
|
||||
const tItem = (i18nKey: string | undefined, fallback: string) => {
|
||||
if (!i18nKey) return fallback
|
||||
try {
|
||||
@@ -89,7 +85,7 @@ export function DocNavigation({ className }: DocNavigationProps) {
|
||||
const flatItems: FlatPage[] = []
|
||||
|
||||
sidebarItems.forEach((item) => {
|
||||
if (item.href) {
|
||||
if (item.href && !isAnchorOnlyHref(item.href)) {
|
||||
flatItems.push({ title: item.title, i18nKey: item.i18nKey, href: item.href })
|
||||
}
|
||||
|
||||
@@ -124,29 +120,14 @@ export function DocNavigation({ className }: DocNavigationProps) {
|
||||
const stripTrailingSlash = (s: string) => (s !== "/" ? s.replace(/\/+$/, "") : s)
|
||||
const normalizedPathname = stripTrailingSlash(pathname)
|
||||
|
||||
// Match attempt order:
|
||||
// 1. pathname + hash (e.g. /docs/storage-share#host) — exact match
|
||||
// against sidebar items that intentionally point to an in-page
|
||||
// anchor as the "current location" for navigation purposes.
|
||||
// 2. pathname alone — the regular case, no anchor in the URL.
|
||||
//
|
||||
// Without step 1, every anchor visit collapsed to the parent page
|
||||
// and Next/Previous walked from there — so on /docs/storage-share#host
|
||||
// the bottom bar offered the same #host as Next (no movement) and on
|
||||
// /docs/storage-share/lxc-mount-points/ Next pointed back at #host
|
||||
// because the entire flat list got indexed from position 0.
|
||||
const effectivePath = normalizedPathname + hash
|
||||
let currentPageIndex = -1
|
||||
if (hash) {
|
||||
currentPageIndex = allPages.findIndex(
|
||||
(page) => stripTrailingSlash(page.href) === effectivePath,
|
||||
)
|
||||
}
|
||||
if (currentPageIndex === -1) {
|
||||
currentPageIndex = allPages.findIndex(
|
||||
(page) => stripTrailingSlash(page.href) === normalizedPathname,
|
||||
)
|
||||
}
|
||||
// Anchored URLs (`/docs/storage-share/#host`) share their pathname
|
||||
// with the Overview page, so they collapse to that page's flat-list
|
||||
// index — Previous walks back into the previous section as usual and
|
||||
// Next advances to the first real subpage (`host-nfs`) instead of
|
||||
// looping back to the same anchor.
|
||||
const currentPageIndex = allPages.findIndex(
|
||||
(page) => stripTrailingSlash(page.href) === normalizedPathname,
|
||||
)
|
||||
|
||||
const prevPage = currentPageIndex > 0 ? allPages[currentPageIndex - 1] : null
|
||||
const nextPage = currentPageIndex < allPages.length - 1 ? allPages[currentPageIndex + 1] : null
|
||||
|
||||
Reference in New Issue
Block a user