diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor index 88b0d264..85a70b8d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor @@ -5,8 +5,12 @@ (brand bar + responsive hamburger) belongs to ThemeShell; this component contributes only the nav items. Collapsible sections use the kit's NavRailSection (
); their open/closed state is persisted client-side - by the kit's nav-state.js (localStorage, keyed by Key) — no JS interop here. *@ + by the kit's nav-state.js (localStorage, keyed by Key) — no JS interop here. + Everything below is wrapped in .rail-scroll, the scroll pane defined in + site.css: a long nav scrolls under the brand bar while the rail footer + stays pinned, and the kit itself is untouched. *@ +
@@ -153,3 +157,4 @@ +
diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css b/src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css index 110bc2ea..83d2595c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/css/site.css @@ -170,3 +170,24 @@ /* Backdrop — slightly darker overlay in dark mode */ --sb-backdrop: rgba(0, 0, 0, 0.6); } + +/* ── Rail nav scroll pane ──────────────────────────────────────────────── + ThemeShell's nav.side-rail is a flex column inside the sticky 100vh + #theme-rail (lg+), but it never fills that height and nothing inside it + scrolls, so a long nav grows past the viewport and carries the footer + with it. Fill the rail and let the nav items (NavMenu's .rail-scroll + wrapper) scroll while the brand bar and .rail-foot stay put. Below lg the + rail is inline and scrolls with the page, so the rule is scoped to lg+. + The kit is untouched (deferred-work register: upstream into + ZB.MOM.WW.Theme so the other consumers get it too). */ +@media (min-width: 992px) { + #theme-rail > .side-rail { + height: 100%; + } + .rail-scroll { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Layout/NavMenuTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Layout/NavMenuTests.cs index f5c80f8a..8f66b005 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Layout/NavMenuTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Layout/NavMenuTests.cs @@ -241,4 +241,25 @@ public class NavMenuTests : BunitContext Assert.DoesNotContain("/audit/log", cut.Markup); }); } + + /// + /// ThemeShell's nav.side-rail never fills the sticky 100vh rail, so + /// a long expanded nav grows past the viewport and pushes + /// .rail-foot off-screen. NavMenu wraps its items in a + /// div.rail-scroll pane that site.css makes scroll on its + /// own (lg+), leaving the brand bar and rail footer pinned. This asserts + /// the wrapper exists and actually contains the nav sections, not just + /// that the class string appears somewhere in the markup. + /// + [Fact] + public void NavItems_RenderInsideTheRailScrollPane() + { + var cut = RenderWithRoles("Administrator", "Designer", "Deployer"); + + cut.WaitForAssertion(() => + { + Assert.Contains("class=\"rail-scroll\"", cut.Markup); + Assert.NotEmpty(cut.FindAll("div.rail-scroll details.rail-section")); + }); + } }