Question: Dock Header Action Button Alongside Tabs Only When Sticky #2968
Zak Siddiqui opened 2 days ago

Problem Statement

Currently:

  • Element A (.tabs): Located inside .card-body > .main
  • Element B (.more-info): Located inside .card-header

When scrolling down:

  1. .tabs uses position: sticky and sticks to top: 0.
  2. .more-info scrolls out of view because its parent container (.card-header) leaves the viewport.

Requirement

  • In Default/Unscrolled State: Both elements must remain in their original DOM locations and visual positions. Do not alter their default layout.
  • In Sticky State (On Scroll): When .tabs reaches top: 0 and becomes sticky, .more-info should dynamically dock at top: 0 alongside .tabs (sitting on the right side) so both are visible side-by-side without overlapping.

Target Elements

Element A (Tabs Bar):

<ul class="tabs nav nav-tabs nav-tabs-line nav-tabs-sticky nav-bold mb-5" id="id9c3">
  <!-- Tab items -->
</ul>

Element B (More Info Button):

<a class="more-info side-info flex-shrink-0 ml-3" id="id99b" href="javascript:;" data-tippy-content="More info">
  <svg class="icon"><use xlink:href="...#ellipsis"></use></svg>
</a>


Motivation

The .more-info button, when the side info panel is hidden, currently scrolls out of view when the user scrolls down the page. This means that in order to access the .side-info pane, the user needs to scroll all the way to the top. However, as we have done in a past issue, we moved the Workspaces UI element from the top into the .side-info pane in order to not need to scroll all the way up to the top in the first place.

Therefore, what we want to do is be able to expose the .side-info panel without needing to scroll all the way up to the top. This makes it easier for users to open and close the side info panel using the .more-info button no matter where they have scrolled in the page.


Tasks for the AI Agent Plan

Please analyze the page DOM structure and provide a step-by-step implementation plan covering:

  1. Handling Parent Scrolling Limits:
  • Standard CSS position: sticky on .more-info will stop working as soon as .card-header scrolls off-screen.
  • Propose a strategy (e.g., JS IntersectionObserver / Scroll listener toggling a CSS class like .is-docked, using position: fixed, or dynamic DOM reparenting/cloning) to retain .more-info at the top next to .tabs only during sticky scroll.
  1. Layout & Collision Alignment:
  • How to ensure .tabs reserves space on the right (e.g., padding-right or flex layout) so .more-info docks cleanly next to it without covering tab text or buttons.
  1. Smooth Transition & Edge Cases:
  • How to ensure the transition between default placement and sticky placement feels smooth and doesn't break event listeners attached to .more-info (e.g., Tippy.js tooltips or dropdown handlers).

