Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
MeasuredARIA APG pattern in the JSDoclibs/components/menu/src/menu.ts — the pattern named in the class comment, together with why focus moves here and points in the select
MeasuredKeyboard map testedapps/sandbox-e2e/src/menu.spec.ts — every row of the table above in three engines, Enter and Space among them because "the platform does it" is a claim about browsers; libs/components/menu/src/menu.spec.ts for the arrow a root menu leaves alone
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /menu view, and separately an open menu with a submenu beside it, page and panels together: role="menu" has required children and role="menuitem" a required parent, and a nested panel is the arrangement where either could go wrong
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — menu-open, with the submenu open: two panels of the page's own surface drawn side by side, where the edges and the shadows are all there is between them and the page — the sandbox's navigation blanked by the stage, so a row in that list is not a pixel here (req-quality-e2e) Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — the row the user is on carries SelectedItem/SelectedItemText, the disabled row GrayText, the panel a CanvasText edge. The one state this component has is a background, and this is the mode that would flatten it
Measuredprefers-reduced-motionapps/sandbox-e2e/src/menu.spec.ts — the duration read from the panel is the reduced one, and the leave still ends
MeasuredTouch target ≥ 24×24 pxlibs/components/menu/src/menu-item.scss — min-block-size: var(--pct-menu-item-target-min) on the row, which is the target: a row runs the full width of the panel, so the pointer never has to find the text
By designSize axisnone — deliberately: a menu row is not a control with a height. req-api-size is about --pct-control-height-*, and the row's floor is a touch target rather than a size step
MeasuredRTLapps/sandbox-e2e/src/menu.spec.ts — the submenu drawn on the starting side and the arrow keys mirrored with it. It is the one component here where the writing direction reaches the key map, not just the layout
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — the /menu view renders and hydrates with no NG05xx; a closed menu contributes no markup, and the trigger's aria-expanded="false" is there from the server. The inline half is measured in the unit spec instead, under ngServerMode — the flag Angular's own afterNextRender reads — because no e2e view renders an inline menu yet
MeasuredInline: a region, not a layerlibs/components/menu/src/menu.spec.ts — the panel drawn in the host with no overlay ever created, one part in the document rather than two, the Tab stop, and one case per dropped promise: no trigger report, no dismissal, no leave. Gap: no e2e view, so the mode has no axe run, no screenshot and no three-engine reading of the roving tabindex
By designFormsnone — deliberately: a menu is not a form control. It holds no value and implements no FormValueControl; a command that edits one is the consumer's
MeasuredThe page behind stays liveapps/sandbox-e2e/src/menu.spec.ts — a control beside the panel answers a press while the menu is up, and <html> is not locked. The popover's promise, measured again because it is what separates both of them from the dialog
MeasuredA tree of panelsapps/sandbox-e2e/src/menu.spec.ts (three levels, one Escape per level, choosing anywhere closing all of it, the pointer opening a submenu without taking focus into it) + libs/components/menu/src/menu.spec.ts (a press inside a submenu is not a press outside its parent)
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json — the command's text measured twice, against the panel and against the row it stands on, because a menu spends half its life with one row painted; the edge against the page; the disabled row as a warning, on the same footing as every other
By designStrings through PCT_TEXTSnone — deliberately: the component writes no string of its own
MeasuredSize budgetlibs/components/size.snapshot.md — the ./menu row, with ./core beside it
MeasuredScreen-reader logRead 2026-09-28 from docs/acr/at/ — three readers, whole sandbox, and the act that opens it. The first row is announced on open by all three, with its position by two: NVDA Rename, 1 of 4, VoiceOver Rename menu item (1 of 4), Orca leaving main content. · Actions · menu · Rename. — Orca gives the menu by name and role and then the item, VoiceOver the item and its role, NVDA the item and its position. On Escape the trigger reads collapsed again (Orca Actions · collapsed button. · opens menu · collapsed, VoiceOver Actions menu pop up button). The TRIGGER on arrival is as it was: all three name it and say it opens a menu — Orca Actions · collapsed button. · opens menu, NVDA File, button, collapsed, sub Menu, VoiceOver Actions menu pop up button — and the nested triggers read the same way, which is what aria-haspopup on a submenu is for
Measureddocs page/components/menu on the published site — prerendered, the demo's own source is the code tab