Implementation Plan: Dock Header Action Button Alongside Tabs Only When Sticky #2969
Zak Siddiqui opened 2 days ago

Relates to #2968

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.


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)

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
  • Zak Siddiqui commented 2 days ago

    I'm not so sure about .align-items-center.

    I suspect stretch might be a better option, which would motivate the.more-info element all the way over to the right so that it's out of the way of all of the nav tabs.

    This would be a much cleaner look because it signifies that the more info button is not part of the nav tabs.

  • Zak Siddiqui commented 2 days ago

    @servicedesk please create text-based mock-up diagrams of what the various states would look like visually. Place them in code blocks so that they look neat.

  • Service Desk commented 2 days ago

    {"content": "@zaakiy Here are text mock-ups of the main visual states for the sticky-tabs + more-info docking plan. Legend: [···] = more-info (ellipsis), | = sticky row edge, === = sticky chrome.\n\n### 1. Default / unscrolled (unchanged)\n\nHeader more-info stays in .card-header. Docked more-info exists in the DOM but is hidden (display: none), so the tabs row looks as it does today.\n\n\n┌──────────────────────────────────────────────────────────────┐\n│ Issues · #1234 · Title of the issue… [···] │ ← card-header\n├──────────────────────────────────────────────────────────────┤\n│ [operations / primary content…] │\n│ │\n│ Activities Commits Builds Files │ ← tabs (not sticky yet)\n│ ───────────────────────────────────────── │\n│ Tab content… │\n│ … │\n└──────────────────────────────────────────────────────────────┘\n\n\n### 2. Today after scroll (problem)\n\nTabs stick; header (and its more-info) scroll away. Side info is unreachable without scrolling back up.\n\n\n┌──────────────────────────────────────────────────────────────┐\n│ Activities Commits Builds Files │ ← sticky tabs\n│ ───────────────────────────────────────── │\n│ Tab content… │\n│ … │\n│ … more scrolled content … │\n│ (no [···] here) │\n└──────────────────────────────────────────────────────────────┘\n\n\n### 3. Proposed sticky / docked — align-items-center\n\nWhen the header trigger leaves the scrollport, .sticky-tabs-row gets .is-docked and the docked more-info appears on the right of the sticky row. With align-items-center, the control is vertically centered with the tab labels.\n\n\n┌──────────────────────────────────────────────────────────────┐\n│ Activities Commits Builds Files [···] │ ← sticky row\n│ ───────────────────────────────────────── │ dock sits mid-row\n│ Tab content… │\n│ … │\n└──────────────────────────────────────────────────────────────┘\n ↑ tabs grow left ↑ dock (centered)\n\n\n### 4. Side info open\n\nExisting rule body.side-info-visible a.side-info { display: none } hides both the header and docked triggers. Pane takes the right side.\n\n\n┌────────────────────────────────────────────┬─────────────────┐\n│ Activities Commits Builds Files │ Side info │\n│ ───────────────────────────────────── │ ───────── │\n│ Tab content… │ fields / │\n│ … │ workspaces / │\n│ │ … │\n│ (no [···]) │ [×] │\n└────────────────────────────────────────────┴─────────────────┘\n\n\n### 5. Transition summary\n\n\nunscrolled scroll past header side info open\n─────────── ────────────────── ──────────────\nheader [···] shown header [···] off-screen all a.side-info hidden\ndock hidden dock shown in sticky row pane visible\n\n\nSticky state uses the centered dock in (3) with align-items-center on .sticky-tabs-row."}

  • Zak Siddiqui commented 2 days ago

    @servicedesk my apologies.

    3. Proposed sticky / docked — align-items-center (plan as written) is the correct way to go.

  • Service Desk commented 2 days ago

    @zaakiy Understood — we'll stick with align-items-center on .sticky-tabs-row as in the plan (mock-up 3). The sticky docked more-info stays vertically centered with the tab labels; no stretch / right-rail layout.

  • Service Desk commented 1 day ago

    @zaakiy Removed option 4 (align-items-stretch / right-rail) from the earlier mock-ups and renumbered the remaining states. Sticky state stays with align-items-center as confirmed.

  • Zak Siddiqui commented 54 minutes ago

    the markdown for the modified comment has been corrupted. Please create a new comment with the text-based mock-up diagrams of what the various states would look like visually. Place them in code blocks so they look neat.

  • Service Desk commented 53 minutes ago

    @zaakiy Here are the text mock-ups of the main visual states for the sticky-tabs + more-info docking plan (refreshed after the earlier comment markdown corruption). Legend: [···] = more-info (ellipsis).

    1. Default / unscrolled (unchanged)

    Header more-info stays in .card-header. Docked more-info exists in the DOM but is hidden (display: none), so the tabs row looks as it does today.

    ┌──────────────────────────────────────────────────────────────┐
    │  Issues · #1234 · Title of the issue…                  [···] │  ← card-header
    ├──────────────────────────────────────────────────────────────┤
    │  [operations / primary content…]                             │
    │                                                              │
    │  Activities   Commits   Builds   Files                       │  ← tabs (not sticky yet)
    │  ─────────────────────────────────────────                   │
    │  Tab content…                                                │
    │  …                                                           │
    └──────────────────────────────────────────────────────────────┘
    

    2. Today after scroll (problem)

    Tabs stick; header (and its more-info) scroll away. Side info is unreachable without scrolling back up.

    ┌──────────────────────────────────────────────────────────────┐
    │  Activities   Commits   Builds   Files                       │  ← sticky tabs
    │  ─────────────────────────────────────────                   │
    │  Tab content…                                                │
    │  …                                                           │
    │  … more scrolled content …                                   │
    │                                              (no [···] here) │
    └──────────────────────────────────────────────────────────────┘
    

    3. Proposed sticky / docked — align-items-center

    When the header trigger leaves the scrollport, .sticky-tabs-row gets .is-docked and the docked more-info appears on the right of the sticky row. With align-items-center, the control is vertically centered with the tab labels.

    ┌──────────────────────────────────────────────────────────────┐
    │  Activities   Commits   Builds   Files                 [···] │  ← sticky row
    │  ─────────────────────────────────────────                   │     dock sits mid-row
    │  Tab content…                                                │
    │  …                                                           │
    └──────────────────────────────────────────────────────────────┘
             ↑ tabs grow left                    ↑ dock (centered)
    

    4. Side info open

    Existing rule body.side-info-visible a.side-info { display: none } hides both the header and docked triggers. Pane takes the right side.

    ┌────────────────────────────────────────────┬─────────────────┐
    │  Activities   Commits   Builds   Files     │  Side info      │
    │  ─────────────────────────────────────     │  ─────────      │
    │  Tab content…                              │  fields /       │
    │  …                                         │  workspaces /   │
    │                                            │  …              │
    │                           (no [···])       │            [×]  │
    └────────────────────────────────────────────┴─────────────────┘
    

    5. Transition summary

    unscrolled          scroll past header         side info open
    ───────────         ──────────────────         ──────────────
    header [···] shown  header [···] off-screen    all a.side-info hidden
    dock hidden         dock shown in sticky row   pane visible
    

    Sticky state uses the centered dock in (3) with align-items-center on .sticky-tabs-row.

1/1
Type
Improvement
Priority
Normal
Assignee
Labels
No labels
Issue Votes (0)
Watchers (3)
Reference
OD-2969
Please wait...
Connection lost or session expired, reload to recover
Page is in error, reload to recover