@astryxdesign/core 0.7.0-canary.fe57f54 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +5 -4
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +1 -1
  7. package/dist/Calendar/styles.d.ts +6 -6
  8. package/dist/Chat/ChatComposerInput.d.ts +0 -11
  9. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  10. package/dist/Chat/ChatComposerInput.js +0 -11
  11. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  12. package/dist/Chat/useTriggerMenu.js +1 -1
  13. package/dist/CommandPalette/CommandPaletteList.d.ts +3 -2
  14. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  15. package/dist/CommandPalette/CommandPaletteList.js +3 -2
  16. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  17. package/dist/DateInput/TouchDateField.js +4 -1
  18. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  19. package/dist/DateTimeInput/TouchDateTimeField.js +29 -13
  20. package/dist/DropdownMenu/DropdownMenu.d.ts +11 -0
  21. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  22. package/dist/DropdownMenu/DropdownMenu.js +173 -11
  23. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenuSubMenu.js +13 -2
  25. package/dist/DropdownMenu/menuRegionsContext.d.ts +2 -0
  26. package/dist/DropdownMenu/menuRegionsContext.d.ts.map +1 -0
  27. package/dist/DropdownMenu/menuRegionsContext.js +17 -0
  28. package/dist/Field/inputStyles.stylex.d.ts +3 -3
  29. package/dist/Layout/container.stylex.d.ts +18 -20
  30. package/dist/Layout/container.stylex.d.ts.map +1 -1
  31. package/dist/Layout/container.stylex.js +15 -25
  32. package/dist/Layout/padding.stylex.d.ts +231 -231
  33. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  34. package/dist/Lightbox/Lightbox.js +15 -6
  35. package/dist/Markdown/plugins/protocol.d.ts.map +1 -1
  36. package/dist/Markdown/plugins/protocol.js +33 -8
  37. package/dist/NavItem/navItemStyles.stylex.d.ts +5 -5
  38. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  39. package/dist/PowerSearch/PowerSearch.js +21 -1
  40. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  41. package/dist/PowerSearch/PowerSearchEditPopover.js +30 -15
  42. package/dist/Stack/stack.stylex.d.ts +22 -22
  43. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  44. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +89 -14
  45. package/dist/Table/table.stylex.d.ts +6 -6
  46. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  47. package/dist/TimeInput/TouchTimeField.js +4 -1
  48. package/dist/Tokenizer/Tokenizer.d.ts +1 -12
  49. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  50. package/dist/Tokenizer/Tokenizer.js +1 -13
  51. package/dist/Typeahead/BaseTypeahead.d.ts +0 -11
  52. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  53. package/dist/Typeahead/BaseTypeahead.js +1 -9
  54. package/dist/Typeahead/Typeahead.d.ts +1 -12
  55. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/Typeahead.js +1 -13
  57. package/dist/astryx.css +17 -12
  58. package/dist/hooks/useFocusTrap.d.ts +8 -0
  59. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  60. package/dist/hooks/useFocusTrap.js +5 -1
  61. package/dist/hooks/useRenamedProp.d.ts +6 -6
  62. package/dist/hooks/useRenamedProp.js +6 -6
  63. package/dist/reset.a11y.chromium.spec.d.ts +2 -0
  64. package/dist/reset.a11y.chromium.spec.d.ts.map +1 -0
  65. package/dist/reset.a11y.chromium.spec.js +176 -0
  66. package/dist/theme/tokens.stylex.d.ts +30 -30
  67. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  68. package/dist/theme/tokens.stylex.js +18 -15
  69. package/dist/theme/useTheme.d.ts +1 -1
  70. package/package.json +4 -4
  71. package/src/BottomSheet/BottomSheet.doc.mjs +4 -4
  72. package/src/BottomSheet/BottomSheet.tsx +5 -4
  73. package/src/BottomSheet/BottomSheetPanel.test.tsx +6 -9
  74. package/src/BottomSheet/BottomSheetPanel.tsx +2 -3
  75. package/src/Chat/ChatComposerInput.doc.mjs +3 -3
  76. package/src/Chat/ChatComposerInput.test.tsx +7 -55
  77. package/src/Chat/ChatComposerInput.tsx +0 -36
  78. package/src/Chat/useTriggerMenu.tsx +1 -4
  79. package/src/CommandPalette/CommandPalette.spec.md +2 -0
  80. package/src/CommandPalette/CommandPaletteList.spec.md +189 -0
  81. package/src/CommandPalette/CommandPaletteList.test.tsx +47 -0
  82. package/src/CommandPalette/CommandPaletteList.tsx +3 -2
  83. package/src/DateInput/TouchDateField.tsx +3 -1
  84. package/src/DateTimeInput/DateTimeInputTouch.test.tsx +60 -1
  85. package/src/DateTimeInput/TouchDateTimeField.tsx +31 -14
  86. package/src/DropdownMenu/DropdownMenu.doc.mjs +12 -0
  87. package/src/DropdownMenu/DropdownMenu.spec.md +65 -40
  88. package/src/DropdownMenu/DropdownMenu.test.tsx +457 -0
  89. package/src/DropdownMenu/DropdownMenu.tsx +278 -49
  90. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +6 -3
  91. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +2 -1
  92. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +11 -1
  93. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +195 -0
  94. package/src/DropdownMenu/menuRegionsContext.ts +17 -0
  95. package/src/Layout/container.stylex.ts +11 -21
  96. package/src/Lightbox/Lightbox.test.tsx +58 -0
  97. package/src/Lightbox/Lightbox.tsx +12 -6
  98. package/src/Markdown/plugins/protocol.ts +45 -12
  99. package/src/Markdown/plugins/sourceLines.test.tsx +121 -0
  100. package/src/PowerSearch/PowerSearch.tsx +28 -1
  101. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +284 -0
  102. package/src/PowerSearch/PowerSearchEditPopover.tsx +41 -13
  103. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +33 -9
  104. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +81 -18
  105. package/src/TimeInput/TouchTimeField.tsx +3 -1
  106. package/src/Tokenizer/Tokenizer.doc.mjs +0 -16
  107. package/src/Tokenizer/Tokenizer.test.tsx +12 -63
  108. package/src/Tokenizer/Tokenizer.tsx +1 -25
  109. package/src/Toolbar/Toolbar.test.tsx +29 -0
  110. package/src/Typeahead/BaseTypeahead.doc.mjs +0 -16
  111. package/src/Typeahead/BaseTypeahead.test.tsx +1 -1
  112. package/src/Typeahead/BaseTypeahead.tsx +1 -21
  113. package/src/Typeahead/Typeahead.doc.mjs +0 -7
  114. package/src/Typeahead/Typeahead.test.tsx +24 -42
  115. package/src/Typeahead/Typeahead.tsx +1 -25
  116. package/src/hooks/useFocusTrap.ts +5 -1
  117. package/src/hooks/useRenamedProp.ts +6 -6
  118. package/src/reset.a11y.chromium.spec.ts +178 -0
  119. package/src/reset.css +8 -8
  120. package/src/reset.test.ts +112 -0
  121. package/src/theme/__tests__/SpacingRootFontSize.a11y.chromium.spec.ts +198 -0
  122. package/src/theme/tokens.stylex.ts +18 -15
  123. package/src/theme/tokens.test.ts +1 -1
  124. package/src/theme/useTheme.test.tsx +4 -4
  125. package/src/theme/useTheme.ts +1 -1
