mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-11 11:06:24 +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 { Link, usePathname } from "@/i18n/navigation"
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { sidebarItems } from "@/components/DocSidebar"
|
import { sidebarItems } from "@/components/DocSidebar"
|
||||||
|
|
||||||
interface DocNavigationProps {
|
interface DocNavigationProps {
|
||||||
@@ -31,6 +30,21 @@ interface FlatPage {
|
|||||||
sectionI18nKey?: string
|
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(
|
function walkSubmenu(
|
||||||
items: SubMenuItem[],
|
items: SubMenuItem[],
|
||||||
section: string,
|
section: string,
|
||||||
@@ -38,13 +52,15 @@ function walkSubmenu(
|
|||||||
out: FlatPage[],
|
out: FlatPage[],
|
||||||
) {
|
) {
|
||||||
items.forEach((sub) => {
|
items.forEach((sub) => {
|
||||||
out.push({
|
if (!isAnchorOnlyHref(sub.href)) {
|
||||||
title: sub.title,
|
out.push({
|
||||||
i18nKey: sub.i18nKey,
|
title: sub.title,
|
||||||
href: sub.href,
|
i18nKey: sub.i18nKey,
|
||||||
section,
|
href: sub.href,
|
||||||
sectionI18nKey,
|
section,
|
||||||
})
|
sectionI18nKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
if (sub.submenu && sub.submenu.length > 0) {
|
if (sub.submenu && sub.submenu.length > 0) {
|
||||||
walkSubmenu(sub.submenu, section, sectionI18nKey, out)
|
walkSubmenu(sub.submenu, section, sectionI18nKey, out)
|
||||||
}
|
}
|
||||||
@@ -56,26 +72,6 @@ export function DocNavigation({ className }: DocNavigationProps) {
|
|||||||
const tNav = useTranslations("docNav")
|
const tNav = useTranslations("docNav")
|
||||||
const tSidebar = useTranslations("docSidebar")
|
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) => {
|
const tItem = (i18nKey: string | undefined, fallback: string) => {
|
||||||
if (!i18nKey) return fallback
|
if (!i18nKey) return fallback
|
||||||
try {
|
try {
|
||||||
@@ -89,7 +85,7 @@ export function DocNavigation({ className }: DocNavigationProps) {
|
|||||||
const flatItems: FlatPage[] = []
|
const flatItems: FlatPage[] = []
|
||||||
|
|
||||||
sidebarItems.forEach((item) => {
|
sidebarItems.forEach((item) => {
|
||||||
if (item.href) {
|
if (item.href && !isAnchorOnlyHref(item.href)) {
|
||||||
flatItems.push({ title: item.title, i18nKey: item.i18nKey, href: 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 stripTrailingSlash = (s: string) => (s !== "/" ? s.replace(/\/+$/, "") : s)
|
||||||
const normalizedPathname = stripTrailingSlash(pathname)
|
const normalizedPathname = stripTrailingSlash(pathname)
|
||||||
|
|
||||||
// Match attempt order:
|
// Anchored URLs (`/docs/storage-share/#host`) share their pathname
|
||||||
// 1. pathname + hash (e.g. /docs/storage-share#host) — exact match
|
// with the Overview page, so they collapse to that page's flat-list
|
||||||
// against sidebar items that intentionally point to an in-page
|
// index — Previous walks back into the previous section as usual and
|
||||||
// anchor as the "current location" for navigation purposes.
|
// Next advances to the first real subpage (`host-nfs`) instead of
|
||||||
// 2. pathname alone — the regular case, no anchor in the URL.
|
// looping back to the same anchor.
|
||||||
//
|
const currentPageIndex = allPages.findIndex(
|
||||||
// Without step 1, every anchor visit collapsed to the parent page
|
(page) => stripTrailingSlash(page.href) === normalizedPathname,
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevPage = currentPageIndex > 0 ? allPages[currentPageIndex - 1] : null
|
const prevPage = currentPageIndex > 0 ? allPages[currentPageIndex - 1] : null
|
||||||
const nextPage = currentPageIndex < allPages.length - 1 ? allPages[currentPageIndex + 1] : null
|
const nextPage = currentPageIndex < allPages.length - 1 ? allPages[currentPageIndex + 1] : null
|
||||||
|
|||||||
Reference in New Issue
Block a user