ui: the navbar shows which section you are in (#649)

* ui: the navbar shows which section you are in

Clicking Videos opened the videos page and left the bar looking untouched.

react-router was doing its part the whole time -- the tab carried `active` and
`aria-current="page"` on every one of those pages.  Nothing in the stylesheet
drew that class, so all eleven tabs rendered identically and the mark was
visible only to a screen reader.

The current tab now takes a deepened version of the hover wash plus an
underline.  The underline is a pseudo-element in `currentColor` rather than a
border, for two reasons: `currentColor` is the foreground the bar already
computed for whichever of the fourteen navbar colors the user picked, so one
rule covers every color in every theme without naming one; and a pseudo-element
adds no height, where a border would jog the whole row as the mark moved
between tabs.  The overflow "More" menu and the mobile hamburger mark their
copy of the tab too -- on a phone that menu is where most tabs live.

Calculators and Statistics also lose `end: true`.  A tab is meant to stay
current for the pages beneath it, and `end` restricts the match to the exact
path, so either one would have dropped its mark the moment it grew a sub-page.
The other nine links never had it, which is why /videos/1234 already kept
Videos marked.

The test renders the real bar, which needed two pieces of browser jsdom does
not have: fresnel renders no navbar at all unless a breakpoint query matches,
and every width is 0, which convinces the overflow logic that nothing fits and
sweeps all eleven tabs into the closed "More" menu -- where every assertion
about them would have passed vacuously.  Both are stubbed, and the styling
itself is asserted against the stylesheet, since jsdom applies none.

* review: the mark follows the tab into the overflow menu

Six review points, no bugs among them.  The substantive one: when the window
narrows enough to fold the current tab into "More", the bar went blank again --
the trigger is a plain button, not a NavLink, so nothing was marked until the
user opened the menu to look, which is exactly when they least need telling.
"More" now takes the mark when it holds the current section.

That needs the matching rule outside a NavLink, where the hooks that answer it
cannot be called in a loop, so `isLinkActive` states it once: a link owns the
pages beneath it unless it asks for an exact match.  The trigger gets the class
only -- the real NavLink inside the menu already carries `aria-current`, and a
second one on the button that opens the menu announces the same page twice.

The other five, all fair:

  * The mobile hamburger and the overflow menu had no test at all, so
    `.mantine-Menu-item.active` -- the whole of the mark on a phone -- was
    never exercised.  Both now are, which meant splitting the viewport and the
    bar width out of the fixture so a test can ask for a narrow bar or a phone.
    The mobile case also needs the real getBoundingClientRect back: floating-ui
    positions the menu's portal and crashes the worker on a stub DOMRect.
  * The CSS guard matched the selector as a substring, which still passed with
    the rule body emptied or the underline deleted -- and the underline is the
    half that survives on a dark bar.  It now reads the declarations.
  * The table cases used `toContain`, which a regression leaving Home
    permanently marked would satisfy on every row.  `toEqual([text])`.
  * A comment claimed the tab "only matched its own exact path", which is the
    root cause the PR description explicitly rules out -- Videos never carried
    `end`.  Corrected to what those cases actually guard.
  * The navbar offers twelve colors, not fourteen.  I had carried over
    Semantic's palette count, which includes black and white.

Writing the tightened height assertion I reached for `(?!0)`, which calls
0.1875rem zero.  It reads the number instead.
This commit is contained in:
lrnselfreliance 2026-08-08 17:10:53 -06:00 • committed by Roland
parent 499ef435f8
commit eaa8b83e54
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
3 changed files with 309 additions and 4 deletions

View file

@ -67,6 +67,46 @@ html .mantine-Notifications-root[data-position^='top'] {
background: rgba(0, 0, 0, 0.12);
}
/*
* The tab for the section the user is in.
*
* react-router marks it with `active` and `aria-current` on its own, and always did -- the
* mark reached the DOM and nothing here drew it, so every tab looked identical and clicking
* Videos appeared to leave the bar untouched.
*
* The underline is a pseudo-element in `currentColor`, not a border: `currentColor` follows
* the foreground the bar computed for whatever navbar color the user picked, so this works
* on all twelve of them (see `navColorNames`) and in all four themes without naming a color;
* and a pseudo-element adds no height, where a border would jog the whole row as the mark
* moved between tabs. The darker wash is the hover treatment deepened, so the two read as
* the same idea.
*/
.wrolpi-navbar-link.active {
position: relative;
background: rgba(0, 0, 0, 0.2);
}
.wrolpi-navbar-link.active::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
/* rem, not px, so the mark grows with the --ui-scale setting like the text above it. */
height: 0.1875rem;
background: currentColor;
}
/*
* The same tabs inside the overflow "More" menu and the mobile hamburger, which are the
* only place most of them appear on a phone. Those render in a portal, outside the bar,
* so they take theme tokens rather than the bar's foreground.
*/
.mantine-Menu-item.active {
background: var(--head);
font-weight: 600;
}
/*
* Everything in the bar takes the bar's foreground.
*