@@ -17,6 +17,7 @@ verified_by:
17
17
  packages/core/src/DropdownMenu/DropdownMenuSubMenu.test.tsx,
18
18
  packages/core/src/DropdownMenu/__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts,
19
19
  packages/core/src/DropdownMenu/__tests__/MenuPress.a11y.chromium.spec.ts,
20
+ packages/core/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts,
20
21
  packages/core/src/BottomSheet/BottomSheet.test.tsx,
21
22
  packages/core/src/List/List.test.tsx,
22
23
  packages/core/src/Indicator/Indicator.test.tsx,
@@ -85,35 +86,37 @@ Consumer migration instructions belong in consumer docs and release notes.
85
86
  ## Public concepts
86
87
 
87
88
  Consumer props, item shapes, subcomponents, and presentation policy remain
88
- documented in `DropdownMenu.doc.mjs` and the subcomponent docs. Two component-local concepts are added, by DEC-2 and DEC-3; each keeps its
89
- released default.
89
+ documented in `DropdownMenu.doc.mjs` and the subcomponent docs. Three component-local concepts are added, by DEC-2, DEC-3 and DEC-8; each
90
+ keeps its released default.
90
91
 
91
- | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
92
- | --------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------- | ------------------------ | --------- | --------------------------------------------------- |
93
- | Menu height cap | `menuMaxHeight`: a number of pixels | Lifts the 300px cap for a menu that must fit its rows; the viewport still bounds it. | Pointer presentation | `300px` | `component:DropdownMenu` | stable | Ignored by the touch sheet. |
94
- | Trigger source | `button` (Button props) or `renderTrigger` (render prop) | Which control the menu hangs off. `trigger` receives `DropdownMenuTriggerProps` — the press model, keyboard opens, toggle click and ARIA wiring — and names the menu. | Both presentations | `button` | `component:DropdownMenu` | stable | Both given: `trigger` wins and a dev warning fires. |
92
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
93
+ | ----------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------- | ------------------------ | --------- | --------------------------------------------------- |
94
+ | Menu height cap | `menuMaxHeight`: a number of pixels | Lifts the 300px cap for a menu that must fit its rows; the viewport still bounds it. | Pointer presentation | `300px` | `component:DropdownMenu` | stable | Ignored by the touch sheet. |
95
+ | Trigger source | `button` (Button props) or `renderTrigger` (render prop) | Which control the menu hangs off. `trigger` receives `DropdownMenuTriggerProps` — the press model, keyboard opens, toggle click and ARIA wiring — and names the menu. | Both presentations | `button` | `component:DropdownMenu` | stable | Both given: `trigger` wins and a dev warning fires. |
96
+ | Header and footer | `header`, `footer`: any React node | Content above and below the rows, beside the menu inside the same popover or sheet — never inside the `role="menu"` element. | Both presentations | none | `component:DropdownMenu` | stable | A false or empty value renders no region. |
95
97
 
