fix(theme): 0.3.1 — interactive-render nav backstop (issue #6)
Under an interactive Blazor render mode the runtime replaces the prerendered
<details> after DOMContentLoaded, so nav-state.js (wired on load, re-run only on
'enhancedload') never wires the live rail — no aria sync, no persistence, no
active-reveal — and native <details> content-hiding is unreliable, leaving a
collapsed section's items visible. 0.3.1:
- nav-state.js: add a MutationObserver backstop that re-runs apply() when
details.rail-section nodes are (re)inserted; idempotent via the per-element
init guard, loop-safe (childList-only + active-reveal's !open guard).
- layout.css: explicit .rail-section:not([open]) > .rail-section-body{display:none}
so visual collapse works across all render modes.
- themeissues.md: document issue #6; Directory.Build.props 0.3.0 -> 0.3.1.
48 bUnit tests green.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Version>0.3.0</Version>
|
<Version>0.3.1</Version>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -210,6 +210,13 @@
|
|||||||
.rail-section > summary::-webkit-details-marker { display: none; }
|
.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 > summary::before { content: '\25B6'; font-size: 0.55rem; color: var(--ink-faint); margin-right: 0.4rem; }
|
||||||
.rail-section[open] > summary::before { content: '\25BC'; }
|
.rail-section[open] > summary::before { content: '\25BC'; }
|
||||||
|
/* Hide a collapsed section's items explicitly. The browser's built-in
|
||||||
|
<details> content-hiding (::details-content content-visibility:hidden) is
|
||||||
|
unreliable once an interactive framework (e.g. Blazor InteractiveServer)
|
||||||
|
owns/re-renders the native <details> — 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) */
|
/* 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); }
|
.chip-info { color: var(--accent-deep); background: var(--info-bg); border-color: var(--info-border); }
|
||||||
|
|||||||
@@ -65,4 +65,31 @@
|
|||||||
if (window.Blazor && typeof window.Blazor.addEventListener === "function") {
|
if (window.Blazor && typeof window.Blazor.addEventListener === "function") {
|
||||||
window.Blazor.addEventListener("enhancedload", apply);
|
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 <details> 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 });
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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 |
|
| 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) |
|
| 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 |
|
| 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 `<details>`/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 `<details>`/`<summary>` 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 `<details>` 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 `<details>` 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
|
||||||
|
`<details>` 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 <details>. */
|
||||||
|
.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:<key>` 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
|
## Not kit bugs — expected consumer adaptations
|
||||||
|
|
||||||
For the avoidance of doubt, the following are **not** theme issues; they are the normal cost
|
For the avoidance of doubt, the following are **not** theme issues; they are the normal cost
|
||||||
|
|||||||
Reference in New Issue
Block a user