From edd49765d615af0d9d8da0550d08fec100fc8b68 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 3 Jun 2026 02:53:15 -0400 Subject: [PATCH] feat(theme): NavRailSection data-nav-key for persistence --- .../Components/NavRailSection.razor | 17 ++++++++++++++++- .../ZB.MOM.WW.Theme.Tests/NavRailTests.cs | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor index 50268c0..bbeced4 100644 --- a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor +++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor @@ -1,7 +1,7 @@ @* Components/NavRailSection.razor — CSS-only collapsible (no JS, works in static SSR). Apps that want cookie-persisted expand state keep their own interactive NavSection. *@ @namespace ZB.MOM.WW.Theme -
+
@Title
@ChildContent
@@ -18,8 +18,23 @@ /// [Parameter] public bool Expanded { get; set; } = true; + /// + /// Stable identifier used to persist this section's open/closed state in + /// localStorage (via the kit's nav-state.js). Defaults to a slug of . + /// + [Parameter] public string? Key { get; set; } + /// /// Section content — typically children. /// [Parameter] public RenderFragment? ChildContent { get; set; } + + private string ResolvedKey => string.IsNullOrWhiteSpace(Key) ? Slug(Title) : Key!; + + private static string Slug(string s) + { + var chars = s.Trim().ToLowerInvariant() + .Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray(); + return string.Join('-', new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries)); + } } diff --git a/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/NavRailTests.cs b/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/NavRailTests.cs index 2d6eddd..017215f 100644 --- a/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/NavRailTests.cs +++ b/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/NavRailTests.cs @@ -55,4 +55,22 @@ public class NavRailTests : TestContext .AddChildContent("X")); Assert.False(cut.Find("details.rail-section").HasAttribute("open")); } + + [Fact] + public void NavRailSection_emits_data_nav_key_slug_from_title_by_default() + { + var cut = RenderComponent(p => p + .Add(x => x.Title, "Site Calls") + .AddChildContent("X")); + Assert.Equal("site-calls", cut.Find("details.rail-section").GetAttribute("data-nav-key")); + } + + [Fact] + public void NavRailSection_emits_explicit_key_when_supplied() + { + var cut = RenderComponent(p => p + .Add(x => x.Title, "Navigation").Add(x => x.Key, "nav") + .AddChildContent("X")); + Assert.Equal("nav", cut.Find("details.rail-section").GetAttribute("data-nav-key")); + } }