فهرست منبع

Closes #22563: Preserve sidebar scroll position across navigation (#22803)

Store the vertical sidebar's scroll position in session storage so it
remains scoped to the current browser tab. Restore it on the next page
and use the browser's native scrolling behavior to reveal the active
item when needed.

When the active dropdown is closed, reveal its top-level heading instead
of the hidden child. Persist only positive offsets from an overflowing
sidebar so a collapsed or non-scrollable layout cannot erase a useful
saved position with zero.
Martin Hauser 1 ماه پیش
والد
کامیت
368ad277e5

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
netbox/project-static/dist/netbox.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
netbox/project-static/dist/netbox.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 50 - 0
netbox/project-static/src/sidenav.ts

@@ -7,6 +7,9 @@ type BodyAttr = 'show' | 'hide' | 'hidden' | 'pinned';
 // Keep in sync with Bootstrap's `lg` breakpoint and `navbar-expand-lg` in base/layout.html.
 const SIDENAV_DESKTOP_MEDIA = '(min-width: 992px)';
 
+// Session-scoped, unlike the localStorage `netbox-sidenav` pin state.
+const SCROLL_STATE_KEY = 'netbox-sidenav-scroll';
+
 class SideNav {
   /**
    * Sidenav container element.
@@ -18,6 +21,11 @@ class SideNav {
    */
   private state: StateManager<NavState>;
 
+  /**
+   * First nav item matching the current page, cached at construction.
+   */
+  private activePageLink: Nullable<HTMLDivElement> = null;
+
   constructor(base: HTMLElement) {
     this.base = base;
     this.state = new StateManager<NavState>(
@@ -27,6 +35,7 @@ class SideNav {
 
     this.init();
     this.initLinks();
+    this.initScrollPosition();
   }
 
   /**
@@ -87,6 +96,8 @@ class SideNav {
    */
   private initLinks(): void {
     for (const link of this.getActiveLinks()) {
+      this.activePageLink ??= link;
+
       if (this.bodyHas('show')) {
         this.activateLink(link, 'expand');
       } else if (this.bodyHas('hidden')) {
@@ -186,6 +197,45 @@ class SideNav {
     }
   }
 
+  /**
+   * Whether the aside currently overflows, as opposed to whether the layout lets it scroll at all.
+   */
+  private isScrollable(): boolean {
+    return this.base.scrollHeight > this.base.clientHeight;
+  }
+
+  /**
+   * Restore the offset saved for this browser tab, reveal the active item when it falls outside
+   * the sidebar viewport, and save the offset again on the way out. One offset per tab, so a
+   * shorter menu can clamp a deeper value.
+   */
+  private initScrollPosition(): void {
+    const initiallyScrollable = this.isScrollable();
+    const storedScrollTop = sessionStorage.getItem(SCROLL_STATE_KEY);
+
+    // Leave a missing offset alone so the browser's own restoration survives.
+    if (storedScrollTop !== null) {
+      this.base.scrollTop = Number(storedScrollTop);
+    }
+
+    // Reveal only in the fixed desktop layout. In the mobile flow layout, scrollIntoView would scroll the page.
+    if (window.matchMedia(SIDENAV_DESKTOP_MEDIA).matches) {
+      const menu = this.activePageLink?.closest<HTMLElement>('.nav-item.dropdown');
+      // A closed dropdown has no box to measure, so reveal its heading instead.
+      const target =
+        menu && !menu.querySelector('.dropdown-menu.show') ? menu : this.activePageLink;
+
+      target?.scrollIntoView({ block: 'nearest' });
+    }
+
+    window.addEventListener('pagehide', () => {
+      // Zero counts only from a sidebar that was already scrollable at load, not one that grew on hover.
+      if (this.isScrollable() && (initiallyScrollable || this.base.scrollTop > 0)) {
+        sessionStorage.setItem(SCROLL_STATE_KEY, String(this.base.scrollTop));
+      }
+    });
+  }
+
   /**
    * Show the sidenav and expand any active menu groups.
    */

+ 6 - 0
netbox/project-static/styles/transitional/_navigation.scss

@@ -7,6 +7,12 @@
     overflow-y: auto;
   }
 
+  // Gap for the reveal in sidenav.ts, which scrolls with `block: 'nearest'`
+  .nav-item.dropdown,
+  .dropdown-item {
+    scroll-margin-block: 0.5rem;
+  }
+
   // Render the <button> menu headings identically to the surrounding nav links
   .navbar-collapse .nav-item.dropdown > button.nav-link {
     width: 100%;

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است