fix(theme): resolve nav/login kit issues + bump 0.2.1 -> 0.3.0
Addresses ZB.MOM.WW.Theme/themeissues.md: - #1 NavRailSection <summary> renders aria-expanded (SSR from Expanded), kept in sync by nav-state.js on restore + toggle. - #2 nav-state.js auto-expands the section holding a.rail-link.active (transient via data-zbnav-transient — does not overwrite saved state). - #3 nav-state.js re-applies on Blazor 'enhancedload' (idempotent via per-element init guard). - #5 LoginCard wraps product in span.login-product + optional Heading override param. - #4 documented as an accepted client-only-persistence tradeoff (no code change). +4 bUnit tests (48 total, all green).
This commit is contained in:
@@ -8,7 +8,17 @@
|
||||
<div class="login-wrap rise">
|
||||
<section class="panel">
|
||||
<div class="login-body">
|
||||
<h1 class="login-title">@Product — sign in</h1>
|
||||
@* The product token is wrapped in its own span so consumers can restyle
|
||||
it and tests can assert the product in isolation (kit issue #5). Set
|
||||
Heading to replace the whole heading copy (e.g. for localization). *@
|
||||
@if (!string.IsNullOrWhiteSpace(Heading))
|
||||
{
|
||||
<h1 class="login-title">@Heading</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1 class="login-title"><span class="login-product">@Product</span> — sign in</h1>
|
||||
}
|
||||
<form method="post" action="@Action" data-enhance="false">
|
||||
@if (!string.IsNullOrEmpty(ReturnUrl))
|
||||
{
|
||||
@@ -36,9 +46,21 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>Product name shown in the card heading. Required.</summary>
|
||||
/// <summary>
|
||||
/// Product name shown in the card heading (rendered inside a
|
||||
/// <c><span class="login-product"></c>, followed by the "— sign in"
|
||||
/// suffix). Required. Ignored when <see cref="Heading"/> is set.
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired] public string Product { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional full heading override. When set (non-whitespace), it replaces the
|
||||
/// default <c><Product> — sign in</c> heading entirely — use it to
|
||||
/// localize or fully customize the heading copy. When unset, the default heading
|
||||
/// (with <see cref="Product"/> in a <c>.login-product</c> span) is rendered.
|
||||
/// </summary>
|
||||
[Parameter] public string? Heading { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Form <c>action</c> URL the sign-in POST targets. Defaults to <c>/auth/login</c>.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
Apps that want cookie-persisted expand state keep their own interactive NavSection. *@
|
||||
@namespace ZB.MOM.WW.Theme
|
||||
<details class="rail-section" open="@Expanded" data-nav-key="@ResolvedKey">
|
||||
<summary class="rail-eyebrow-toggle">@Title</summary>
|
||||
@* aria-expanded mirrors the native <details open> state so tests and assistive
|
||||
tech have a stable, queryable attribute (kit issue #1). It is rendered from
|
||||
Expanded at SSR time and kept in sync by nav-state.js on restore and toggle. *@
|
||||
<summary class="rail-eyebrow-toggle" aria-expanded="@(Expanded ? "true" : "false")">@Title</summary>
|
||||
<div class="rail-section-body">@ChildContent</div>
|
||||
</details>
|
||||
|
||||
|
||||
@@ -5,22 +5,64 @@
|
||||
(function () {
|
||||
var PREFIX = "zbnav:";
|
||||
var INIT_ATTR = "data-zbnav-initialized";
|
||||
function apply() {
|
||||
document.querySelectorAll("details.rail-section[data-nav-key]").forEach(function (el) {
|
||||
if (el.hasAttribute(INIT_ATTR)) return; // already wired — avoid duplicate listeners
|
||||
el.setAttribute(INIT_ATTR, "");
|
||||
var key = PREFIX + el.getAttribute("data-nav-key");
|
||||
var saved = null;
|
||||
try { saved = window.localStorage.getItem(key); } catch (e) { return; }
|
||||
if (saved === "1") el.open = true;
|
||||
else if (saved === "0") el.open = false;
|
||||
el.addEventListener("toggle", function () {
|
||||
try { window.localStorage.setItem(key, el.open ? "1" : "0"); } catch (e) { /* ignore */ }
|
||||
});
|
||||
var TRANSIENT_ATTR = "data-zbnav-transient";
|
||||
|
||||
// Mirror a section's native <details open> onto its <summary aria-expanded>
|
||||
// so tests and assistive tech have a stable, queryable attribute (issue #1).
|
||||
function syncAria(el) {
|
||||
var summary = el.querySelector("summary.rail-eyebrow-toggle");
|
||||
if (summary) summary.setAttribute("aria-expanded", el.open ? "true" : "false");
|
||||
}
|
||||
|
||||
function wire(el) {
|
||||
el.setAttribute(INIT_ATTR, "");
|
||||
var key = PREFIX + el.getAttribute("data-nav-key");
|
||||
var saved = null;
|
||||
try { saved = window.localStorage.getItem(key); } catch (e) { saved = null; }
|
||||
if (saved === "1") el.open = true;
|
||||
else if (saved === "0") el.open = false;
|
||||
el.addEventListener("toggle", function () {
|
||||
syncAria(el);
|
||||
// An active-link reveal (issue #2) is a transient open that must NOT
|
||||
// overwrite the user's saved preference. The reveal flags the element
|
||||
// before flipping open; consume the flag here and skip persistence.
|
||||
if (el.getAttribute(TRANSIENT_ATTR) !== null) {
|
||||
el.removeAttribute(TRANSIENT_ATTR);
|
||||
return;
|
||||
}
|
||||
try { window.localStorage.setItem(key, el.open ? "1" : "0"); } catch (e) { /* ignore */ }
|
||||
});
|
||||
}
|
||||
|
||||
function apply() {
|
||||
document.querySelectorAll("details.rail-section[data-nav-key]").forEach(function (el) {
|
||||
if (!el.hasAttribute(INIT_ATTR)) wire(el); // wire once — avoid duplicate listeners
|
||||
syncAria(el); // re-sync aria on every pass
|
||||
});
|
||||
|
||||
// Reveal the section that holds the active link even if the user (or the
|
||||
// app) left it collapsed, so the nav always shows where the user is
|
||||
// (issue #2). Transient: flagged so the toggle handler does not persist it.
|
||||
document.querySelectorAll("details.rail-section a.rail-link.active").forEach(function (link) {
|
||||
var sec = link.closest("details.rail-section");
|
||||
if (sec && !sec.open) {
|
||||
sec.setAttribute(TRANSIENT_ATTR, "");
|
||||
sec.open = true;
|
||||
syncAria(sec);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading")
|
||||
document.addEventListener("DOMContentLoaded", apply);
|
||||
else
|
||||
apply();
|
||||
|
||||
// Re-run after Blazor static-SSR enhanced navigation (or any re-render that
|
||||
// replaces the rail nodes) so freshly inserted sections are wired, restored,
|
||||
// and active-revealed (issue #3). The per-element INIT_ATTR guard keeps this
|
||||
// idempotent for nodes that survived the navigation.
|
||||
if (window.Blazor && typeof window.Blazor.addEventListener === "function") {
|
||||
window.Blazor.addEventListener("enhancedload", apply);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user