Skip to content

feat(webkit): give Menu rows an arrow control split by href, and make Back a button - #952

Merged
isaque-bock-azion merged 5 commits into
mainfrom
feat/menu-split-trigger-and-back-button
Sep 8, 2026
Merged

isaque-bock-azion merged 5 commits into
mainfrom
feat/menu-split-trigger-and-back-button

Conversation

@gabriel-lisboa-azion

@gabriel-lisboa-azion gabriel-lisboa-azion commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A row that owns children now always carries a trailing transparent IconButton arrow — the affordance that says the row owns children. href decides what the label beside it does: with one, the label is a real <a> and the arrow alone reveals the children; without one, the label reveals them too, so the whole row does.
  • aria-expanded / aria-controls sit on whichever control expands the children and on exactly one of them; when the label owns them the arrow becomes a redundant pointer target (tabindex="-1" + aria-hidden), adding neither a second tab stop nor a second announced control.
  • Menu.Back is a compact button instead of a full-width row with a centred label, and its text names where it lands (Back, Back to Settings, or label) rather than the level the reader is already in.

How to test

  1. pnpm webkit:test — 2216 pass, including the menu suite's axe checks.
  2. pnpm storybook:dev → Components / Navigation / Menu / Types. Neither row has an href, so each reveals its children from the whole row while still showing the arrow. Tab: one stop per row.
  3. Add href="/anything" to a SubTrigger in that story: the label becomes a real link (middle-click opens a tab), the arrow alone reveals, and aria-expanded moves from the label to the arrow.
  4. Menu / Drill — activate the row to push its level; Back appears as a compact left-aligned button reading Back. Push a second level: Back to <parent>. Escape / ArrowLeft / clicking Back pops and returns focus to whichever control pushed.
  5. Menu / Disabled — the disabled trigger suppresses every control of the row, and its arrow paints no fill (IconButton's filled disabled box would otherwise make the arrow the brightest thing on a row whose label had dimmed away).

Notes

  • Not marked breaking: no prop, event, slot or export was removed or renamed, and a row with no href behaves as it did before this PR. The trigger's root is now always a div holding its controls, so a consumer whose tests or CSS target the trigger root as the interactive element should point at …__control / …__reference / …__arrow.
  • New prop: SubTrigger.href (default ''). In data-driven mode it comes from MenuNode.href, which already existed — so the behaviour falls out of the tree with no new concept for consumers, and it follows root-element.md (the element switches on a data prop, never an as string).
  • IconButton now forwards $attrs to its root, per .claude/rules/root-element.md. It declared inheritAttrs: false and never spread them, so every consumer attribute was silently swallowed — that is why the arrow could not carry aria-expanded. class and data-testid stay excluded (already applied explicitly).
  • The unconditional split was wrong on a phone, measured in the sample's docs sheet: the label was 320×32 and navigated away and dismissed the sheet, while the only control that expanded was 28×28 — 7% of the row's area, under the 44px touch guideline.

@gabriel-lisboa-azion
gabriel-lisboa-azion requested a review from a team as a code owner August 31, 2026 22:02
@gabriel-lisboa-azion
gabriel-lisboa-azion force-pushed the feat/menu-split-trigger-and-back-button branch from 3a66975 to c544773 Compare August 31, 2026 22:11
… and make Back a button

A row that owns children is a destination as well as a container, so it is now
two controls: a label that references wherever the row points, and a trailing
transparent IconButton that reveals its children. Condensed rows expand in
place, drill rows push a level. Reaching the children no longer costs the reader
the destination, and each control has its own surface, focus ring and hit area.

aria-expanded and aria-controls move to the arrow, the control that expands the
children. The arrow keys still work from the label, and focus returns to the
arrow when a drill level is popped.

Menu.Back is a compact button rather than another full-width row, so the one
control that leaves a level does not read as one of the rows inside it, and its
text names where it lands rather than the level the reader is already in.

IconButton now forwards $attrs to its root, per .claude/rules/root-element.md.
It set inheritAttrs: false and never spread them, so every consumer attribute
was silently swallowed -- which is why the arrow could not carry aria-expanded.
@gabriel-lisboa-azion
gabriel-lisboa-azion force-pushed the feat/menu-split-trigger-and-back-button branch from c544773 to df6a6d1 Compare August 31, 2026 22:26
… arrow on every row

A row that owns children now always renders its arrow as a transparent IconButton
-- that arrow IS the affordance saying the row owns children, so it must not change
shape according to whether the row also has a destination. What `href` decides is
what the LABEL beside it does, and therefore how much of the row reveals the
children:

- with an `href`, the label is an <a> to that destination and the arrow alone
  reveals the children, so reaching them does not cost the reader the destination;
- without one, the label reveals them too, so the whole row does.

The unconditional split was wrong on a phone, measured in the sample's docs sheet:
the label was 320x32 and navigated away AND dismissed the sheet, while the only
control that expanded was 28x28 -- 7% of the row's area, under the 44px touch
guideline. Tapping a chevron row to look inside it therefore threw the reader onto
a page and closed the menu. Every one of the 28 container rows in that tree carries
no href (a container is not a destination there), so all 28 now reveal from their
full width.

aria-expanded / aria-controls sit on whichever control expands the children and on
exactly one of them: the arrow when the label is a link, the label otherwise -- in
which case the arrow becomes a redundant pointer target (tabindex="-1" +
aria-hidden), so it adds neither a second tab stop nor a second announced control.
Focus after a pop returns to whichever control pushed. A disabled row drops its
`href` outright, since an anchor with no destination is not a link.
@gabriel-lisboa-azion gabriel-lisboa-azion changed the title feat(webkit): split a Menu sub-trigger into a reference and an arrow, and make Back a button feat(webkit): give Menu rows an arrow control split by href, and make Back a button Sep 1, 2026
gabriel-lisboa-azion added a commit that referenced this pull request Sep 1, 2026
… Back button

Merges feat/menu-split-trigger-and-back-button, which carries main up to 11d2bd5
(9 commits) plus the two of the PR. Resolutions, none of them mechanical:

- packages/theme/src/tokens/semantic/texts.data.js — hand-merged. The demo side is a
  superset inside the conflict hunk (its own text-amount-* ladder, textWrapStyle on the
  heading tokens) and already carried main's regular-weight change there, so the hunk
  took ours; text-body-prose-md sat OUTSIDE the hunk, where only main had changed it
  light -> normal, so the auto-merge kept main's value. Canaries: 34 text-* entries
  (demo 34 / incoming 31), 0 fontWeight.light, 3 text-amount-*, 16 textWrapStyle.
- packages/theme/dist/v4/globals.{css,scss} — generated; rebuilt from the merged tokens
  rather than hand-merged. The rebuilt CSS keeps the demo-only utilities and leaves
  exactly 3 font-weight:300 declarations, all @font-face for the Light files, matching
  the incoming side's count.
- packages/webkit/scripts/authoring-baseline.json — generated; regenerated on the merged
  tree (224 entries, the demo's own count).
- .specs/doc-prose.md — only checksum and last_updated conflicted; the body auto-merged,
  so neither side's checksum was valid and it was recomputed on the merged body.
- packages/webkit/catalog.json — regenerated on the merged tree (258 exports @ 4.4.0).

apps/webkit-sample never entered the merge: it does not exist at the base or on the
incoming side, so its 579 files pass through untouched.
…er-and-back-button

# Conflicts:
#	packages/webkit/src/components/navigation/menu/injection-key.ts
#	packages/webkit/src/components/navigation/menu/menu-back/menu-back.vue
#	packages/webkit/src/components/navigation/menu/menu-sub-content/menu-sub-content.vue
#	packages/webkit/src/components/navigation/menu/menu-sub-trigger/menu-sub-trigger.vue
#	packages/webkit/src/components/navigation/menu/menu.vue
@isaque-bock-azion
isaque-bock-azion merged commit cad6c1c into main Sep 8, 2026
23 checks passed
@isaque-bock-azion
isaque-bock-azion deleted the feat/menu-split-trigger-and-back-button branch September 8, 2026 13:55
@github-actions github-actions Bot mentioned this pull request Sep 8, 2026
gabriel-lisboa-azion added a commit that referenced this pull request Sep 21, 2026
demo/vue-sample had drifted 540 ahead / 31 behind main while most of its
webkit work was reviewed and landed upstream (#941 command-menu, #942
GlobalHeader placements, #943 NavigationMenu, #952 Menu, #960 sidebar
collapse, #973 floating-panel placement, #975 paginator, #977 templates
exports, #921 amount ladder), after which main swept comments (#951) and
reworked the CLI onto a derived bundle (#968/#970).

Main is the source for everything the two share. Of the 28 conflicted
source files, 17 differed from main only in comments — their code was
byte-identical — and 6 more were genuinely newer on main, so all 23 take
main verbatim, including the use-placement TypeScript rewrite that
replaces the old index.js.

Three carve-outs keep demo work main does not have yet:

- sidebar: main's CSS-driven collapse is the base; the `side` axis is
  re-applied on top of it as data-[side=end] variants rather than the JS
  edge map it used to be, and the drag sign moves into the rail
  composable. The one test that asserted the removed inline transform now
  asserts the panel's published edge, since the direction is CSS the
  unstyled test env cannot see.
- theme: texts.data.js keeps demo's 13 extra text-wrap and 10 extra
  tracking declarations (PR #982, in flight). Both sides carry the same
  34 entries, so an entry count would not have caught the loss.
- doc-prose keeps its chrome-padding fix (PR #981, in flight).

catalog.json, authoring-baseline.json, the theme dist and the lockfile
are regenerated rather than hand-resolved; the 13 conflicted visual
baselines take main's and are regenerated from CI next.

Verified: every file main touched is byte-identical to main except the
22 accounted for above; 2364 browser tests, 179 toolkit tests, lint,
vue-tsc, 99.98% type coverage, both ratchets, the Storybook build and
the webkit-sample build all pass.
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.

3 participants