Skip to content

Commit 955a23a

Browse files
committed
Pin the header and title bar to the viewport while a wide table scrolls
On a wide index table the whole page scrolls horizontally. Pin #header and #title_bar to the viewport's left edge and cap them to the viewport width, so the logo, nav, page title AND action buttons all stay visible while the table scrolls underneath (no `top`, so they still scroll vertically). #titlebar_right resets to static — the bar does the pinning now, not the buttons. Width is min(100vw - var(--scrollbar-width), 100%): plain 100vw includes the vertical scrollbar gutter and shifts the left-pinned bars at the far-right scroll position. The optional wigu/scrollbar_width.js sets --scrollbar-width; without it the var falls back to 0 (plain 100vw). Consuming apps each solved this with their own CSS (scroll_buttons_init.scss, title_bar overrides) and JS — this owns it for all of them.
1 parent e93a418 commit 955a23a

6 files changed

Lines changed: 98 additions & 2 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Expose the vertical scrollbar width as a CSS custom property
2+
// (--scrollbar-width) on <html>. The sticky #header and #title_bar are pinned to
3+
// the viewport's left edge and sized with 100vw, but 100vw INCLUDES the vertical
4+
// scrollbar gutter — so when a page has a vertical scrollbar the bars are ~15px
5+
// wider than the visible viewport and, clamped inside their full-content-width
6+
// containing block, get pushed left by that amount at the far-right horizontal
7+
// scroll position. Subtracting this variable (0 when there is no scrollbar) sizes
8+
// them to the visible viewport instead, so they stay put. Optional: the CSS falls
9+
// back to `var(--scrollbar-width, 0px)` = plain 100vw when this script is absent.
10+
(function () {
11+
function update() {
12+
var w = window.innerWidth - document.documentElement.clientWidth;
13+
document.documentElement.style.setProperty("--scrollbar-width", (w > 0 ? w : 0) + "px");
14+
}
15+
window.addEventListener("resize", update);
16+
document.addEventListener("DOMContentLoaded", function () {
17+
update();
18+
// The presence of a vertical scrollbar can change after load (content grows/
19+
// shrinks) without firing resize — watch the body's box too.
20+
if (window.ResizeObserver) { new ResizeObserver(update).observe(document.body); }
21+
});
22+
update();
23+
})();