96
98
  ## Behavioral and layout contract
97
99
 
98
- | ID | Candidate invariant | Basis | Draft review state |
99
- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
100
- | FR1 | Every current render contains a Trigger button. Pointer presentation renders a Pointer menu surface with pointer-owned rows and optional pointer headings, dividers, indicators, and nested flyouts. Touch presentation renders a Touch sheet frame containing a Touch menu surface, Touch heading, Touch action list, and Touch action rows. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
101
- | FR2 | The six current local targets are `dropdown-menu`, `dropdown-menu-item`, `dropdown-menu-radio`, `dropdown-menu-section-heading`, `dropdown-menu-divider`, and `dropdown-menu-indicator-icon`; every target remains on its current painted element. | Current source, docs, and tests | Verified current inventory; no target change |
102
- | FR3 | Button owns the Trigger button, BottomSheet owns the Touch sheet frame, List owns the Touch action list and Touch action rows, Indicator owns checkbox chrome, and Icon owns ordinary rendered icons. | Current source and owner docs | Verified current delegation; no ownership change |
103
- | FR4 | The same `dropdown-menu` target reaches the alternative Pointer menu surface and Touch menu surface. Pointer action rows retain `dropdown-menu-item`; touch action rows instead use List's `list-item` target. | Current source and tests | Verified modality split; no target change |
104
- | FR5 | Both menu surfaces carry `data-astryx-menu-press` and follow `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the browser's stray click never acts, and the Pointer menu surface declares `touch-action` by overflow. Trigger opening and keyboard navigation are unchanged. | `module:DropdownMenu/useMenuPress`; `DropdownMenu.test.tsx` press model suite | Proposed; verified in jsdom and real Chromium |
105
- | FR6 | When the pointer menu closes, focus returns to the Trigger button with a visible ring only when the menu was driven by keyboard; a press outside that landed on a focusable control keeps focus there. | `architecture:interaction-modality` INV1; proposed in this change; `DropdownMenu.test.tsx` closing suite | Proposed; verified in jsdom, pending owner review |
106
- | FR7 | `menuMaxHeight` MUST replace the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | `component:DropdownMenu/DEC-3` | Proposed; verified in jsdom; owner to confirm DEC-3 |
107
- | FR8 | The Trigger button opens the pointer menu on a mouse press-down and on a finger held for the long-press delay, with the module's settle rule on the opening release; pressing the trigger of an open menu closes it without reopening in the same gesture; a tap and the keyboard open as before (`module:DropdownMenu/useMenuPress` proposed FR8, FR9). | Proposed `module:DropdownMenu/useMenuPress/DEC-4`; `DropdownMenu.test.tsx` press model suite | Proposed; verified in jsdom, pending owner review |
108
- | Trigger source | `button` (Button props) or `renderTrigger` (render prop) | Which control the menu hangs off. `trigger` receives `DropdownMenuTriggerProps` — the press model, keyboard opens, toggle click and ARIA wiring — and names the menu. | Both presentations | `button` | `component:DropdownMenu` | stable | Both given: `trigger` wins and a dev warning fires. |
109
- | FR9 | With `renderTrigger`, the rendered control MUST carry `aria-haspopup`, `aria-expanded`, `aria-controls` and the `id` the menu's `aria-labelledby` points at, and MUST receive the same press-model, keyboard-open and toggle-click handlers as the Button path; the menu is named by that control. `button` and `trigger` are mutually exclusive: both given, `trigger` wins and a dev warning fires, in both presentations. A drilled-in Back row (FR11) returns to the menu by that same name: the caller's `aria-label` when given, else the control's accessible name, read as the menu opens. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner to confirm DEC-2 |
110
- | Row destination | `href` (+ `target`, `rel`) on an action item | The row's act is navigation: the row itself is the link (`family:navigation-destinations`). | Pointer action row and Touch action row, compound and data mode | none (an action row) | `component:DropdownMenu` | stable | Blocked schemes follow the family rule; a disabled row has none. |
111
- | FR10 | An action row with `href` MUST render as one anchor carrying `role="menuitem"` (the pointer menu) or the touch sheet's link row, routed through `LinkProvider`; a plain click MUST run `onClick`, then close the menu (`hasCloseOnSelect`), then let the browser navigate; a modified click or middle click MUST be left to the browser and MUST NOT run `onClick`; Enter and Space MUST activate through a synthesized click that carries the key's modifiers. A disabled link row has no `href`. | `component:DropdownMenu/DEC-2`; `family:navigation-destinations` | Proposed; verified in jsdom; owner to confirm DEC-1 |
112
- | Sub-menu presentation | `presentation` on a sub-menu row: `flyout`, `drill-in`, `adaptive` | Whether a sub-menu opens beside its row or replaces the menu's rows with its own and a Back row, in the same box. `adaptive` drills in when a coarse pointer opened the menu. | Pointer presentation, compound and data mode | `adaptive` | `component:DropdownMenu` | stable | An unknown value reads as `adaptive`. |
113
- | FR11 | When a compact touch display opened the pointer menu (`COMPACT_TOUCH_PRESENTATION_QUERY`, the same query the root presentation uses for its bottom sheet, sampled once as it opens) an `adaptive` sub-menu row MUST drill in: its rows and a leading Back row named "Back to <parent>" replace the menu's rows in the same Pointer menu surface, no flyout opens, the drilled list is a `menu` named by the row's label, and Back, Escape and ArrowLeft MUST return to the row with focus on it; a pick inside closes the whole menu. Roving focus and typeahead scope to the shown rows; sibling rows and dividers need no knowledge of it. `presentation="flyout"` keeps the flyout; `"drill-in"` drills in on any pointer. | `component:DropdownMenu/DEC-2`; `architecture:interaction-modality` | Proposed; verified in jsdom and Chromium (coarse pointer); owner to confirm DEC-5 |
114
- | FR12 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp move to the last and first fully visible enabled row and, pressed there again, one viewport further without wrapping; ArrowUp on the trigger opens with the last enabled row highlighted; the key that opened the menu and its auto-repeats do not activate; typeahead matches the row's label element alone and ignores Control/Command chords and input-method composition. | Proposed in this change; `DropdownMenu.test.tsx` keyboard suite, `useListFocus.test.tsx`, `useTypeahead.test.tsx` | Proposed; verified in jsdom, pending owner review |
115
- | FR13 | On a mouse, a nested flyout stays open while the pointer is inside the triangle from where it left its row to the flyout's near edge — including while the pointer is paused there — and closes after the existing delay once the pointer has left both the row and that triangle. The row's click toggle and its guard window are unchanged. | Proposed in this change; `DropdownMenuSubMenu.test.tsx` safe-triangle suite, `useMenuHover.test.tsx` | Proposed; verified in jsdom, pending owner review |
116
- | FR14 | A compound `DropdownMenuGroup` is one `role="group"` named by its heading through `aria-labelledby`; the heading carries `dropdown-menu-section-heading`, is plain text rather than a menu item, and is skipped by roving focus and typeahead. An untitled group is an unnamed `role="group"`. | DEC-1, docs, and tests | Proposed; awaiting owner approval |
100
+ | ID | Candidate invariant | Basis | Draft review state |
101
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
102
+ | FR1 | Every current render contains a Trigger button. Pointer presentation renders a Pointer menu surface with pointer-owned rows and optional pointer headings, dividers, indicators, and nested flyouts. Touch presentation renders a Touch sheet frame containing a Touch menu surface, Touch heading, Touch action list, and Touch action rows. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
103
+ | FR2 | The six current local targets are `dropdown-menu`, `dropdown-menu-item`, `dropdown-menu-radio`, `dropdown-menu-section-heading`, `dropdown-menu-divider`, and `dropdown-menu-indicator-icon`; every target remains on its current painted element. | Current source, docs, and tests | Verified current inventory; no target change |
104
+ | FR3 | Button owns the Trigger button, BottomSheet owns the Touch sheet frame, List owns the Touch action list and Touch action rows, Indicator owns checkbox chrome, and Icon owns ordinary rendered icons. | Current source and owner docs | Verified current delegation; no ownership change |
105
+ | FR4 | The same `dropdown-menu` target reaches the alternative Pointer menu surface and Touch menu surface. Pointer action rows retain `dropdown-menu-item`; touch action rows instead use List's `list-item` target. | Current source and tests | Verified modality split; no target change |
106
+ | FR5 | Both menu surfaces carry `data-astryx-menu-press` and follow `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the browser's stray click never acts, and the Pointer menu surface declares `touch-action` by overflow. Trigger opening and keyboard navigation are unchanged. | `module:DropdownMenu/useMenuPress`; `DropdownMenu.test.tsx` press model suite | Proposed; verified in jsdom and real Chromium |
107
+ | FR6 | When the pointer menu closes, focus returns to the Trigger button with a visible ring only when the menu was driven by keyboard; a press outside that landed on a focusable control keeps focus there. | `architecture:interaction-modality` INV1; proposed in this change; `DropdownMenu.test.tsx` closing suite | Proposed; verified in jsdom, pending owner review |
108
+ | FR7 | `menuMaxHeight` MUST replace the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | `component:DropdownMenu/DEC-3` | Proposed; verified in jsdom; owner to confirm DEC-3 |
109
+ | FR8 | The Trigger button opens the pointer menu on a mouse press-down and on a finger held for the long-press delay, with the module's settle rule on the opening release; pressing the trigger of an open menu closes it without reopening in the same gesture; a tap and the keyboard open as before (`module:DropdownMenu/useMenuPress` proposed FR8, FR9). | Proposed `module:DropdownMenu/useMenuPress/DEC-4`; `DropdownMenu.test.tsx` press model suite | Proposed; verified in jsdom, pending owner review |
110
+ | Trigger source | `button` (Button props) or `renderTrigger` (render prop) | Which control the menu hangs off. `trigger` receives `DropdownMenuTriggerProps` — the press model, keyboard opens, toggle click and ARIA wiring — and names the menu. | Both presentations | `button` | `component:DropdownMenu` | stable | Both given: `trigger` wins and a dev warning fires. |
111
+ | FR9 | With `renderTrigger`, the rendered control MUST carry `aria-haspopup`, `aria-expanded`, `aria-controls` and the `id` the menu's `aria-labelledby` points at, and MUST receive the same press-model, keyboard-open and toggle-click handlers as the Button path; the menu is named by that control. `button` and `trigger` are mutually exclusive: both given, `trigger` wins and a dev warning fires, in both presentations. A drilled-in Back row (FR11) returns to the menu by that same name: the caller's `aria-label` when given, else the control's accessible name, read as the menu opens. | `component:DropdownMenu/DEC-2` | Proposed; verified in jsdom; owner to confirm DEC-2 |
112
+ | Row destination | `href` (+ `target`, `rel`) on an action item | The row's act is navigation: the row itself is the link (`family:navigation-destinations`). | Pointer action row and Touch action row, compound and data mode | none (an action row) | `component:DropdownMenu` | stable | Blocked schemes follow the family rule; a disabled row has none. |
113
+ | FR10 | An action row with `href` MUST render as one anchor carrying `role="menuitem"` (the pointer menu) or the touch sheet's link row, routed through `LinkProvider`; a plain click MUST run `onClick`, then close the menu (`hasCloseOnSelect`), then let the browser navigate; a modified click or middle click MUST be left to the browser and MUST NOT run `onClick`; Enter and Space MUST activate through a synthesized click that carries the key's modifiers. A disabled link row has no `href`. | `component:DropdownMenu/DEC-2`; `family:navigation-destinations` | Proposed; verified in jsdom; owner to confirm DEC-1 |
114
+ | Sub-menu presentation | `presentation` on a sub-menu row: `flyout`, `drill-in`, `adaptive` | Whether a sub-menu opens beside its row or replaces the menu's rows with its own and a Back row, in the same box. `adaptive` drills in when a coarse pointer opened the menu. | Pointer presentation, compound and data mode | `adaptive` | `component:DropdownMenu` | stable | An unknown value reads as `adaptive`. |
115
+ | FR11 | When a compact touch display opened the pointer menu (`COMPACT_TOUCH_PRESENTATION_QUERY`, the same query the root presentation uses for its bottom sheet, sampled once as it opens) an `adaptive` sub-menu row MUST drill in: its rows and a leading Back row named "Back to <parent>" replace the menu's rows in the same Pointer menu surface, no flyout opens, the drilled list is a `menu` named by the row's label, and Back, Escape and ArrowLeft MUST return to the row with focus on it; a pick inside closes the whole menu. Roving focus and typeahead scope to the shown rows; sibling rows and dividers need no knowledge of it. `presentation="flyout"` keeps the flyout; `"drill-in"` drills in on any pointer. | `component:DropdownMenu/DEC-2`; `architecture:interaction-modality` | Proposed; verified in jsdom and Chromium (coarse pointer); owner to confirm DEC-5 |
116
+ | FR12 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp move to the last and first fully visible enabled row and, pressed there again, one viewport further without wrapping; ArrowUp on the trigger opens with the last enabled row highlighted; the key that opened the menu and its auto-repeats do not activate; typeahead matches the row's label element alone and ignores Control/Command chords and input-method composition. | Proposed in this change; `DropdownMenu.test.tsx` keyboard suite, `useListFocus.test.tsx`, `useTypeahead.test.tsx` | Proposed; verified in jsdom, pending owner review |
117
+ | FR13 | On a mouse, a nested flyout stays open while the pointer is inside the triangle from where it left its row to the flyout's near edge — including while the pointer is paused there — and closes after the existing delay once the pointer has left both the row and that triangle. The row's click toggle and its guard window are unchanged. | Proposed in this change; `DropdownMenuSubMenu.test.tsx` safe-triangle suite, `useMenuHover.test.tsx` | Proposed; verified in jsdom, pending owner review |
118
+ | FR14 | A compound `DropdownMenuGroup` is one `role="group"` named by its heading through `aria-labelledby`; the heading carries `dropdown-menu-section-heading`, is plain text rather than a menu item, and is skipped by roving focus and typeahead. An untitled group is an unnamed `role="group"`. | DEC-1, docs, and tests | Proposed; awaiting owner approval |
119
+ | FR15 | With `header` or `footer`, the pointer popover holds the header, then the `role="menu"` element, then the footer, and the menu element holds only its rows. A region keeps its own keys and presses: neither the menu's key handling nor its press model claims an event that begins there. The menu is one Tab stop between the regions. Tab from the menu moves to the footer, and Shift+Tab to the header, when that region holds a control Tab can reach; otherwise Tab closes the menu as before. Tab past the footer's last control or Shift+Tab before the header's first closes the menu, and focus returns to the trigger. From a row of an open sub-menu, Tab and Shift+Tab close every open flyout and continue as they do from a row of the root menu. Escape closes from anywhere in the popover. The bottom sheet shows the header above the actions and the footer below them. Without either, the menu renders and behaves exactly as it does without the props. | DEC-8, docs, and tests | Proposed; awaiting owner approval |
117
120
 
