Repository navigation
Conversation
…olors - Paint a focused menu row with the system Highlight/HighlightText pair under forced colors, where the translucent focus overlay is replaced by the page color (WCAG 2.4.7). Action, destructive, checkbox, radio, and sub-menu rows. - Reflect the documented presentation capability on the anchored menu and its flyouts (data-presentation="popover"), so presentation:popover theme rules apply. - Correct consumer docs: there is no trigger prop (renderTrigger), the private menu radius defaults to --radius-container, and disabled checkbox/radio rows take no focus. - Add jsdom regressions and an exact-head Chromium evidence spec with a CI step gated on the component-audit PR title.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
…state - Correct the forced-colors changeset: the focused row paints the Highlight/HighlightText pair, not an outline. - Reclassify the warning and docs changeset as [fix]: it changes the development warning string. - Give every Chromium evidence frame its own claimed-state assertion (focused row, semantics and checked state, overflow, resolved presentation, header and footer regions) and fail any frame without one.
…ut row The sub-menu frames compared two root-menu indexes that are both -1 when focus is in the flyout, so the assertion could pass without proving focus moved. Record the focused row's role, label and its menu's position among the visible menus, and require menuitem "Folder A" in the second menu. Root rows now exclude rows that belong to a nested flyout.
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewNo new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Visual Regression44 of 456 shot(s) changed. A change here is a question, not a failure: check whether the after is the
and 24 more. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
… chunks The 31-frame browser test hit its single 10-minute timeout after 19 frames (about 31 seconds a frame on the CI runner). Capture the frames as four serial tests of at most eight frames each, sharing one results map and one manifest, then run a final test that requires every state and the cross-frame presentation and probe-theme checks. Per-frame fail-closed assertions and context cleanup are unchanged.
…er sees it The sub-menu frames waited for two role="menu" elements matching :popover-open, but :popover-open belongs to the flyout's layer host, not to its inner role="menu" element, so the wait could never succeed and the chunk hung until its timeout. Wait up to five seconds for a second visible menu with focus on a menuitem inside it. The exact "Folder A" in the second menu assertion is unchanged.
There was a problem hiding this comment.
Changes requested: the forced-colors focus fix, the presentation reflection and the doc corrections are all in scope, but the PR's own forced-colors evidence fails and the forced-colors styling hides some row content in Chromium.
- The
forced-colors-focusframe fails inpr-a11ybecausehighlightFill()compares pixels toHighlight's RGB while Chromium reports it with 0.8 alpha, so the composited fill never matches; account for the alpha and rerun so the forced-colors frames exist. - With forced colors on, text that inherits the row's
HighlightText(such as a plain-stringendContentshortcut) disappears on the focused row behind Chromium's Canvas backplate and icons drop to about 2:1, so mark focus with a system-color outline or make the focused rowforced-color-adjust: nonewith explicitHighlightTextcolors. - Then cover a destructive or icon row and an end-content row in the forced-colors frames and update the changeset to match what renders.
[Automated review]
| document.body.append(probe); | ||
| const highlight = getComputedStyle(probe).color; | ||
| probe.remove(); | ||
| const [hr, hg, hb] = (highlight.match(/[\d.]+/g) ?? []).map(Number); |
There was a problem hiding this comment.
Chromium's emulated forced-colors palette reports Highlight as rgba(5, 0, 73, 0.8), so this drops the alpha and compares the rendered fill (rgb(55, 51, 109) once composited over Canvas) with rgb(5, 0, 73). Nearly every pixel misses the 30-unit tolerance, which is why forced-colors-focus fails in pr-a11y with a ratio of 0.0005. Composite the parsed color over the row's backdrop before comparing, or sample a reference element painted with background-color: Highlight in the same page.
| default: colorVars['--color-text-primary'], | ||
| ':focus': { | ||
| default: null, | ||
| '@media (forced-colors: active)': 'HighlightText', |
There was a problem hiding this comment.
In Chromium's forced colors this HighlightText only reaches text that inherits from the row, and Chromium paints a Canvas backplate behind that text, so a plain-string endContent (a shortcut hint) turns white-on-white and disappears while the row is focused. The label and description spans set their own colors, so they are forced to CanvasText on a Canvas chip, and icons render at about 2:1 on the fill. The same arm is in the checkbox, radio and sub-menu rows. Either mark focus with a system-color outline and leave the row's colors alone, or make the focused row forced-color-adjust: none and set label, description, end-content and icon colors to HighlightText explicitly.
Primary intent
Whole-component audit of
core/DropdownMenuunder the Component Audit Rubric 1.16.6 (nightly mode), with only the objective remediations its findings settle.spec:AST-029/FR7lets those fixes travel with the audit evidence; each keeps its own red-before/green-after proof below.@astryxdesign/core—DropdownMenu,DropdownMenuItem,DropdownMenuCheckboxItem,DropdownMenuRadioGroup/DropdownMenuRadioItem,DropdownMenuGroup,DropdownMenuDivider,DropdownMenuSubMenu5f9be6c18deb78e6e27493c7a2932a0f86308273(pre-fix score recorded at58c4f241e92954e1b0c6058201bc5537a888f95e; the only commit between them is a Grid docs change)component:DropdownMenu(authority: draft) andmodule:DropdownMenu/useMenuHover(draft) at the base;module:DropdownMenu/useMenuPressis current. Draft records were read for context only and govern no finding.User or maintainer impact
Highlight/HighlightTextpair, as native menus do. Action, destructive, checkbox, radio, and sub-menu rows; the same rows render inContextMenuand breadcrumb menus. Nothing changes outside forced colors.presentation:popoverrules for thedropdown-menutarget — a visual prop the docs list — got nothing: only the bottom sheet reflected its presentation. The anchored menu and its flyouts now reflectdata-presentation="popover".buttonis "mutually exclusive withtrigger" (no such prop exists; it isrenderTrigger, and the development warning said the same), that the private menu radius defaults to--radius-element(it is--radius-container), and that disabled checkbox and radio rows "stay focusable" (they take no focus; arrows, typeahead, and the pointer highlight skip them).Problem and solution fit
backgroundColoron:focus,outline: none). Why does that fail? Forced colors forcebackground-colorto the system Canvas color, keeping the overlay's alpha. Why does it matter? Keyboard operation depends on knowing where focus is (WCAG 2.4.7). Fix: add@media (forced-colors: active)arms —Highlightbackground andHighlightTexttext — to the existing:focus(and pressed) arms of each row.:focusmatches the menu's one-highlight model, where pointer hover moves focus. The destructive variant's owncolorkeeps the same arm, because a later flatcolorwould otherwise replace it.themeProps('dropdown-menu')without the documentedpresentationcapability. Fix: pass{presentation: 'popover'}on the anchored menu and on sub-menu flyouts, the other pointer surface carrying the same target.skips aria-disabled menuitemradio and menuitemcheckbox items during arrow navigation). Fix: correct the prose, the TriggerProps JSDoc, and the warning text. Behavior does not change.Compiled CSS against the base differs by exactly three rules, all inside
@media (forced-colors: active). Every non-forced-colors frame therefore paints identically to the base.Authority
spec:AST-020/FR1(WCAG 2.2 A/AA baseline) andFR10(forced colors is a component outcome to review)presentation(§2 INV6 / X17, FIX)architecture:component-theming-surface/INV6: variant state is reflected on the owning target throughthemeProps; thedropdown-menutarget documentspresentationtriggerprop (§8 X5a, BLOCK)architecture:public-component-apiconsumer-doc projection: consumer docs change when they "would otherwise become false"; docs naming a nonexistent prop is a listed failure.renderTrigger/buttonexclusivity followsspec:AST-055/FR7Global review routing:
node scripts/review-global-baselines.mjs --authority-commit 58c4f241e92954e1b0c6058201bc5537a888f95e --review-head <head> --triggers accessibility,theming,public-api,behaviormatched current claims inarchitecture:component-theming-surface,architecture:public-component-api,architecture:theme-application,architecture:theme-authoring-contract,architecture:theme-compilation,architecture:container-padding,spec:AST-002,AST-009,AST-012,AST-020,AST-055,AST-056,AST-057,AST-058, andAST-059. Only the claims cited in the table govern a change here. AST-056/057/058 do not apply to DropdownMenu. AST-009 is not triggered: no AT-specific focus-order or announcement claim changes (FR7 proportionate evidence).Component audit score
triggerpropRubric 1.16.6. §5b (rendered judgment) is
not measuredin both rows and its weight is redistributed, as the rubric requires. The managed Mac used for this audit cannot launch a browser, so rendered evidence comes from exact-head Linux CI (below). What moved: §1 4 → 4.5, §2 4 → 4.5, §8 3 → 4.5. §11 is not run (existing core component).Section scorecard
pr-rtlsupplies the runRetained NITs: the focus-on-open Effect schedules an animation frame without cancelling it (benign: the callback is a no-op on a closed or detached menu; a naive cancel would drop focus after a re-render, so it is left alone);
useMenuOverflowcreates a Resize/MutationObserver per open menu (one per open menu, not a repeated-row path, soarchitecture:react-component-runtime/INV7holds); therenderTriggerJSDoc@examplecarries//comment lines inside its bare fence (wiki guidance only; no current repository rule).FR5 evidence inventory (closed)
Rows come from the public exports in
DropdownMenu/index.ts, documented concepts, states reachable through the public API, and branches reachable from them. "CI frame" names a frame of the exact-head evidence spec.Inventory
button,hasChevron, default i18n label, size/variant, icon-only, open paint, tooltip hidden while open)DropdownMenu.tsxDropdownMenu.test.tsxDropdownMenu.doc.mjstrigger-rest,trigger-rest-darkcomponent-theming-surface/INV5renderTrigger(ARIA wiring, press model, keyboard opens, mutual exclusion and warning)DropdownMenu.tsxCustomTriggerstory, not capturedAST-055/FR1–FR3, FR7isMenuOpen/onOpenChange, mounted open,onClickDropdownMenu.tsxkeyboard-open,pointer-openarchitecture:layer-runtimeDropdownMenu.tsx,useListFocus,useTypeaheadkeyboard-open,disabled-row(End)AST-020/FR1; component-local otherwiseDropdownMenu.tsxDropdownMenuFocusReturn.a11y.browser.spec.tskeyboard-openvspointer-openarchitecture:interaction-modality/INV1useMenuPressMenuPress.a11y.chromium.spec.tspressed-rowmodule:DropdownMenu/useMenuPressFR1–FR7 (current)presentation: popover, bottom-sheet, adaptiveDropdownMenu.tsx,useAdaptivePresentationbottom-sheet(-dark),adaptive-narrow-coarseinteraction-modality,component-theming-surface/INV6presentation:adaptiveis never reflected (Needs Review)placement/alignment, logical in RTLDropdownMenu.tsxkeyboard-open-rtl; curated RTL targetcore-dropdownmenu--alignment-endlayer-runtimemenuWidth,menuMaxHeight, viewport caps, overflow scrollDropdownMenu.tsx,menuWidth.ts,useMenuOverflow.tsoverflow-scroll,popover-narrowAST-059/FR2, FR7header/footerregionsDropdownMenu.tsxMenuHeaderFooter.a11y.chromium.spec.tsheader-footerrenderDropdownItems.tsx,DropdownMenuItem.tsxkeyboard-open,destructive-row(-dark),disabled-row(-dark)family:navigation-destinations(href rows)DropdownMenuItemcompoundDropdownMenuItem.tsxDropdownMenuItem.doc.mjssubmenu-open(compound rows)DropdownMenuDividerDropdownMenuDivider.tsxdestructive-row(story includes a divider)component-theming-surface/INV5DropdownMenuGroup(namedrole="group")DropdownMenuGroup.tsxCompoundWithGroupsstory, not capturedDropdownMenuCheckboxItem(aria-checked, disabled, close-on-select)DropdownMenuCheckboxItem.tsxDropdownMenuSelectable.test.tsx; new forced-colors testcheckbox-rows(-dark)AST-020/FR1DropdownMenuRadioGroup/RadioItemDropdownMenuRadio*.tsxradio-rowsDropdownMenuSubMenu: flyout, drill-in, adaptive, async spinnerDropdownMenuSubMenu.tsx,useMenuDrillIn.tsxDropdownMenuSubMenu.test.tsx;DropdownMenuDrillIn.a11y.chromium.spec.tssubmenu-open(-dark, -rtl)interaction-modalityDropdownMenuContext/useDropdownMenuContext(custom items)DropdownMenuContext.tsxpublic-component-api/INV12themingTargets.test.ts,derivedVarRegistry.test.tskeyboard-open-probe,keyboard-open-butter(-dark)component-theming-surfaceDropdownMenuForcedColors.test.tsxforced-colors-focus(-dark),forced-colors-focus-before-controlAST-020/FR1, FR10DropdownMenu.tsx,DropdownMenuSubMenu.tsxcheck:i18n-cataloguseAdaptivePresentationadaptive-narrow-coarseinteraction-modalityChange and evidence
TDD, red before green, for each production change (logs kept with the run):
DropdownMenuForcedColors.test.tsx— 6 cases. Red on the base row files (no forced-colors:focusrule on any row class); green at the head.DropdownMenuThemeReflection.test.tsx— root popover, data-mode popover, and sub-menu flyout reflectdata-presentation="popover"; the warning namesrenderTrigger. Red before each change (flyout case red separately); green at the head.Screenshots and evidence obligations
Real-browser frames come from exact-head CI, not from this description. This PR adds
packages/core/src/DropdownMenu/__tests__/DropdownMenuAudit.a11y.chromium.spec.ts, run by apr-a11ystep gated on this PR title. The step runsaccessibility-audit.js --components DropdownMenu --fail-on-newand the spec, then uploadsdropdown-menu-audit-evidence-<sha7>. The artifact holds:manifest.jsonwith the state-visual matrix and a rendered contrast-pair matrix.The spec fails closed when the environment is not what the frame claims (build, theme, mode, viewport, pointer, forced colors, settled geometry, page errors) and when the claimed state is not observed. Every frame has its own state assertion, and a frame without one fails:
aria-expanded="false";popover, first enabled row focused; at 320px the menu box stays inside the viewport;menuitemcheckboxrows witharia-checkedfalse/true; radio: threemenuitemradiorows with only "Newest" checked;menuitemis "Folder A" in the second visible menu (the flyout), not a root row;scrollHeight>clientHeight,overflow-yauto);bottom-sheetinside a dialog;Highlightand more than half its interior pixels match; the before-control arm matches under 5%.The 320px frames must not scroll horizontally.
Before/after: no frame outside forced colors can change, because the compiled CSS delta is confined to
@media (forced-colors: active). The forced-colors pair uses a helper-isolated control arm that restores the overlay focus paint on the head build. It is labelled as such and is not a base build.Expected visual-gate delta: the probe theme ships a
presentation:popoverrule fordropdown-menuthat never matched before. Probe-theme DropdownMenu captures will now show it. That change is the fix working, not drift.These frames, the axe report, and
pr-rtlare this head's open evidence obligations. §5b staysnot measureduntil they are read.Needs Review (deliberately unresolved)
DropdownMenu.spec.md(draft) has concept rows inside the behavior table, DEC references that do not match the decision log (FR7 cites DEC-3 and FR9 cites DEC-2, but those bodies sit under DEC-7 and DEC-3), a verification map whose FR numbers are shifted, duplicated "Open questions"/"Content boundary" sections, andtriggerwhere the prop isrenderTrigger. Three open pull requests reflow this file, so the observational repair is left to its owner rather than reflowed here.presentation:adaptivenever matches. The target reflects the resolved presentation (popoverorbottom-sheet), so the documentedadaptivevalue can never select a theme rule. Reflecting the requested value or narrowing the documented values is a theming-contract decision.dropdown-menutarget placement. On the anchored menu, Popover'spopovertarget paints the surface (background, shadow, outer radius).dropdown-menusits on the inner menu element (padding, inner radius), so a theme radius ondropdown-menucan disagree with the outer corner. Who owns the anchored surface (component-theming-surface/INV4–INV5) is an ownership question.No public API, default, or compatibility promise changes here. The
data-presentationattribute was already documented on this target.FR10 eligibility report
{ "version": 1, "component": "core/DropdownMenu", "mode": "nightly", "rubricVersion": "1.16.6", "repositoryBase": "5f9be6c18deb78e6e27493c7a2932a0f86308273", "componentContract": {"id": "component:DropdownMenu", "authority": "draft", "backfillInThisPr": false}, "inventory": {"rows": 22, "closed": true, "rowsWithoutRenderedFrame": [2, 14, 17, 18, 21]}, "remediations": [ {"id": "forced-colors-focus", "kind": "bug", "evidence": ["DropdownMenuForcedColors.test.tsx red->green", "CI forced-colors frames + before-control arm"]}, {"id": "presentation-reflection", "kind": "bug", "evidence": ["DropdownMenuThemeReflection.test.tsx red->green", "CI probe-theme frame"]}, {"id": "consumer-doc-drift", "kind": "doc-drift", "evidence": ["warning test red->green", "types/source/tests"]} ], "unresolved": {"objective": [], "manual": ["spec-damage", "adaptive-reflection", "target-ownership", "apg-adoption", "selectable-pressed", "open-branch-forced-colors"]}, "approvals": {"specOwner": "not requested", "reviewLoop": "pending"}, "checks": "pending exact head", "verdict": "ineligible", "reasons": ["spec:AST-029 is phase: accepted (FR11 auto-merge inactive)", "manual decisions recorded", "exact-head evidence pending"] }Scope
spec:AST-029/FR7).Testing
Local, managed Mac (no browser available):
pnpm vitest run packages/core/src/DropdownMenu packages/core/src/ContextMenu packages/core/src/Breadcrumbs packages/core/src/MoreMenu packages/core/src/hooks/useMenuPress.test.tsx packages/core/src/hooks/useMenuHover.test.tsx packages/core/src/theme packages/core/src/docPropReferences.test.ts packages/core/src/docPropLiterals.test.ts packages/core/src/pressableCoverage.test.ts .github/scripts— 102 files, 3063 tests passpnpm vitest run packages/core/src/DropdownMenu packages/core/src/ContextMenu packages/core/src/Breadcrumbs packages/core/src/theme/themingTargets.test.ts packages/core/src/docPropReferences.test.tsat the final head — 772 passASTRYX_STRICT_LINT=1 eslint packages/core/src/DropdownMenu apps/storybook/stories/DropdownMenu.stories.tsx— cleanpnpm -F @astryxdesign/core typecheck,pnpm -F @astryxdesign/core typecheck:docs,pnpm -F @astryxdesign/storybook typecheckpnpm build,node scripts/verify-exports.mjs,pnpm sync:exports:checkpnpm -F @astryxdesign/docsite generate && pnpm -F @astryxdesign/docsite testpnpm storybook:buildpnpm check:repo,pnpm check:changesets,pnpm check:knowledge,pnpm check:use-client,pnpm check:i18n-catalogpackages/core/dist/astryx.css, base vs head: three added rules, all under@media (forced-colors: active)Exact-head CI:
pr-a11y(including the new audit evidence step),pr-rtl, visual gate, and the repository-wide test suite.