‎app/assets/stylesheets/wigu/active_admin_theme.scss‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,12 @@ body.active_admin {
463463
// wrapped menu.
464464
position: sticky;
465465
left: 0;
466-
width: 100vw;
466+
// Viewport width minus the vertical scrollbar (JS-set --scrollbar-width var,
467+
// 0 when absent — load wigu/scrollbar_width), capped to the content width.
468+
// Plain 100vw includes the scrollbar gutter, which shifts this left-pinned
469+
// bar at the far-right scroll position; subtracting it keeps it put. unquote
470+
// so Sass emits the min() literally (it can't mix % + vw).
471+
width: unquote("min(calc(100vw - var(--scrollbar-width, 0px)), 100%)");
467472
box-sizing: border-box;
468473
h1.site_title {
469474
color: $skinMainSecondColor;
@@ -657,11 +662,37 @@ body.active_admin {
657662
background-color: $skinTitleBarColor;
658663
border: none;
659664
border-bottom: $skinTitleBarBorderWidth solid $skinTitleBarBorderColor;
665+
// Pin the whole bar to the viewport like #header, so the title AND the
666+
// action buttons stay visible while a wide index table scrolls the page
667+
// sideways. width = the viewport (minus the vertical scrollbar via the
668+
// JS-set --scrollbar-width var, 0 when absent — load wigu/scrollbar_width to
669+
// populate it), capped to the content width so 100vw's scrollbar allowance
670+
// can't add a stray horizontal scrollbar. No `top`, so it still scrolls
671+
// vertically. unquote so Sass emits the min() literally (it can't mix % + vw).
672+
position: sticky;
673+
left: 0;
674+
width: unquote("min(calc(100vw - var(--scrollbar-width, 0px)), 100%)");
675+
box-sizing: border-box;
676+
// Lay the title (left) and action items (right) on one compact row. Without
677+
// this, resetting #titlebar_right to static (below) stacks it under the
678+
// title and the bar grows tall. Fixed height so pages with a breadcrumb
679+
// (detail pages) match plain index pages. Drop the base clearfix — in a flex
680+
// container it becomes a stray third item that space-between strands right.
681+
display: flex;
682+
align-items: center;
683+
justify-content: space-between;
684+
height: 37px;
685+
&::after { display: none; }
660686
#titlebar_left {
687+
display: flex;
688+
align-items: center;
689+
gap: 6px;
690+
height: auto;
661691
span.breadcrumb {
662692
font-size: 12px;
663693
text-shadow: none;
664-
margin-bottom: 8px;
694+
float: none;
695+
margin-bottom: 0;
665696
a, span {
666697
color: lighten( $skinMainFirstColor, 30% )!important; ;
667698
}
@@ -672,6 +703,16 @@ body.active_admin {
672703
}
673704
}
674705
#titlebar_right {
706+
// Horizontal pinning is handled by #title_bar (sticky + viewport width),
707+
// so the action items just sit at the right of the bar. Reset any
708+
// consumer `position: absolute/sticky` (each app used to pin the buttons
709+
// itself via a local scroll_buttons_init.scss / title_bar override).
710+
position: static;
711+
right: auto;
712+
top: auto;
713+
// Shrink to the action-items height (some apps/base leave it ~50px, taller
714+
// than the 37px bar, so centering pushed it over the top edge).
715+
height: auto;
675716
// `> a` and `> .dropdown_menu > a`: an action item may hold a dropdown, and
676717
// a descendant selector here paints its menu entries with the button fill
677718
// too — with !important, so the entry rules below cannot undo it.

‎img/titlebar-scroll-after.png‎

29.8 KB
Loading

‎img/titlebar-scroll-before.png‎

25.9 KB
Loading

‎test/dummy/app/admin/wide_table.rb‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Demo page with a table wider than the viewport, so the page scrolls
2+
# horizontally — used to show the header + title bar (title on the left, the
3+
# action button on the right) staying pinned while the table scrolls under them.
4+
ActiveAdmin.register_page "Wide table" do
5+
menu priority: 4
6+
7+
action_item(:new_record) { link_to "New record", "#" }
8+
9+
WIDE_COLS = (1..12).map { |i| "Column #{i}" }.freeze
10+
WIDE_ROWS = (1..5).map { |r| (1..12).map { |c| "row #{r} / value #{c}" } }.freeze
11+
12+
content do
13+
# nowrap + a min cell width so the 12 columns push the table past the
14+
# viewport and the page scrolls sideways (what the sticky bars are for).
15+
text_node <<~HTML.html_safe
16+
<style>#wrapper .wide-demo td, #wrapper .wide-demo th {
17+
white-space: nowrap; min-width: 150px;
18+
}</style>
19+
HTML
20+
panel "A table wider than the viewport" do
21+
head = "<thead><tr>" + WIDE_COLS.map { |c| "<th>#{c}</th>" }.join + "</tr></thead>"
22+
body = WIDE_ROWS.map do |row|
23+
"<tr>" + row.map { |cell| "<td>#{ERB::Util.html_escape(cell)}</td>" }.join + "</tr>"
24+
end.join
25+
text_node(
26+
(%(<table class="index_table wide-demo">) + head + "<tbody>" + body + "</tbody></table>")
27+
.html_safe
28+
)
29+
end
30+
end
31+
end

‎test/dummy/app/assets/javascripts/active_admin.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
//= require wigu/server_clock
44
//= require wigu/theme_toggle
55
//= require wigu/table_crisscross
6+
//= require wigu/scrollbar_width

0 commit comments

Comments
 (0)