Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
955a23a
Pin the header and title bar to the viewport while a wide table scrolls
Fivell Oct 14, 2026
fa32ac5
Use max-width instead of min() so the sticky bar compiles under sassc
Fivell Oct 14, 2026
34a9a9b
Sticky index-table headers via CSS position:sticky
Fivell Oct 14, 2026
8e295d3
Extract the viewport-pinned-bar mixin shared by #header and #title_bar
Fivell Oct 14, 2026
7c3500a
Own the title-bar h2 size, action-item spacing and delete-link in the…
Fivell Oct 14, 2026
3a2ba33
fix: let the pinned title bar contain its content
Fivell Oct 14, 2026
cb048c9
refactor: drop dead title-bar declarations
Fivell Oct 14, 2026
ecd13eb
feat: promote the title-bar height into the $skin* contract
Fivell Oct 14, 2026
85db2d1
fix: keep long titles wrapping in print
Fivell Oct 14, 2026
4fd29db
fix: pin the flash bar to the viewport like its neighbours
Fivell Oct 14, 2026
34cad60
docs: document the reachable sticky-header opt-outs
Fivell Oct 14, 2026
ff15869
fix: square sticky thead corners so rows don't show through
Fivell Oct 14, 2026
47f3138
fix: pin only the first thead row
Fivell Oct 14, 2026
af2d5f7
fix: keep the scrollbar-width observer alive past Turbo swaps
Fivell Oct 14, 2026
63b47ae
perf: skip same-value writes of --scrollbar-width
Fivell Oct 14, 2026
fe39c45
docs: record why the custom property is bare --scrollbar-width
Fivell Oct 14, 2026
c315e4c
fix(dummy): build the demo data as locals, not top-level constants
Fivell Oct 14, 2026
8fa58be
fix(dummy): escape header cells like the body cells
Fivell Oct 14, 2026
45785c0
test: exercise the sticky-thead selector against a real element
Fivell Oct 14, 2026
566473a
docs: document scrollbar_width.js and the sticky-header classes
Fivell Oct 14, 2026
0408555
docs: drop a consumer-internal file name from a comment
Fivell Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/assets/javascripts/wigu/scrollbar_width.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Expose the vertical scrollbar width as a CSS custom property
// (--scrollbar-width) on <html>. The sticky #header and #title_bar are pinned to
// the viewport's left edge and sized with 100vw, but 100vw INCLUDES the vertical
// scrollbar gutter — so when a page has a vertical scrollbar the bars are ~15px
// wider than the visible viewport and, clamped inside their full-content-width
// containing block, get pushed left by that amount at the far-right horizontal
// scroll position. Subtracting this variable (0 when there is no scrollbar) sizes
// them to the visible viewport instead, so they stay put. Optional: the CSS falls
// back to `var(--scrollbar-width, 0px)` = plain 100vw when this script is absent.
Comment on lines +8 to +9
(function () {
function update() {
var w = window.innerWidth - document.documentElement.clientWidth;
document.documentElement.style.setProperty("--scrollbar-width", (w > 0 ? w : 0) + "px");
}
window.addEventListener("resize", update);
document.addEventListener("DOMContentLoaded", function () {
update();
// The presence of a vertical scrollbar can change after load (content grows/
// shrinks) without firing resize — watch the body's box too.
if (window.ResizeObserver) { new ResizeObserver(update).observe(document.body); }
});
update();
})();
62 changes: 60 additions & 2 deletions app/assets/stylesheets/wigu/active_admin_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,14 @@ body.active_admin {
// wrapped menu.
position: sticky;
left: 0;
width: 100vw;
// Viewport width minus the vertical scrollbar (JS-set --scrollbar-width var,
// 0 when absent — load wigu/scrollbar_width), capped to the content width.
// Plain 100vw includes the scrollbar gutter, which shifts this left-pinned
// bar at the far-right scroll position; subtracting it keeps it put. The cap
// is a plain max-width, not min(…, 100%): sassc/libsass (sprockets consumers)
// evaluates the Sass min() function and rejects calc() as a non-number.
width: calc(100vw - var(--scrollbar-width, 0px));
max-width: 100%;
box-sizing: border-box;
h1.site_title {
color: $skinMainSecondColor;
Expand Down Expand Up @@ -657,11 +664,39 @@ body.active_admin {
background-color: $skinTitleBarColor;
border: none;
border-bottom: $skinTitleBarBorderWidth solid $skinTitleBarBorderColor;
// Pin the whole bar to the viewport like #header, so the title AND the
// action buttons stay visible while a wide index table scrolls the page
// sideways. width = the viewport (minus the vertical scrollbar via the
// JS-set --scrollbar-width var, 0 when absent — load wigu/scrollbar_width to
// populate it), capped to the content width with a plain max-width so 100vw's
// scrollbar allowance can't add a stray horizontal scrollbar. No `top`, so it
// still scrolls vertically. The cap is max-width, not min(…, 100%): sassc/
// libsass (sprockets consumers) evaluates the Sass min() and rejects calc().
position: sticky;
left: 0;
width: calc(100vw - var(--scrollbar-width, 0px));
max-width: 100%;
box-sizing: border-box;
// Lay the title (left) and action items (right) on one compact row. Without
// this, resetting #titlebar_right to static (below) stacks it under the
// title and the bar grows tall. Fixed height so pages with a breadcrumb
// (detail pages) match plain index pages. Drop the base clearfix — in a flex
// container it becomes a stray third item that space-between strands right.
display: flex;
align-items: center;
justify-content: space-between;
height: 37px;
&::after { display: none; }
#titlebar_left {
display: flex;
align-items: center;
gap: 6px;
height: auto;
span.breadcrumb {
font-size: 12px;
text-shadow: none;
margin-bottom: 8px;
float: none;
margin-bottom: 0;
a, span {
color: lighten( $skinMainFirstColor, 30% )!important; ;
}
Expand All @@ -672,6 +707,16 @@ body.active_admin {
}
}
#titlebar_right {
// Horizontal pinning is handled by #title_bar (sticky + viewport width),
// so the action items just sit at the right of the bar. Reset any
// consumer `position: absolute/sticky` (each app used to pin the buttons
// itself via a local scroll_buttons_init.scss / title_bar override).
position: static;
right: auto;
top: auto;
// Shrink to the action-items height (some apps/base leave it ~50px, taller
// than the 37px bar, so centering pushed it over the top edge).
height: auto;
// `> a` and `> .dropdown_menu > a`: an action item may hold a dropdown, and
// a descendant selector here paints its menu entries with the button fill
// too — with !important, so the entry rules below cannot undo it.
Expand Down Expand Up @@ -1307,6 +1352,19 @@ table.index_table thead th a {
color: var(--aa-table-header-text) !important;
}

// Sticky table headers: keep the column labels visible while a long index table
// scrolls vertically. The header/title bar pin only horizontally (no `top`), so
// they scroll away on vertical scroll and this thead pins to the viewport top —
// no overlap. Scoped to .paginated_collection so it only hits real AA index
// listings, not custom report/dashboard tables. Opt a listing out with
// .skip-sticky-headers; opt a non-index table_for in with .with-sticky-headers.
.paginated_collection table.index_table:not(.skip-sticky-headers) thead tr th,
table.with-sticky-headers thead tr th {
position: sticky;
top: -1px;
z-index: 98;
}

// Form section header: a <legend> interrupts the fieldset's top border, so the
// header bar has no border around it (unlike a panel header). Float the legend
// so it flows as a normal block and the fieldset border wraps around it.
Expand Down
Binary file added img/titlebar-scroll-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/titlebar-scroll-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions test/dummy/app/admin/wide_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Demo page with a table wider than the viewport, so the page scrolls
# horizontally — used to show the header + title bar (title on the left, the
# action button on the right) staying pinned while the table scrolls under them.
ActiveAdmin.register_page "Wide table" do
menu priority: 4

action_item(:new_record) { link_to "New record", "#" }

WIDE_COLS = (1..12).map { |i| "Column #{i}" }.freeze
WIDE_ROWS = (1..5).map { |r| (1..12).map { |c| "row #{r} / value #{c}" } }.freeze

content do
# nowrap + a min cell width so the 12 columns push the table past the
# viewport and the page scrolls sideways (what the sticky bars are for).
text_node <<~HTML.html_safe
<style>#wrapper .wide-demo td, #wrapper .wide-demo th {
white-space: nowrap; min-width: 150px;
}</style>
HTML
panel "A table wider than the viewport" do
head = "<thead><tr>" + WIDE_COLS.map { |c| "<th>#{c}</th>" }.join + "</tr></thead>"
body = WIDE_ROWS.map do |row|
"<tr>" + row.map { |cell| "<td>#{ERB::Util.html_escape(cell)}</td>" }.join + "</tr>"
end.join
text_node(
(%(<table class="index_table wide-demo">) + head + "<tbody>" + body + "</tbody></table>")
.html_safe
)
end
end
end
1 change: 1 addition & 0 deletions test/dummy/app/assets/javascripts/active_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
//= require wigu/server_clock
//= require wigu/theme_toggle
//= require wigu/table_crisscross
//= require wigu/scrollbar_width
Loading