Skip to content

feat(webkit): rework the NavigationMenu panel and fix its dead keyboard model - #943

Open
gabriel-lisboa-azion wants to merge 2 commits into
mainfrom
feat/navigation-menu-panel-treatment
Open

feat(webkit): rework the NavigationMenu panel and fix its dead keyboard model#943
gabriel-lisboa-azion wants to merge 2 commits into
mainfrom
feat/navigation-menu-panel-treatment

Conversation

@gabriel-lisboa-azion

Copy link
Copy Markdown
Collaborator

What

The NavigationMenu panel is reworked for the mega-menu shape, and two silent defects found while building it are fixed.

Panel treatment

  • The group heading is an overline with a hairline under it — not the Overline component, which painted brand primary at text-overline-md and brought its own padding. That component, not the uppercase, was what made the heading shout. The inset is a margin, not padding, and that is the trick: the words start on the entries' icon column while the painted bottom edge stops that far short of the track on each side, so adjacent columns are separated by a real break (twice the inset) instead of the 2px their transparent side borders happen to leave.
  • A group heading takes an optional href, so a section can link to its own page above the entries that lead into parts of it. It stays the same quiet row — pointer, ink lifting to --text-default, focus ring, no hover surface (a filled row here would read as one of the entries it labels) and no radius (a rounded box would propose a control that is not there). Following it closes the menu, the same close a close-on-click entry performs and for the same reason: the panel must not outlive the navigation it triggered.
  • NavigationMenuPopup takes kind="contrast", which inverts the panel rather than lightening it. Over a dark canvas the two separators an overlay leans on are inert — a backdrop cannot darken black, a shadow on black is invisible — and the fill cannot simply be raised either: entry copy is --text-muted (#808080), which holds 4.66:1 on --bg-surface-raised (#141414) and falls to 2.07:1 by ~#4D4D4D, so every fill lighter than ~#181818 is under the 4.5:1 floor. The variant redefines the tokens the panel's parts already paint from, so no sub-component knows the panel inverted. Measured: title 20.3:1, description 5.7:1.
  • Entry rhythm comes from the entry's own block padding, with no gap on top of it. Setting the vertical spacing from two unrelated numbers is what made the column read as loose — one entry's words sat further from the next than the group's rule sat from the first. Entry icons move to the system's 16px box (the same box MenuItem gives a row's glyph), and descriptions to 14px so title and description read as one two-line block rather than a footnote under it.

Fixes

The trigger's keyboard model was entirely dead.

@keydown="!isLink ? root.onTriggerKeydown : undefined"

compiles to an inline statement — Vue only treats a bare identifier or member path as a method handler — so the ternary was evaluated on every keystroke and its result thrown away. Nothing errored, and the failure was partial in the most misleading way: Escape never closed the panel and the arrows never roved between triggers, while Enter and Space kept working because a native <button> turns them into a click.

Panel measurement took the height at the wrong width. --popup-width was measured with width: auto, which is "fill the parent" — that reads as the panel's natural width only while the positioner is itself shrink-wrapped. Give the positioner a width of its own (a mega-menu laid on a page column) and auto measures every panel as that column, so they all morph to one width and none is ever measured. It is fit-content now. The height is then taken in a second pass at the width the panel will actually be laid out at: measuring it while the target is still max-content reads a wider layout, so every description that will wrap is still on one line, the height comes back short, and the popup — overflow-hidden at exactly that height — clipped its own last row. When nothing constrains the popup the two passes lay out identically and it is a no-op.

Verification

  • vitest (browser mode, Playwright Chromium) — 18/18 pass
  • The keyboard fix ships with tests that prove it. The suite had zero keyboard coverage, so the two new cases (Escape closes, ArrowRight roves and opens) were run against the old dead-ternary form first and both fail there; they pass with the handler. Without that step this fix would have merged unguarded.
  • vue-tsc --noEmit clean
  • eslint --max-warnings=0 clean
  • prettier --check clean
  • check-authoring.mjs — 0 new violations
  • catalog.json regenerated — no diff (the root's public API is unchanged; kind/href are sub-component props)

Contrast ratios and the panel geometry are asserted by measurement, not by the unit suite — that env renders without Tailwind, so a computed-style assertion there would pass in both the broken and fixed states.

…rd model

The panel is reworked for the mega-menu shape, and two silent defects
found while building it are fixed.

Panel treatment:

- The group heading is an overline with a hairline under it, not the
  `Overline` COMPONENT — which painted brand primary at
  `text-overline-md` and brought its own padding, and was what made the
  heading shout. The inset is a margin, not padding, so the words start
  on the entries' icon column while the painted bottom edge stops short
  of the track on each side; adjacent columns are then separated by a
  real break instead of the 2px their transparent side borders leave.
- A group heading takes an optional `href`, so a section can link to its
  own page above the entries that lead into parts of it. Following it
  closes the menu, the same close a `close-on-click` entry performs: the
  panel must not outlive the navigation it triggered.
- `NavigationMenuPopup` takes `kind="contrast"`, which INVERTS the panel
  rather than lightening it. Over a dark canvas a backdrop cannot darken
  black and a shadow on black is invisible, and the fill cannot simply
  be raised: entry copy is `--text-muted` (#808080), which holds 4.66:1
  on `--bg-surface-raised` and falls under the 4.5:1 floor by ~#181818.
  The variant redefines the tokens the panel's parts already paint from,
  so no sub-component knows the panel inverted.
- Entry rhythm comes from the entry's own block padding, with no gap on
  top of it — setting the vertical spacing from two unrelated numbers is
  what made the column read as loose. Entry icons move to the system's
  16px box, and descriptions to 14px so the pair reads as one two-line
  block instead of a footnote.

Fixes:

- The trigger's keyboard model was entirely dead.
  `@keydown="!isLink ? root.onTriggerKeydown : undefined"` compiles to an
  inline statement — Vue only treats a bare identifier or member path as
  a method handler — so the ternary was evaluated per keystroke and its
  result discarded. Nothing errored: `Escape` never closed the panel and
  the arrows never roved, while `Enter`/`Space` kept working because a
  native button turns them into a click. Now a real handler, guarded by
  two tests that fail against the old form.
- Panel measurement took the height at the wrong width. `--popup-width`
  was measured with `width: auto`, which is "fill the parent" and only
  reads as the panel's natural width while the positioner is
  shrink-wrapped; give the positioner a width of its own (a mega-menu on
  a page column) and every panel measures as that column. It is
  `fit-content` now, and the height is taken in a second pass at the
  width the panel is actually laid out at — measuring it while the
  target is still `max-content` reads a wider layout, so every
  description that will wrap is still on one line and the popup, which
  is `overflow-hidden` at exactly that height, clipped its own last row.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants