mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-22 21:22:04 +00:00
Fix scrollbar gutter
This commit is contained in:
1 parent
c29da67de3
commit
562b859174
3 files changed
+115
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user