118
121
  ### Allowed variation
119
122
 
@@ -261,21 +264,22 @@ than adding a DropdownMenu-owned heading target.
261
264
 
262
265
  ## Verification map
263
266
 
264
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
265
- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
266
- | FR1, FR4 | `DropdownMenu.test.tsx` pointer, bottom-sheet, adaptive, section, divider, and drill-in suites | Root pointer, nested pointer, root touch, drill-in | Collapsing presentation-specific parts or owners fails structure, target, or role assertions. | `audit:DropdownMenu/anatomy` |
267
- | FR2 | DropdownMenu target suites, source inspection, and theming target inventories | All six local targets | Removing, renaming, or assigning a current target to the wrong part fails evidence or inventory. | `audit:DropdownMenu/theming` |
268
- | FR3 | `DropdownMenuSelectable.test.tsx` plus BottomSheet, List, Indicator, Icon, and Divider owner tests | Trigger, touch actions, icons, checkbox, radio | A composed part loses its owner target or is documented as a new local target. | `audit:DropdownMenu/theming` |
269
- | Layer relationships | `DropdownMenu.test.tsx`, `DropdownMenuSubMenu.test.tsx`, and current layer/dismissal architecture records | Light dismiss, nested flyout, sheet dismissal | Documentation claims a shared owner where current source retains local behavior, or the reverse. | `audit:DropdownMenu/behavior` |
270
- | FR5 | `DropdownMenu.test.tsx` press model suite; the module record's own map | Finger slide across rows, outside release by mouse and finger, `touch-action`, root marker | A row acting on the press or the stray click, a lost `touch-action`, or a finger release that closes fails. | `audit:DropdownMenu/behavior` |
271
- | FR6 | `DropdownMenu.test.tsx` dismissal, closing and press model suites; `__tests__/MenuPress.a11y.chromium.spec.ts` in real Chromium | keyboard/pointer dismissal, Escape after press-open, press outside on a control, pointer pick, closed menu | Focus left on the body after a pointer pick, on the control focused before a press-open, or a ring after pointer input, fails. | `audit:DropdownMenu/behavior` |
272
- | FR7 | `menuMaxHeight` MUST replace the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | `component:DropdownMenu/DEC-3` | Proposed; verified in jsdom; owner to confirm DEC-3 |
273
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target inventory | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:DropdownMenu/theming` |
274
- | FR8 | `DropdownMenu.test.tsx` press model suite (trigger cases) | Press-open + drag-release, unsettled opening release, trigger toggle, held touch, tap | A menu that does not open on a mouse press, an unsettled release that acts, or a trigger that reopens in the same gesture fails. | `audit:DropdownMenu/behavior` |
275
- | FR9 | `DropdownMenu.test.tsx` custom trigger suite; `DropdownMenuSubMenu.test.tsx` custom-trigger Back row cases | Icon button, chip, list row; open, toggle, keyboard open, name; drill-in Back row | A rendered control missing `aria-haspopup`, `aria-expanded` or `aria-controls`, an unnamed menu, a lost keyboard open, or a "Back to Menu" row, fails. | `audit:DropdownMenu/anatomy` |
276
- | FR8, AR2 | `DropdownMenu.test.tsx` "DropdownMenuItem href" suite (pointer menu and bottom sheet) | Plain click, ⌘-click, Enter with modifiers, disabled link row | A row rendering an inner anchor, running `onClick` on a modified click, or a synthesized click dropping the modifiers fails. | `audit:DropdownMenu/behavior` |
277
- | FR12, AR3 | `DropdownMenuSubMenu.test.tsx` "drill-in on a phone" suite; `__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts` (real Chromium, coarse pointer) | Drill in, Back, Escape, ArrowLeft, nested level, scoped typeahead, forced presentations, close resets | A flyout opening on a coarse pointer, a missing Back row, sibling rows staying in the order, or focus not returning to the row fails. | `audit:DropdownMenu/behavior` |
278
- | FR10 | `DropdownMenuSubMenu.test.tsx` safe-triangle suite; `useMenuHover.test.tsx` `isPointInSafeTriangle` and click-guard suites | diagonal path toward the flyout, a pause inside it, path away, click-opened and hover-opened guard windows | A flyout that closes while the pointer is inside the triangle or paused in it, one that never closes after leaving it, or a changed guard-window rule, fails. | `audit:DropdownMenu/behavior` |
267
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
268
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
269
+ | FR1, FR4 | `DropdownMenu.test.tsx` pointer, bottom-sheet, adaptive, section, divider, and drill-in suites | Root pointer, nested pointer, root touch, drill-in | Collapsing presentation-specific parts or owners fails structure, target, or role assertions. | `audit:DropdownMenu/anatomy` |
270
+ | FR2 | DropdownMenu target suites, source inspection, and theming target inventories | All six local targets | Removing, renaming, or assigning a current target to the wrong part fails evidence or inventory. | `audit:DropdownMenu/theming` |
271
+ | FR3 | `DropdownMenuSelectable.test.tsx` plus BottomSheet, List, Indicator, Icon, and Divider owner tests | Trigger, touch actions, icons, checkbox, radio | A composed part loses its owner target or is documented as a new local target. | `audit:DropdownMenu/theming` |
272
+ | Layer relationships | `DropdownMenu.test.tsx`, `DropdownMenuSubMenu.test.tsx`, and current layer/dismissal architecture records | Light dismiss, nested flyout, sheet dismissal | Documentation claims a shared owner where current source retains local behavior, or the reverse. | `audit:DropdownMenu/behavior` |
273
+ | FR5 | `DropdownMenu.test.tsx` press model suite; the module record's own map | Finger slide across rows, outside release by mouse and finger, `touch-action`, root marker | A row acting on the press or the stray click, a lost `touch-action`, or a finger release that closes fails. | `audit:DropdownMenu/behavior` |
274
+ | FR6 | `DropdownMenu.test.tsx` dismissal, closing and press model suites; `__tests__/MenuPress.a11y.chromium.spec.ts` in real Chromium | keyboard/pointer dismissal, Escape after press-open, press outside on a control, pointer pick, closed menu | Focus left on the body after a pointer pick, on the control focused before a press-open, or a ring after pointer input, fails. | `audit:DropdownMenu/behavior` |
275
+ | FR7 | `menuMaxHeight` MUST replace the 300px term of the menu's and its popover viewport's block-size cap while the viewport gutters still bound both; the touch sheet ignores it. | `component:DropdownMenu/DEC-3` | Proposed; verified in jsdom; owner to confirm DEC-3 |
276
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target inventory | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:DropdownMenu/theming` |
277
+ | FR8 | `DropdownMenu.test.tsx` press model suite (trigger cases) | Press-open + drag-release, unsettled opening release, trigger toggle, held touch, tap | A menu that does not open on a mouse press, an unsettled release that acts, or a trigger that reopens in the same gesture fails. | `audit:DropdownMenu/behavior` |
278
+ | FR9 | `DropdownMenu.test.tsx` custom trigger suite; `DropdownMenuSubMenu.test.tsx` custom-trigger Back row cases | Icon button, chip, list row; open, toggle, keyboard open, name; drill-in Back row | A rendered control missing `aria-haspopup`, `aria-expanded` or `aria-controls`, an unnamed menu, a lost keyboard open, or a "Back to Menu" row, fails. | `audit:DropdownMenu/anatomy` |
279
+ | FR8, AR2 | `DropdownMenu.test.tsx` "DropdownMenuItem href" suite (pointer menu and bottom sheet) | Plain click, ⌘-click, Enter with modifiers, disabled link row | A row rendering an inner anchor, running `onClick` on a modified click, or a synthesized click dropping the modifiers fails. | `audit:DropdownMenu/behavior` |
280
+ | FR12, AR3 | `DropdownMenuSubMenu.test.tsx` "drill-in on a phone" suite; `__tests__/DropdownMenuDrillIn.a11y.chromium.spec.ts` (real Chromium, coarse pointer) | Drill in, Back, Escape, ArrowLeft, nested level, scoped typeahead, forced presentations, close resets | A flyout opening on a coarse pointer, a missing Back row, sibling rows staying in the order, or focus not returning to the row fails. | `audit:DropdownMenu/behavior` |
281
+ | FR10 | `DropdownMenuSubMenu.test.tsx` safe-triangle suite; `useMenuHover.test.tsx` `isPointInSafeTriangle` and click-guard suites | diagonal path toward the flyout, a pause inside it, path away, click-opened and hover-opened guard windows | A flyout that closes while the pointer is inside the triangle or paused in it, one that never closes after leaving it, or a changed guard-window rule, fails. | `audit:DropdownMenu/behavior` |
282
+ | FR15 | `DropdownMenu.test.tsx` header and footer suite; `__tests__/MenuHeaderFooter.a11y.chromium.spec.ts` in real Chromium | regions outside the menu element, typing in a header field, Tab walk header → menu → footer → close, Tab and Shift+Tab from an open sub-menu, Escape from the header, a finger tap on a footer button, bottom sheet order, no regions | A region rendered inside the menu element, a header key taken by typeahead, a Tab that leaves the menu open with focus outside it, or a footer tap swallowed by the press model fails. |
279
283
 