Expected Output

  1. Technical Strategy: Recommended approach (JS scroll observer vs CSS fixed overlay vs teleportation) with pros/cons.
  2. Step-by-Step Implementation: Exact CSS, HTML, or JS modifications needed.
  3. Proposed Code Changes / Diff.
  • Service Desk commented 2 days ago

    Analysis

    Current structure on issue detail (same pattern on build / PR detail):

    .card.issue-detail
      .card-header
        .card-title
        a.more-info.side-info          <!-- Element B -->
      .card-body.d-flex
        .main
          …operations / primary…
          ul.tabs.nav-tabs-sticky      <!-- Element A: position:sticky; top:0; z-index:990 -->
          …tab content…
        div.side-info                  <!-- SideInfoPanel -->
    

    Sticky on .tabs works because it lives inside the scrolling .main / .autofit chain. Sticky on .more-info cannot work while it stays under .card-header: once the header scrolls off, sticky is clipped by that ancestor. Existing CSS already hides triggers when the pane is open (.side-info-visible a.side-info { display: none }), and PR summary links already proxy-click the header trigger (pull-request-detail.js).

    Tabbable already has a right-aligned .options slot (margin-left: auto), but it is owned by the selected tab’s renderOptions() — not a good place for a page-level more-info control without API changes.


    Recommended: sticky tabs row wrapper + second SideInfoLink + IntersectionObserver

    ApproachProsCons
    A. Sticky row + docked SideInfoLink (recommended)Keeps header link in place for default layout; docked link is a real Wicket SideInfoLink (Tippy / Ajax / SideInfoOpened stay intact); layout reserved via flex on the sticky row; no position:fixed mathSmall markup change (move sticky from ul to a thin wrapper)
    B. position:fixed overlay toggled by observerZero DOM movesMust sync top/right/height to tabs on scroll/resize; easy to drift vs side pane / dark mode blur bar
    C. Reparent / clone the header <a> into tabsVisually exactFragile with Wicket Ajax partial updates; Tippy/event rebind risk

    Why A: Default state keeps Element B in .card-header and Element A as the tab list. Only when the header trigger leaves the scrollport do we show a second trigger that already lives inside the sticky row, so it docks at top: 0 beside the tabs with no fixed-position bookkeeping.


    2. Step-by-step implementation

    Step 1 — Markup (e.g. IssueDetailPage.html)

    Wrap tabs + docked trigger; move sticky to the wrapper so both stick together. Keep the original header link unchanged:

    <div class="card-header d-flex align-items-center flex-nowrap">
      <div wicket:id="title" class="card-title flex-grow-1 mx-0"></div>
      <a wicket:id="moreInfo" class="more-info side-info flex-shrink-0 ml-3"
         t:data-tippy-content="More info">…</a>
    </div>
    …
    <div class="sticky-tabs-row d-flex align-items-center mb-5">
      <ul wicket:id="issueTabs"
          class="tabs nav nav-tabs nav-tabs-line nav-bold flex-grow-1 mb-0"></ul>
      <a wicket:id="moreInfoDock"
         class="more-info side-info more-info-dock flex-shrink-0 ml-3"
         t:data-tippy-content="More info">…</a>
    </div>
    
    • Remove nav-tabs-sticky from the ul (sticky moves to the row).
    • Default: .more-info-dock is hidden (see CSS), so the unscrolled layout matches today.

    Mirror on BuildDetailPage / PullRequestDetailPage (same header + sticky tabs pattern).

    Step 2 — Java

    Add a second SideInfoLink("moreInfoDock") next to the existing SideInfoLink("moreInfo"). Both broadcast SideInfoOpened; no click proxying required (unlike PR summary text links).

    Step 3 — CSS (base.css or page CSS)

    .sticky-tabs-row {
      position: sticky;
      top: 0;
      z-index: 990;
      /* reuse existing sticky tab chrome */
      background: rgba(255, 255, 255, 0.88);
      -webkit-backdrop-filter: blur(8px);
      backdrop-filter: blur(8px);
    }
    .dark-mode .sticky-tabs-row {
      background: rgba(35, 35, 45, 0.88);
    }
    /* hidden until docked — preserves default layout */
    .sticky-tabs-row > .more-info-dock {
      display: none;
    }
    .sticky-tabs-row.is-docked > .more-info-dock {
      display: inline-flex;
    }
    /* keep existing: body.side-info-visible a.side-info { display: none } */
    html:has(.revision-diff) .sticky-tabs-row {
      position: static; /* same exception as today’s sticky tabs on PR changes */
    }
    

    Optionally move the existing .nav-tabs-line.nav-tabs-sticky rules onto .sticky-tabs-row so all sticky-tab pages share one definition.

    Step 4 — JS (IntersectionObserver)

    Observe the header trigger against the real scroll root (the nearest scrolling ancestor, typically the .autofit scroller — not viewport alone):

    onedev.server.sideInfo.dockMoreInfoWithStickyTabs = function(options) {
      var $root = $(options.root);
      var header = $root.find(options.headerTrigger)[0];
      var $row = $root.find(options.stickyRow);
      if (!header || !$row.length) return;
    
      var scrollRoot = $row.closest(".autofit")[0] || null;
      var observer = new IntersectionObserver(function(entries) {
        var visible = entries[0].isIntersecting;
        $row.toggleClass("is-docked", !visible);
      }, { root: scrollRoot, threshold: 0 });
    
      observer.observe(header);
    };
    

    Call from page onDomReady (and after Ajax refreshes that replace the header/tabs).

    Stuck detection: “header more-info not intersecting” ≈ “tabs have reached sticky” for this layout, because the header sits above the tabs. If pixel-perfect stick detection is needed, observe a 1px sentinel placed immediately above .sticky-tabs-row instead.

    Step 5 — Collision / alignment

    • Row is display: flex; tabs flex-grow-1; dock flex-shrink-0 + ml-3 — same spacing as the header control.
    • Tabs keep wrapping (flex-wrap on the ul if needed); dock stays on the right of the row.
    • No padding-right hack required because the dock participates in flex layout only while .is-docked is on (when hidden via display: none it takes no space).

    Step 6 — Transitions & edge cases

    ConcernHandling
    Tippy / Ajax on docked controlReal second SideInfoLink — same as header
    Side pane openExisting .side-info-visible a.side-info { display: none } hides both
    PR changes / revision diffposition: static exception on the row (same as current sticky tabs)
    Ajax tab body updatesObserver targets header + row outside tab child content; re-init only if those nodes are replaced
    SmoothnessToggle class only (no DOM move). Optional short opacity transition on .more-info-dock if desired; avoid animating layout width of the tab list if it feels jumpy
    Mobile overlay side infoUnchanged — docked link still sends SideInfoOpened

    3. Proposed code touch points (diff outline)

    No code landed in this answer (Question). Implementation would touch roughly:

    1. IssueDetailPage.html (+ Build/PR detail HTML) — sticky row + moreInfoDock
    2. IssueDetailPage.java (+ Build/PR) — new SideInfoLink("moreInfoDock")
    3. base.css.sticky-tabs-row / .is-docked (lift styles off .nav-tabs-sticky)
    4. side-info.js — shared dockMoreInfoWithStickyTabs
    5. issue-detail.js / pull-request-detail.js / build detail JS — call helper from onDomReady

    If this plan looks right, we can open an implementation issue / convert follow-up work from this. If you want a different approach (fixed overlay, or always-visible dock in the tab bar), say which and we can adjust.

  • Zak Siddiqui changed title 2 days ago
    Previous Value Current Value
    Dock Header Action Button Alongside Tabs Only When Sticky
    Question: Dock Header Action Button Alongside Tabs Only When Sticky
  • Zak Siddiqui commented 2 days ago

    See #2969 for implementation

  • Zak Siddiqui changed state to 'Closed' 2 days ago
    Previous Value Current Value
    Open
    Closed
1/1
Type
Question
Priority
Normal
Assignee
Labels
No labels
Issue Votes (0)
Watchers (3)
Reference
OD-2968
Please wait...
Connection lost or session expired, reload to recover
Page is in error, reload to recover