@defra/docusaurus-theme-govuk 0.0.6-alpha → 0.0.7-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/docusaurus-theme-govuk",
3
- "version": "0.0.6-alpha",
3
+ "version": "0.0.7-alpha",
4
4
  "description": "A Docusaurus theme implementing the GOV.UK Design System for consistent, accessible documentation sites",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import '../../css/theme.scss';
3
- import {SkipLink, Header, Footer, PhaseBanner, ServiceNavigation, NavigationMenu} from '@not-govuk/simple-components';
3
+ import {SkipLink, Header, Footer, PhaseBanner, ServiceNavigation} from '@not-govuk/simple-components';
4
4
  import SidebarNav from '../SidebarNav';
5
5
  import {useLocation} from '@docusaurus/router';
6
6
  import Head from '@docusaurus/Head';
@@ -141,11 +141,6 @@ export default function Layout(props) {
141
141
  }))
142
142
  : null;
143
143
 
144
- // Anchor-based sidebars (auto-generated) use a hash-aware component.
145
- // Page-based sidebars (manually configured arrays) use NavigationMenu which
146
- // uses React Router's active detection for sub-page expansion.
147
- const isAnchorSidebar = effectiveSidebar?._auto === true;
148
-
149
144
  // Convert navigation to service navigation format (Level 1 only)
150
145
  const serviceNavItems = navigation.map(item => ({
151
146
  href: withBase(item.href),
@@ -198,9 +193,7 @@ export default function Layout(props) {
198
193
  {sidebarItems ? (
199
194
  <div className="app-layout-sidebar">
200
195
  <aside className="app-layout-sidebar__nav">
201
- {isAnchorSidebar
202
- ? <SidebarNav items={sidebarItems} />
203
- : <NavigationMenu items={sidebarItems} />}
196
+ <SidebarNav items={sidebarItems} />
204
197
  </aside>
205
198
  <div className="app-layout-sidebar__content">
206
199
  {children}
@@ -1,21 +1,24 @@
1
1
  import React, { useState, useEffect } from 'react';
2
+ import { useLocation } from '@docusaurus/router';
2
3
 
3
4
  /**
4
- * Hash-aware sidebar navigation for anchor-based (auto-generated) sidebars.
5
+ * Hash- and pathname-aware sidebar navigation.
6
+ *
7
+ * Works for both anchor-based (auto-generated) and page-based (manual) sidebars.
5
8
  *
6
9
  * SSR / no-JS: all groups are rendered expanded so content is accessible.
7
- * CSR after hydration: exactly one group expands based on the URL hash.
8
- * - hash matches the group's h2 anchor that group expands
9
- * - hash matches any child's anchor that group expands
10
- * - no hash all groups collapse
10
+ * CSR after hydration:
11
+ * - Anchor hrefs (e.g. /api#constructor): active when pathname AND hash both match
12
+ * - Page hrefs (e.g. /building-a-plugin): active when pathname matches
13
+ * - Groups expand when the group itself or any child is active
11
14
  *
12
15
  * State sentinel:
13
- * null = not yet hydrated (SSR or before first useEffect) → expand all
14
- * '' = JS loaded, no hash → collapse all
15
- * str = JS loaded, hash present → expand matching group only
16
+ * null = not yet hydrated expand all groups (SSR safe)
17
+ * str = JS loaded (empty string means no hash present)
16
18
  */
17
19
  export default function SidebarNav({ items }) {
18
20
  const [hash, setHash] = useState(null);
21
+ const location = useLocation();
19
22
 
20
23
  useEffect(() => {
21
24
  const update = () => setHash(window.location.hash.slice(1));
@@ -24,6 +27,28 @@ export default function SidebarNav({ items }) {
24
27
  return () => window.removeEventListener('hashchange', update);
25
28
  }, []);
26
29
 
30
+ // Split an href into its path and anchor components.
31
+ function parseHref(href) {
32
+ if (!href) return { path: '', anchor: '' };
33
+ const idx = href.indexOf('#');
34
+ if (idx === -1) return { path: href, anchor: '' };
35
+ return { path: href.slice(0, idx) || '/', anchor: href.slice(idx + 1) };
36
+ }
37
+
38
+ // Return true if the given href matches the current browser location.
39
+ // Anchor hrefs: both pathname and hash must match.
40
+ // Page hrefs: pathname match is sufficient (exact or child path).
41
+ function isActive(href) {
42
+ const { path, anchor } = parseHref(href);
43
+ if (anchor) {
44
+ return location.pathname === path && hash === anchor;
45
+ }
46
+ return (
47
+ path !== '' &&
48
+ (location.pathname === path || location.pathname.startsWith(path + '/'))
49
+ );
50
+ }
51
+
27
52
  const cls = 'not-govuk-navigation-menu';
28
53
  const lCls = `${cls}__list`;
29
54
 
@@ -31,23 +56,16 @@ export default function SidebarNav({ items }) {
31
56
  <nav className={cls}>
32
57
  <ul className={lCls}>
33
58
  {items.map((item, i) => {
34
- const anchor = item.href ? (item.href.split('#')[1] ?? '') : '';
35
59
  const hasChildren = Array.isArray(item.items) && item.items.length > 0;
36
- const childAnchors = hasChildren
37
- ? item.items.map(sub => sub.href ? (sub.href.split('#')[1] ?? '') : '')
38
- : [];
39
60
 
40
- // null → not yet hydrated, expand everything
41
- // '' → no hash, collapse everything
42
- // value expand if hash is this group's h2 anchor or any child anchor
43
- const hashMatchesGroup =
44
- hash !== '' && (
45
- hash === anchor ||
46
- childAnchors.includes(hash)
47
- );
61
+ // Pre-hydration (hash === null): expand everything so content is
62
+ // accessible without JS. After hydration: expand only if this group
63
+ // or one of its children is the active location.
64
+ const expanded =
65
+ hasChildren &&
66
+ (hash === null || isActive(item.href) || item.items.some(sub => isActive(sub.href)));
48
67
 
49
- const expanded = hasChildren && (hash === null || hashMatchesGroup);
50
- const active = hash !== null && hashMatchesGroup;
68
+ const active = hash !== null && isActive(item.href);
51
69
 
52
70
  return (
53
71
  <li
@@ -60,8 +78,7 @@ export default function SidebarNav({ items }) {
60
78
  {expanded && (
61
79
  <ul className={`${lCls}__subitems`}>
62
80
  {item.items.map((sub, j) => {
63
- const subAnchor = sub.href ? (sub.href.split('#')[1] ?? '') : '';
64
- const subActive = hash !== null && hash === subAnchor;
81
+ const subActive = hash !== null && isActive(sub.href);
65
82
  return (
66
83
  <li
67
84
  key={j}
@@ -82,3 +99,4 @@ export default function SidebarNav({ items }) {
82
99
  </nav>
83
100
  );
84
101
  }
102
+