Fix scrollbar gutter

This commit is contained in:
Jonathon Leight committed 2026-08-09 18:05:59 -04:00
1 parent c29da67de3
commit 562b859174
3 files changed
+115

No files matched your search

+10
View File
@@ -121,6 +121,16 @@ markup for it. The one that has already bitten us:
this; it parses the templates structurally, so extend it there if you find
another flex-container component with the same trap.
- **Tabler shifts the whole page sideways when a scrollbar appears.** It ships
`@media (min-width:992px) { :root { margin-left: calc(100vw - 100%) } }`; `100vw`
counts the scrollbar and `100%` doesn't, so any page tall enough to scroll slides
right by the scrollbar width with an unpainted strip down the left edge. It reads
as page-specific (the tall tabs of a section show it, the short ones don't), but
it's every scrollable page. `app.css` overrides it and reserves the space with
`scrollbar-gutter: stable` instead; `TestE2ENoLeftGutterWhenScrollbarAppears`
locks it in. Override in `app.css` rather than editing `tabler.min.css` — that
file's hash is pinned by `internal/licenses`.
- **A `<button>` centers its text.** Invisible on a normal button, wrong the
moment you use one as a full-width row (`d-flex w-100`, e.g. a `list-group-item`
that expands a collapse): the label sits centered while every neighboring row is
+79
View File
@@ -0,0 +1,79 @@
//go:build browser
package e2e
import (
"crypto/rand"
"fmt"
"testing"
"github.com/chromedp/chromedp"
meshcore "github.com/meshcore-go/meshcore-go"
"github.com/MeshTender/MeshTender/internal/store"
)
// TestE2ENoLeftGutterWhenScrollbarAppears: a page long enough to scroll must still
// start at the left edge of the window.
//
// Tabler ships `@media (min-width:992px) { :root { margin-left: calc(100vw - 100%) } }`
// — 100vw counts the scrollbar and 100% doesn't, so any scrollable page gets a left
// margin the width of the scrollbar, sliding the whole layout (navbar included) right
// and leaving an unpainted strip down the left edge. app.css overrides it and reserves
// the space with scrollbar-gutter instead.
//
// It has to be a browser test: it only reproduces at a viewport wide enough for the
// media query AND tall enough content to produce a real scrollbar, which is also why
// it looked page-specific — the short tabs of the same section were fine.
func TestE2ENoLeftGutterWhenScrollbarAppears(t *testing.T) {
srv := newE2EServer(t)
owner, cookie := srv.login(t, "gutteruser")
org, err := srv.store.CreateOrg(srv.ctx, "Gutter Org", owner.ID)
if err != nil {
t.Fatalf("create org: %v", err)
}
// Enough rows that the list is taller than the window.
for i := 0; i < 30; i++ {
id, err := meshcore.GenerateLocalIdentity(rand.Reader)
if err != nil {
t.Fatalf("generate identity: %v", err)
}
if _, err := srv.store.CreateRepeater(srv.ctx, &store.Repeater{
OwnerID: owner.ID, Name: fmt.Sprintf("Rep %02d", i), PublicKeyHex: id.String(),
RadioFreqHz: 869525000, RadioBwHz: 250000, RadioSF: 11, RadioCR: 5,
}); err != nil {
t.Fatalf("create repeater: %v", err)
}
}
bctx, cancel, watch := startBrowser(t)
defer cancel()
var scrollbar, navLeft int
var htmlMargin string
if err := chromedp.Run(bctx,
// Above the 992px breakpoint the Tabler rule applies; below it, it doesn't.
chromedp.EmulateViewport(1280, 700),
setSessionCookie(cookie),
chromedp.Navigate(srv.appURL+"/orgs/"+org.Slug+"/repeaters"),
chromedp.WaitVisible(`#rep-list`, chromedp.ByQuery),
chromedp.Evaluate(`window.innerWidth - document.documentElement.clientWidth`, &scrollbar),
chromedp.Evaluate(`getComputedStyle(document.documentElement).marginLeft`, &htmlMargin),
chromedp.Evaluate(`Math.round(document.querySelector('header.navbar').getBoundingClientRect().left)`, &navLeft),
); err != nil {
t.Fatalf("browser run: %v", err)
}
watch.assertClean(t)
// Guard the guard: with no scrollbar the margin is 0 anyway and this proves nothing.
if scrollbar == 0 {
t.Skip("no classic scrollbar in this browser, so the rule cannot misfire")
}
if htmlMargin != "0px" {
t.Errorf("html margin-left = %s with a %dpx scrollbar; the page is pushed off the left edge",
htmlMargin, scrollbar)
}
if navLeft != 0 {
t.Errorf("navbar starts at x=%d, want 0 — there is a gutter down the left of the page", navLeft)
}
}
+26
View File
@@ -239,3 +239,29 @@ code.pubkey {
.footer-centered .row {
justify-content: center;
}
/* Undo Tabler's scrollbar compensation, which shifts the whole page sideways.
@media (min-width:992px) it sets `:root { margin-left: calc(100vw - 100%) }` —
100vw includes the scrollbar, 100% doesn't, so a page long enough to scroll gets a
left margin the width of the scrollbar. The intent is to stop layout shifting when
a scrollbar appears, but the result is that every scrollable page sits ~15px right
of the window with an unpainted strip down the left edge, navbar included (it was
visible on the org Repeaters and Configuration tabs, and absent on the shorter Home
and Members tabs, which is what made it look page-specific).
scrollbar-gutter reserves the space on the correct side instead: no shift when the
scrollbar appears, no gutter on the left. Where it isn't supported the declaration
is ignored, which lands on the plain browser behavior rather than back on the
sideways shift. Overlay scrollbars (macOS, mobile) take no space at all, so the
gutter is a no-op there.
Overriding here rather than editing tabler.min.css keeps that file byte-identical
to the audited upstream artifact (internal/licenses pins its hash). */
:root {
scrollbar-gutter: stable;
}
@media (min-width: 992px) {
:root {
margin-left: 0;
}
}