280
284
  ## Decision log
281
285
 
@@ -440,3 +444,24 @@ The value is a number of pixels. Rejected: an arbitrary CSS length (`50vh`,
440
444
  `calc(...)`), which is product-shaped tuning on a shared component that
441
445
  `spec:AST-002` does not admit, and which lets a caller write a cap the
442
446
  viewport term cannot reason about.
447
+
448
+ ### DEC-8 — Controls beside the rows go in `header` and `footer`
449
+
450
+ **Reference:** `component:DropdownMenu/DEC-8`
451
+ **Decider:** `cixzhang`, 2026-10-11
452
+
453
+ `role="menu"` admits only menu items, groups and separators. A filter field
454
+ or a "Create" button placed among the rows makes the menu invalid to a screen
455
+ reader, and makes the menu's key handling and press model fight the control.
456
+ The control belongs beside the menu, not in it. `header` and `footer` place it
457
+ there: in the same popover or sheet, outside the menu element. Outside the
458
+ menu element, the menu never claims the control's keys or presses. Because
459
+ `children` stay rows only, nothing can be placed between rows; the component
460
+ needs no inspection of its children to keep that true.
461
+
462
+ The menu becomes one Tab stop between the regions, so the popover reads as
463
+ one Tab sequence: header, menu, footer. Leaving it past either end closes the
464
+ menu, as Tab from the rows always has. Focus on open is unchanged. Rejected:
465
+ letting the menu's handlers step aside for controls placed among the rows —
466
+ that makes invalid structure work instead of fixing it; and a caller-wrapped
467
+ items element, which leaves "nothing between the rows" to convention.