diff --git a/ZB.MOM.WW.Theme/Directory.Build.props b/ZB.MOM.WW.Theme/Directory.Build.props index 4212faa..2ccf1e0 100644 --- a/ZB.MOM.WW.Theme/Directory.Build.props +++ b/ZB.MOM.WW.Theme/Directory.Build.props @@ -4,7 +4,7 @@ enable enable latest - 0.3.0 + 0.3.1 true diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/layout.css b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/layout.css index 838fe06..fd81e5b 100644 --- a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/layout.css +++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/css/layout.css @@ -210,6 +210,13 @@ .rail-section > summary::-webkit-details-marker { display: none; } .rail-section > summary::before { content: '\25B6'; font-size: 0.55rem; color: var(--ink-faint); margin-right: 0.4rem; } .rail-section[open] > summary::before { content: '\25BC'; } +/* Hide a collapsed section's items explicitly. The browser's built-in +
content-hiding (::details-content content-visibility:hidden) is + unreliable once an interactive framework (e.g. Blazor InteractiveServer) + owns/re-renders the native
— a closed section can otherwise keep + showing its items under a "collapsed" chevron. An explicit display:none makes + the visual collapse work across all render modes (kit issue #6). */ +.rail-section:not([open]) > .rail-section-body { display: none; } /* StatusPill: info variant (on-palette, reuses the info blue wash) */ .chip-info { color: var(--accent-deep); background: var(--info-bg); border-color: var(--info-border); } diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js index 2fa0fb9..c318f5d 100644 --- a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js +++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/wwwroot/js/nav-state.js @@ -65,4 +65,31 @@ if (window.Blazor && typeof window.Blazor.addEventListener === "function") { window.Blazor.addEventListener("enhancedload", apply); } + + // Re-run whenever rail sections are (re)inserted into the DOM. Under an + // interactive render mode (Blazor InteractiveServer/WebAssembly/Auto) the + // prerendered
wired on DOMContentLoaded are replaced when the + // runtime adopts the page, and `enhancedload` does NOT fire — so without this + // the live sections are never wired (no persistence, no aria sync, no + // active-reveal). A MutationObserver is the render-mode-agnostic backstop; + // the per-element INIT_ATTR guard keeps re-applies idempotent, and the + // childList-only filter (plus the active-reveal's `if (!sec.open)` guard) + // avoids any observe→mutate→observe loop (issue #6). + if (typeof MutationObserver === "function") { + var observer = new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var added = mutations[i].addedNodes; + for (var j = 0; j < added.length; j++) { + var node = added[j]; + if (node.nodeType !== 1) continue; + if ((node.matches && node.matches("details.rail-section")) || + (node.querySelector && node.querySelector("details.rail-section"))) { + apply(); + return; + } + } + } + }); + observer.observe(document.documentElement, { childList: true, subtree: true }); + } })(); diff --git a/ZB.MOM.WW.Theme/themeissues.md b/ZB.MOM.WW.Theme/themeissues.md index 5ebdb88..4610f6a 100644 --- a/ZB.MOM.WW.Theme/themeissues.md +++ b/ZB.MOM.WW.Theme/themeissues.md @@ -20,6 +20,7 @@ All file references below point at the kit source under `src/ZB.MOM.WW.Theme/`. | 3 | Medium | `nav-state.js` | Persistence wires once on `DOMContentLoaded`; not re-applied after Blazor enhanced navigation / dynamic re-render. | ✅ Fixed | | 4 | Low | `NavRailSection` | Always-expanded SSR default causes a flash / layout shift of collapsed sections on load. | 📄 Accepted tradeoff (documented) | | 5 | Low (optional) | `LoginCard` | Heading bakes the localizable `— sign in` suffix into the product title with no separate hook. | ✅ Fixed | +| 6 | High | `NavRailSection` / `nav-state.js` | Under **interactive** Blazor render mode the whole collapsible nav is non-functional: clicking a header doesn't hide items, and `nav-state.js` never wires (no aria sync, no persistence, no active-reveal). | ❌ Open (found in 0.3.0) | --- @@ -230,6 +231,92 @@ or add an optional `Heading` parameter that, when set, replaces the default head --- +## Issue 6 — Collapsible nav is non-functional under interactive Blazor render mode + +**Severity:** High · **Files:** `Components/NavRailSection.razor`, `wwwroot/js/nav-state.js`, +`wwwroot/css/layout.css` · **Status:** Open (found in 0.3.0) + +> **This corrects Issue 3's note**, which claimed interactive Blazor Server consumers are +> "largely unaffected because the rail is patched in place." Direct observation of the live +> ScadaBridge Central UI (global `@rendermode InteractiveServer`) shows that is **false** — +> the kit's `
`/JS nav does not work under interactive render modes at all. + +**Symptom.** In an app that renders the rail under an interactive render mode +(`InteractiveServer`, `InteractiveWebAssembly`, or `InteractiveAuto`), the collapsible nav is +visually and functionally dead: + +1. Clicking a section header toggles the chevron (▶/▼) but **does not hide the section's + items** — the links stay fully visible under a "collapsed" chevron. +2. `aria-expanded` never changes, `localStorage` is never written, the saved state is not + restored on reload, and the active-section auto-reveal (Issue 2) does not fire. + +**Root cause.** The kit nav is a **static-SSR / CSS-only** design (NavRailSection's own +comment: *"works in static SSR"*). Under an interactive render mode, Blazor's runtime +**owns and re-renders the `
`/`` DOM** after it adopts the prerendered +markup. Two independent consequences, both observed live: + +- **Native collapse is defeated.** On the live page a closed section has `details.open === false` + and its `::details-content` computes `content-visibility: hidden`, yet the + `.rail-section-body` and its links remain laid out and visible (measured non-zero height / + non-null `offsetParent`). Blazor's management of the native `
` desyncs the browser's + built-in content-hiding. The body's `display` value (flex/block/grid/inline) makes no + difference — only an explicit `display: none` actually hides it. +- **`nav-state.js` never wires the live DOM.** The interactive `
` elements have **no + `data-zbnav-initialized` attribute**, i.e. `wire()` never ran on them: `apply()` runs on + `DOMContentLoaded` against the *prerendered* nodes, which Blazor then replaces, and the only + re-run hook (`enhancedload`, added for Issue 3) does not fire under interactive render modes. + So aria sync, localStorage persistence, and active-reveal are all inert. + +This matters for the kit's stated goal: per the normalization notes, nav-expand persistence was +promoted into the kit at 0.2.0 *"so all three apps share one persistence mechanism."* One of the +three consumers (ScadaBridge Central UI) is interactive Blazor Server, where that mechanism +silently does nothing. + +**Verified evidence (live, global InteractiveServer).** On a logged-in dashboard: +`data-zbnav-initialized` absent on every `details.rail-section`; after clicking a header, +`details.open === false` but the section's link still reports `clientHeight: 33` and a non-null +`offsetParent`; setting `.rail-section-body { display:none }` is the only thing that hides it; +`localStorage` has no `zbnav:*` keys before or after toggling. + +**Recommended fix (two parts — both belong in the kit).** + +1. **Make the collapse render-mode-robust (CSS).** Don't rely solely on the native + `
` content-hiding; hide the body explicitly when closed: + + ```css + /* layout.css — robust across render modes; native ::details-content hiding + is unreliable once an interactive framework manages the
. */ + .rail-section:not([open]) > .rail-section-body { display: none; } + ``` + + (Verified live: this is exactly what hides the items.) + +2. **Make persistence/aria/reveal work under interactive render.** `enhancedload` is + static-SSR-only; also wire after the interactive runtime has (re)rendered. Options, in + preference order: + - Re-run `apply()` from Blazor's post-render hooks — `Blazor.addEventListener('afterStarted', …)` + (interactive WASM/Server boot) and re-apply on circuit/render updates; and/or + - Add a `MutationObserver` on the rail container that calls `apply()` when + `details.rail-section` nodes are added/replaced (framework-agnostic backstop — covers + interactive re-renders, enhanced nav, and dynamic nav alike); + - **Or** ship an explicitly **interactive** `NavRailSection` variant (a small Blazor + component with an `@onclick` toggle and `[Parameter] bool Expanded` two-way state) for + consumers that render interactively — which is what NavRailSection's own comment already + gestures at (*"Apps that want cookie-persisted expand state keep their own interactive + NavSection"*). If the kit's intent is that interactive apps bring their own section + component, say so loudly in the docs and have the CSS-only one degrade gracefully (part 1 + still applies so at least the visual collapse works). + +**Verify.** In an interactive-render host: clicking a header hides the section's items; the +summary's `aria-expanded` flips; `localStorage` gets a `zbnav:` entry; the state survives +a reload; and deep-linking into a collapsed section reveals it. + +**Consumer note (ScadaBridge).** Until the kit ships this, ScadaBridge's Central UI nav +collapse is a no-op; the Playwright `NavCollapseTests` that exercise toggling, persistence, and +auto-reveal are therefore testing behavior the app does not currently have. + +--- + ## Not kit bugs — expected consumer adaptations For the avoidance of doubt, the following are **not** theme issues; they are the normal cost