@astryxdesign/core 0.7.2 → 0.7.3-canary.0bf23bd

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 (183) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  2. package/dist/BottomSheet/BottomSheet.js +8 -4
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
  5. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  6. package/dist/CheckboxInput/CheckboxInput.js +19 -24
  7. package/dist/ClickableCard/ClickableCard.d.ts +2 -2
  8. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  9. package/dist/ClickableCard/ClickableCard.js +11 -9
  10. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  11. package/dist/CodeBlock/CodeBlock.js +11 -1
  12. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  13. package/dist/ComplexSelector/ComplexSelector.js +23 -5
  14. package/dist/DateInput/DateInput.d.ts +4 -4
  15. package/dist/DateInput/DateInput.d.ts.map +1 -1
  16. package/dist/DateInput/DateInput.js +27 -20
  17. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  18. package/dist/DateInput/NativeDateField.js +14 -8
  19. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  20. package/dist/DateInput/TouchDateField.js +14 -10
  21. package/dist/Dialog/Dialog.d.ts.map +1 -1
  22. package/dist/Dialog/Dialog.js +9 -4
  23. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenu.js +52 -10
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
  26. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
  31. package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
  32. package/dist/DropdownMenu/menuRegionStops.js +2 -2
  33. package/dist/FileInput/FileInput.d.ts.map +1 -1
  34. package/dist/FileInput/FileInput.js +14 -12
  35. package/dist/Link/Link.d.ts +2 -3
  36. package/dist/Link/Link.d.ts.map +1 -1
  37. package/dist/Link/Link.js +11 -19
  38. package/dist/Markdown/Markdown.d.ts.map +1 -1
  39. package/dist/Markdown/Markdown.js +48 -15
  40. package/dist/MultiSelector/MultiSelector.d.ts +4 -4
  41. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  42. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
  43. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
  44. package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
  45. package/dist/MultiSelector/MultiSelector.js +25 -18
  46. package/dist/Popover/Popover.d.ts.map +1 -1
  47. package/dist/Popover/Popover.js +8 -4
  48. package/dist/Selector/Selector.d.ts +4 -4
  49. package/dist/Selector/Selector.d.ts.map +1 -1
  50. package/dist/Selector/Selector.js +23 -14
  51. package/dist/Selector/SelectorOption.d.ts.map +1 -1
  52. package/dist/Selector/SelectorOption.js +1 -0
  53. package/dist/Slider/Slider.d.ts.map +1 -1
  54. package/dist/Slider/Slider.js +7 -14
  55. package/dist/Switch/Switch.d.ts.map +1 -1
  56. package/dist/Switch/Switch.js +25 -17
  57. package/dist/Switch/switch.markers.stylex.d.ts +2 -0
  58. package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
  59. package/dist/Switch/switch.markers.stylex.js +6 -0
  60. package/dist/TimeInput/TimeInput.d.ts +2 -1
  61. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  62. package/dist/Token/Token.d.ts.map +1 -1
  63. package/dist/Token/Token.js +8 -3
  64. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  65. package/dist/Tooltip/Tooltip.js +36 -14
  66. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  67. package/dist/Tooltip/useTooltip.js +11 -2
  68. package/dist/TopNav/TopNav.d.ts.map +1 -1
  69. package/dist/TopNav/TopNav.js +2 -2
  70. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  71. package/dist/TopNav/TopNavHeading.js +1 -0
  72. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  73. package/dist/Typeahead/Typeahead.js +14 -2
  74. package/dist/astryx.css +8 -2
  75. package/dist/hooks/useMenuPress.d.ts +11 -1
  76. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  77. package/dist/hooks/useMenuPress.js +43 -5
  78. package/dist/utils/disabledInteraction.d.ts +21 -0
  79. package/dist/utils/disabledInteraction.d.ts.map +1 -0
  80. package/dist/utils/disabledInteraction.js +33 -0
  81. package/dist/utils/timeParser.d.ts +4 -0
  82. package/dist/utils/timeParser.d.ts.map +1 -1
  83. package/dist/utils/timeParser.js +9 -2
  84. package/package.json +4 -4
  85. package/scripts/agent-doc-state.mjs +35 -8
  86. package/src/Banner/Banner.spec.md +37 -32
  87. package/src/BottomSheet/BottomSheet.spec.md +13 -6
  88. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  89. package/src/BottomSheet/BottomSheet.tsx +8 -4
  90. package/src/BottomSheet/BottomSheetSwitcher.spec.md +121 -51
  91. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
  92. package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
  93. package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
  94. package/src/CheckboxInput/CheckboxInput.tsx +16 -26
  95. package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
  96. package/src/CheckboxList/CheckboxList.test.tsx +9 -6
  97. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  98. package/src/ClickableCard/ClickableCard.test.tsx +5 -4
  99. package/src/ClickableCard/ClickableCard.tsx +9 -9
  100. package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
  101. package/src/CodeBlock/CodeBlock.tsx +11 -1
  102. package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
  103. package/src/ComplexSelector/ComplexSelector.tsx +21 -4
  104. package/src/ContextMenu/ContextMenu.test.tsx +41 -0
  105. package/src/DateInput/DateInput.doc.mjs +9 -8
  106. package/src/DateInput/DateInput.test.tsx +88 -13
  107. package/src/DateInput/DateInput.tsx +49 -21
  108. package/src/DateInput/DateInputTouch.test.tsx +22 -1
  109. package/src/DateInput/NativeDateField.test.tsx +29 -3
  110. package/src/DateInput/NativeDateField.tsx +20 -7
  111. package/src/DateInput/TouchDateField.tsx +24 -9
  112. package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
  113. package/src/Dialog/Dialog.test.tsx +39 -0
  114. package/src/Dialog/Dialog.tsx +9 -4
  115. package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
  116. package/src/DropdownMenu/DropdownMenu.tsx +79 -8
  117. package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
  118. package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
  119. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
  120. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
  121. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
  122. package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
  123. package/src/DropdownMenu/menuRegionStops.ts +2 -2
  124. package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
  125. package/src/FieldStatus/FieldStatus.spec.md +3 -2
  126. package/src/FileInput/FileInput.test.tsx +30 -7
  127. package/src/FileInput/FileInput.tsx +21 -11
  128. package/src/Link/Link.test.tsx +21 -7
  129. package/src/Link/Link.tsx +16 -24
  130. package/src/Link/__tests__/Link.a11y.states.ts +1 -1
  131. package/src/Markdown/Markdown.rerender.test.tsx +143 -0
  132. package/src/Markdown/Markdown.tsx +89 -35
  133. package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
  134. package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
  135. package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
  136. package/src/MultiSelector/MultiSelector.test.tsx +49 -7
  137. package/src/MultiSelector/MultiSelector.tsx +53 -29
  138. package/src/Popover/Popover.test.tsx +15 -0
  139. package/src/Popover/Popover.tsx +19 -8
  140. package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
  141. package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
  142. package/src/Selector/Selector.doc.mjs +4 -4
  143. package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
  144. package/src/Selector/Selector.spec.md +11 -11
  145. package/src/Selector/Selector.test.tsx +64 -6
  146. package/src/Selector/Selector.tsx +51 -21
  147. package/src/Selector/SelectorOption.tsx +4 -0
  148. package/src/SideNav/SideNav.test.tsx +35 -0
  149. package/src/Slider/Slider.spec.md +11 -11
  150. package/src/Slider/Slider.test.tsx +3 -3
  151. package/src/Slider/Slider.tsx +8 -15
  152. package/src/Switch/Switch.test.tsx +85 -8
  153. package/src/Switch/Switch.tsx +26 -16
  154. package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
  155. package/src/Switch/switch.markers.stylex.ts +4 -0
  156. package/src/Table/Table.spec.md +2 -0
  157. package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
  158. package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
  159. package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
  160. package/src/TimeInput/TimeInput.doc.mjs +5 -3
  161. package/src/TimeInput/TimeInput.test.tsx +20 -0
  162. package/src/TimeInput/TimeInput.tsx +2 -1
  163. package/src/Token/Token.test.tsx +26 -4
  164. package/src/Token/Token.tsx +6 -2
  165. package/src/Tokenizer/Tokenizer.spec.md +28 -28
  166. package/src/Tooltip/Tooltip.test.tsx +51 -6
  167. package/src/Tooltip/Tooltip.tsx +41 -18
  168. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
  169. package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
  170. package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
  171. package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
  172. package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
  173. package/src/Tooltip/useTooltip.test.tsx +23 -0
  174. package/src/Tooltip/useTooltip.tsx +22 -9
  175. package/src/TopNav/TopNav.tsx +11 -1
  176. package/src/TopNav/TopNavHeading.tsx +1 -0
  177. package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
  178. package/src/Typeahead/Typeahead.test.tsx +89 -0
  179. package/src/Typeahead/Typeahead.tsx +15 -2
  180. package/src/hooks/useMenuPress.ts +45 -7
  181. package/src/utils/disabledInteraction.ts +40 -0
  182. package/src/utils/timeParser.test.ts +11 -0
  183. package/src/utils/timeParser.ts +9 -2
@@ -9,7 +9,9 @@
9
9
  * Spinner, and DropdownMenu context + item roles
10
10
  * @output Exports DropdownMenuSubMenu — a single menu row that reveals a nested
11
11
  * menu of its own children/items: a flyout beside the row, or, on a phone,
12
- * a drilled-in view in place of the parent rows.
12
+ * a drilled-in view in place of the parent rows. With a header or footer,
13
+ * that surface is a modal dialog rendered into the root's surface host,
14
+ * beside every menu element.
13
15
  * @position Sub-component; place inside a DropdownMenu (or ContextMenu)
14
16
  * alongside plain items.
15
17
  *
@@ -65,12 +67,17 @@ import * as stylex from '@stylexjs/stylex';
65
67
  import {Icon, renderIconSlot, type IconType} from '../Icon';
66
68
  import {Spinner} from '../Spinner';
67
69
  import {MenuRegionsContext} from './menuRegionsContext';
70
+ import {moveRegionsTab} from './menuRegionStops';
68
71
  import {Item} from '../Item';
69
72
  import {useLayer} from '../Layer/useLayer';
70
73
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
71
74
  import {useListFocus} from '../hooks/useListFocus';
72
75
  import {useMenuHover} from '../hooks/useMenuHover';
73
- import {isMenuPressActivation, useMenuPress} from '../hooks/useMenuPress';
76
+ import {
77
+ isMenuPressActivation,
78
+ MENU_PRESS_SURFACE_ATTR,
79
+ useMenuPress,
80
+ } from '../hooks/useMenuPress';
74
81
  import {useTypeahead} from '../hooks/useTypeahead';
75
82
  import {useMenuOverflow} from './useMenuOverflow';
76
83
  import {resolveMenuWidth} from './menuWidth';
@@ -85,8 +92,17 @@ import {
85
92
  typographyVars,
86
93
  typeScaleVars,
87
94
  } from '../theme/tokens.stylex';
88
- import {composeEventHandlers, mergeProps, rtlStyles} from '../utils';
95
+ import {
96
+ composeEventHandlers,
97
+ isImeKeyEvent,
98
+ isRenderable,
99
+ mergeProps,
100
+ rtlStyles,
101
+ } from '../utils';
102
+ import {focusOutlineStyles} from '../utils/focusOutline.stylex';
103
+ import {getInteractionModality} from '../utils/interactionModality';
89
104
  import {useMergedRefs} from '../hooks/useMergedRefs';
105
+ import {useDevWarning} from '../hooks/useDevWarning';
90
106
  import {themeProps} from '../utils/themeProps';
91
107
  import type {BaseProps} from '../BaseProps';
92
108
  import {
@@ -98,6 +114,7 @@ import {
98
114
  import {
99
115
  DropdownMenuContext,
100
116
  DropdownMenuRootOpenContext,
117
+ DropdownMenuSurfaceContext,
101
118
  useDropdownMenuContext,
102
119
  useDropdownMenuRootOpen,
103
120
  type DropdownMenuContextValue,
@@ -244,6 +261,70 @@ const drillInStyles = stylex.create({
244
261
  },
245
262
  });
246
263
 
264
+ // With a header or footer the sub-menu's surface is a frame holding the
265
+ // header, the sub-menu's own role="menu" element and the footer, in one column
266
+ // under the flyout's block cap: the regions keep their height and the rows
267
+ // shrink and scroll between them.
268
+ // A drilled-in view with a header or footer takes the root menu's place, so
269
+ // it caps where the root does when the root's `menuMaxHeight` lifts the 300px
270
+ // cap; the viewport still bounds it.
271
+ const dynamicRegionStyles = stylex.create({
272
+ viewMaxHeight: (maxHeight: string) => ({
273
+ maxHeight: `min(${maxHeight}, ${layerViewportInset.maxBlockSize})`,
274
+ }),
275
+ });
276
+
277
+ const regionStyles = stylex.create({
278
+ frame: {
279
+ boxSizing: 'border-box',
280
+ display: 'flex',
281
+ flexDirection: 'column',
282
+ maxHeight: stylex.firstThatWorks(
283
+ MENU_MAX_BLOCK_SIZE,
284
+ MENU_MAX_BLOCK_SIZE_FALLBACK,
285
+ ),
286
+ '--_dropdown-menu-radius': radiusVars['--radius-container'],
287
+ '--_dropdown-menu-padding': spacingVars['--spacing-1'],
288
+ borderRadius: 'var(--_dropdown-menu-radius)',
289
+ },
290
+ // The flyout paints its own surface; a drilled-in view sits inside the
291
+ // root's.
292
+ flyoutSurface: {
293
+ // Each constant inside calc(), as for the plain flyout: a bare pair would
294
+ // ship as var(--<hash>, <fallback>) with the hash defined nowhere.
295
+ maxInlineSize: stylex.firstThatWorks(
296
+ `calc(${layerViewportInset.maxInlineSize})`,
297
+ `calc(${layerViewportInset.maxInlineSizeFallback})`,
298
+ ),
299
+ backgroundColor: colorVars['--color-background-popover'],
300
+ boxShadow: shadowVars['--shadow-low'],
301
+ opacity: 1,
302
+ transitionProperty: 'opacity',
303
+ transitionDuration: durationVars['--duration-fast'],
304
+ transitionTimingFunction: easeVars['--ease-standard'],
305
+ },
306
+ menu: {
307
+ boxSizing: 'border-box',
308
+ display: 'flex',
309
+ flexDirection: 'column',
310
+ gap: spacingVars['--spacing-0-5'],
311
+ flexGrow: 1,
312
+ flexShrink: 1,
313
+ minBlockSize: 0,
314
+ padding: spacingVars['--spacing-1'],
315
+ outline: 'none',
316
+ WebkitTouchCallout: 'none',
317
+ userSelect: 'none',
318
+ },
319
+ region: {
320
+ flexShrink: 0,
321
+ padding: spacingVars['--spacing-1'],
322
+ },
323
+ hidden: {
324
+ display: 'none',
325
+ },
326
+ });
327
+
247
328
  export type DropdownMenuSubMenuPresentation =
248
329
  'flyout' | 'drill-in' | 'adaptive';
249
330
 
@@ -285,13 +366,30 @@ interface DropdownMenuSubMenuBaseProps extends Omit<
285
366
  * 'adaptive'
286
367
  */
287
368
  presentation?: DropdownMenuSubMenuPresentation;
369
+ /**
370
+ * Content above the sub-menu's rows: a field that filters them, say. Like
371
+ * the root menu's `header`, it sits beside the sub-menu's `role="menu"`
372
+ * element, never inside it, and keeps its own keys and presses. With a
373
+ * header or footer the sub-menu's surface (the flyout, or the drilled-in
374
+ * view on a phone) is a modal dialog named by the row: the row says
375
+ * `aria-haspopup="dialog"`, Tab and Shift+Tab walk the header's controls,
376
+ * the rows as one stop and the footer's controls and wrap, and Escape
377
+ * returns to the row. A pointer leaving the flyout does not close it while
378
+ * focus is inside. A `ContextMenu` does not show it.
379
+ */
380
+ header?: ReactNode;
381
+ /**
382
+ * Content below the sub-menu's rows: a door to where the options are
383
+ * managed, say. Same rules as `header`.
384
+ */
385
+ footer?: ReactNode;
288
386
  /** Test id for the trigger row. */
289
387
  'data-testid'?: string;
290
388
  /** Test id for the flyout menu. */
291
389
  menuDataTestId?: string;
292
390
  /**
293
391
  * Ref forwarded to the trigger row — the element carrying `role="menuitem"`
294
- * and `aria-haspopup="menu"`.
392
+ * and `aria-haspopup="menu"` (`"dialog"` with a header or footer).
295
393
  */
296
394
  ref?: React.Ref<HTMLElement>;
297
395
  }
@@ -309,6 +407,49 @@ export interface DropdownMenuSubMenuProps extends DropdownMenuSubMenuBaseProps {
309
407
  children: ReactNode;
310
408
  }
311
409
 
410
+ /**
411
+ * Close every open flyout with a header or footer whose row sits in `menu`.
412
+ * Such a flyout renders beside the root's menu, not inside the flyout that
413
+ * holds its row, so hiding that flyout does not hide it. Each closes through
414
+ * the popover API, as DropdownMenu closes its flyouts when it hides, so its
415
+ * layer's toggle handling resets its state and reports the close; its own
416
+ * nested flyouts close the same way, innermost first.
417
+ */
418
+ function hideRegionedFlyoutsIn(menu: HTMLElement | null): void {
419
+ if (menu == null) {
420
+ return;
421
+ }
422
+ const doc = menu.ownerDocument;
423
+ const rows = menu.querySelectorAll<HTMLElement>(
424
+ '[aria-expanded="true"][aria-controls]',
425
+ );
426
+ for (const row of [...rows].reverse()) {
427
+ const frame = doc.getElementById(row.getAttribute('aria-controls') ?? '');
428
+ if (frame?.getAttribute('data-astryx-submenu-regions') !== 'flyout') {
429
+ continue;
430
+ }
431
+ const flyout = frame.closest<HTMLElement>('[popover]');
432
+ if (flyout == null) {
433
+ continue;
434
+ }
435
+ if (typeof flyout.hidePopover === 'function') {
436
+ if (flyout.matches(':popover-open')) {
437
+ flyout.hidePopover();
438
+ }
439
+ } else if (flyout.style.display === 'block') {
440
+ // Without the popover API the layer toggles `display`; mirror it and
441
+ // tell its reconciler, as DropdownMenu's own sweep does.
442
+ flyout.style.display = 'none';
443
+ const toggle = new Event('toggle', {bubbles: false});
444
+ Object.defineProperty(toggle, 'newState', {
445
+ value: 'closed',
446
+ configurable: true,
447
+ });
448
+ flyout.dispatchEvent(toggle);
449
+ }
450
+ }
451
+ }
452
+
312
453
  /**
313
454
  * A single menu row that reveals a nested flyout of its own items. The row
314
455
  * adopts DropdownMenuItem semantics (label / icon / description / isDisabled);
@@ -341,6 +482,8 @@ export function DropdownMenuSubMenu(
341
482
  menuWidth,
342
483
  onOpenChange,
343
484
  presentation = 'adaptive',
485
+ header,
486
+ footer,
344
487
  children,
345
488
  xstyle,
346
489
  className,
@@ -360,9 +503,29 @@ export function DropdownMenuSubMenu(
360
503
 
361
504
  const t = useTranslator();
362
505
  const menuCtx = useDropdownMenuContext();
506
+ const rootSurface = use(DropdownMenuSurfaceContext);
363
507
  const rootHasRegions = use(MenuRegionsContext);
364
508
  const menuSize = menuCtx?.menuSize ?? 'md';
365
509
  const canOpen = !isDisabled;
510
+ // With a header or footer the sub-menu's surface holds controls that are not
511
+ // menu items, so it is a modal dialog around the sub-menu's menu, rendered
512
+ // into the root's surface host: outside every menu element. A root that
513
+ // renders no host (no surface context, as in ContextMenu) has nowhere
514
+ // outside its menu to put them, so the sub-menu shows its rows alone there.
515
+ const wantsRegions = isRenderable(header) || isRenderable(footer);
516
+ const rootHostsSurfaces = rootSurface != null;
517
+ const hasRegions = wantsRegions && rootHostsSurfaces;
518
+ const surfaceHost = rootSurface?.host.getElement() ?? null;
519
+ useDevWarning(
520
+ 'DropdownMenuSubMenu',
521
+ 'header and footer show only inside a DropdownMenu; this menu has no place outside its rows for them, so they are not rendered.',
522
+ wantsRegions && !rootHostsSurfaces,
523
+ );
524
+ const frameId = useId();
525
+ const frameRef = useRef<HTMLDivElement | null>(null);
526
+ const headerRef = useRef<HTMLDivElement | null>(null);
527
+ const footerRef = useRef<HTMLDivElement | null>(null);
528
+ const [isMenuKeyboardFocused, setIsMenuKeyboardFocused] = useState(false);
366
529
 
367
530
  const contentId = useId();
368
531
  const generatedTriggerId = useId();
@@ -394,17 +557,32 @@ export function DropdownMenuSubMenu(
394
557
  const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
395
558
  const isOpen = isDrillIn ? isOnStack : isFlyoutOpen;
396
559
 
560
+ // Whether focus is anywhere in this flyout's frame, a level nested in it
561
+ // included: React brings a nested level's focus events back here through
562
+ // its portal, where the DOM would not.
563
+ const isFocusInFrameRef = useRef(false);
564
+ // A pointer that left while focus was inside: the flyout closes once focus
565
+ // leaves too.
566
+ const closeWhenFocusLeavesRef = useRef(false);
397
567
  const layer = useLayer({
398
568
  mode: 'context',
399
569
  lightDismiss: false,
400
570
  onShow: useCallback(() => {
571
+ closeWhenFocusLeavesRef.current = false;
401
572
  setIsFlyoutOpen(true);
402
573
  onOpenChange?.(true);
403
574
  }, [onOpenChange]),
404
575
  onHide: useCallback(() => {
576
+ closeWhenFocusLeavesRef.current = false;
577
+ // Whatever closed this flyout, a flyout with a header or footer opened
578
+ // from one of its rows closes with it: that flyout renders beside the
579
+ // root's menu, not inside this one, so hiding this one leaves it shown.
580
+ hideRegionedFlyoutsIn(
581
+ triggerRef.current?.ownerDocument.getElementById(contentId) ?? null,
582
+ );
405
583
  setIsFlyoutOpen(false);
406
584
  onOpenChange?.(false);
407
- }, [onOpenChange]),
585
+ }, [onOpenChange, contentId]),
408
586
  });
409
587
 
410
588
  const showLayer = useCallback(() => {
@@ -413,8 +591,49 @@ export function DropdownMenuSubMenu(
413
591
  }
414
592
  }, [canOpen, layer]);
415
593
  const hideLayer = useCallback(() => {
594
+ // A pointer leaving a flyout with a header or footer does not close it
595
+ // while focus is inside: what was typed into its field stays.
596
+ if (hasRegions && isFocusInFrameRef.current) {
597
+ closeWhenFocusLeavesRef.current = true;
598
+ return;
599
+ }
416
600
  layer.hide();
417
- }, [layer]);
601
+ }, [layer, hasRegions]);
602
+ const handleFrameFocus = useCallback(() => {
603
+ isFocusInFrameRef.current = true;
604
+ }, []);
605
+ const handleFrameBlur = useCallback(
606
+ (event: React.FocusEvent<HTMLElement>) => {
607
+ isFocusInFrameRef.current = false;
608
+ if (!closeWhenFocusLeavesRef.current) {
609
+ return;
610
+ }
611
+ // Focus moving to another control in the frame keeps it open.
612
+ const next = event.relatedTarget;
613
+ if (next instanceof Node && event.currentTarget.contains(next)) {
614
+ return;
615
+ }
616
+ // Focus moving into a level nested in this one (React brings that
617
+ // focus event back through its portal), or moved by the browser itself
618
+ // (an arrow key in a native radio group, whose focus event follows a
619
+ // microtask checkpoint), arrives after this blur: decide in a task of
620
+ // its own, once the move has completed.
621
+ setTimeout(() => {
622
+ if (
623
+ !isFocusInFrameRef.current &&
624
+ closeWhenFocusLeavesRef.current &&
625
+ triggerRef.current?.isConnected === true
626
+ ) {
627
+ closeWhenFocusLeavesRef.current = false;
628
+ layer.hide();
629
+ }
630
+ }, 0);
631
+ },
632
+ [layer],
633
+ );
634
+ const handleFramePointerEnter = useCallback(() => {
635
+ closeWhenFocusLeavesRef.current = false;
636
+ }, []);
418
637
 
419
638
  // Dedicated roving-focus + typeahead for this flyout level. The boundary
420
639
  // selector scopes item collection and key handling to this flyout's own
@@ -435,8 +654,13 @@ export function DropdownMenuSubMenu(
435
654
  hasPaging: true,
436
655
  onEscape: () => close({focusTrigger: true}),
437
656
  });
438
- // A drilled-in list scrolls with the root menu, not on its own.
439
- const hasOverflow = useMenuOverflow(menuRef, children, isOpen && !isDrillIn);
657
+ // A drilled-in list scrolls with the root menu, not on its own, unless it
658
+ // has a header or footer: then its rows scroll between them.
659
+ const hasOverflow = useMenuOverflow(
660
+ menuRef,
661
+ children,
662
+ isOpen && (!isDrillIn || hasRegions),
663
+ );
440
664
 
441
665
  const typeahead = useTypeahead({
442
666
  getItemLabels: () => getItems().map(el => el.textContent),
@@ -465,10 +689,10 @@ export function DropdownMenuSubMenu(
465
689
  isOpen: isFlyoutOpen,
466
690
  isEnabled: canOpen && !isDrillIn && isRootMenuOpen,
467
691
  // The safe triangle toward the flyout's near edge is built from the
468
- // flyout this component renders, not the hook's own list ref.
469
- flyoutRef: menuRef,
692
+ // flyout this component renders, not the hook's own list ref: the
693
+ // frame, with a header or footer.
694
+ flyoutRef: hasRegions ? frameRef : menuRef,
470
695
  });
471
-
472
696
  // Where focus lands once a pushed view shows: the first row after Back for
473
697
  // a keyboard open, the list itself for a pointer open; and the row itself
474
698
  // once its view is popped.
@@ -664,12 +888,29 @@ export function DropdownMenuSubMenu(
664
888
  // stopPropagation needed — each level self-filters.
665
889
  const handleContentKeyDown = useCallback(
666
890
  (e: React.KeyboardEvent) => {
667
- // In a menu with a header or footer, Tab leaves the whole flyout stack:
668
- // every open level closes as the key bubbles past it — this one's own
669
- // rows or a deeper level's — and the root menu moves focus to a region.
670
- if (e.key === 'Tab' && rootHasRegions) {
671
- close({focusTrigger: false});
672
- return;
891
+ if (e.key === 'Tab') {
892
+ // With its own header or footer this level is a dialog: Tab walks
893
+ // its regions and its rows as one stop, and wraps. A deeper level
894
+ // has closed itself as the key bubbled up, and the walk continues
895
+ // from this level's rows.
896
+ if (hasRegions) {
897
+ moveRegionsTab(
898
+ e,
899
+ menuRef.current,
900
+ headerRef.current,
901
+ footerRef.current,
902
+ !ownsEvent(e),
903
+ );
904
+ return;
905
+ }
906
+ // In a menu with a header or footer above this level, Tab leaves the
907
+ // flyout stack: every open level closes as the key bubbles past it,
908
+ // this one's own rows or a deeper level's, and the level with the
909
+ // regions moves focus on.
910
+ if (rootHasRegions) {
911
+ close({focusTrigger: false});
912
+ return;
913
+ }
673
914
  }
674
915
  if (!ownsEvent(e)) {
675
916
  return;
@@ -718,7 +959,46 @@ export function DropdownMenuSubMenu(
718
959
  }
719
960
  listNavKeyDown(e);
720
961
  },
721
- [ownsEvent, close, listNavKeyDown, typeahead, menuRef, rootHasRegions],
962
+ [
963
+ ownsEvent,
964
+ close,
965
+ listNavKeyDown,
966
+ typeahead,
967
+ menuRef,
968
+ rootHasRegions,
969
+ hasRegions,
970
+ ],
971
+ );
972
+
973
+ // A region keeps its own keys: arrows move a field's caret and letters type.
974
+ // Tab moves on through the dialog's stops, and Escape returns to the row as
975
+ // it does from the rows.
976
+ const handleRegionKeyDown = useCallback(
977
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
978
+ if (e.key === 'Tab') {
979
+ moveRegionsTab(
980
+ e,
981
+ menuRef.current,
982
+ headerRef.current,
983
+ footerRef.current,
984
+ );
985
+ return;
986
+ }
987
+ // An Escape that ends an IME composition belongs to the field, and one a
988
+ // control in the region already handled (a menu it opened closing, a
989
+ // field clearing itself) belongs to that control: either closes nothing
990
+ // here, so one Escape affects one surface.
991
+ if (
992
+ e.key === 'Escape' &&
993
+ !e.defaultPrevented &&
994
+ !isImeKeyEvent(e.nativeEvent)
995
+ ) {
996
+ e.preventDefault();
997
+ e.stopPropagation();
998
+ close({focusTrigger: true});
999
+ }
1000
+ },
1001
+ [menuRef, close],
722
1002
  );
723
1003
 
724
1004
  // Re-provide the menu context so nested items behave exactly like top-level
@@ -776,35 +1056,133 @@ export function DropdownMenuSubMenu(
776
1056
  : flyoutStyles.popoverCustomWidth(resolvedMenuWidth.value)
777
1057
  : flyoutStyles.popover;
778
1058
 
1059
+ // The flyout is a layer of its own, where layer-scoped contexts reset:
1060
+ // nested rows get this level's menu context and the root's surface again.
779
1061
  const nestedRows = (
780
1062
  <DropdownMenuContext value={nestedMenuContext}>
781
- {isDrillIn && (
782
- <DropdownMenuItem
783
- icon={
784
- <Icon
785
- icon="chevronLeft"
786
- size="sm"
787
- color="secondary"
788
- xstyle={rtlStyles.mirror}
789
- />
790
- }
791
- label={backLabel}
792
- hasCloseOnSelect={false}
793
- onClick={() => close({focusTrigger: true})}
794
- />
795
- )}
796
- {children}
1063
+ <DropdownMenuSurfaceContext value={rootSurface}>
1064
+ {isDrillIn && (
1065
+ <DropdownMenuItem
1066
+ icon={
1067
+ <Icon
1068
+ icon="chevronLeft"
1069
+ size="sm"
1070
+ color="secondary"
1071
+ xstyle={rtlStyles.mirror}
1072
+ />
1073
+ }
1074
+ label={backLabel}
1075
+ hasCloseOnSelect={false}
1076
+ onClick={() => close({focusTrigger: true})}
1077
+ />
1078
+ )}
1079
+ {children}
1080
+ </DropdownMenuSurfaceContext>
797
1081
  </DropdownMenuContext>
798
1082
  );
1083
+ // A level with its own header or footer tells the levels below it, so Tab
1084
+ // from a deeper flyout closes that flyout and this level walks on.
1085
+ //
799
1086
  // Nested rows live in this level's flyout, a layer of their own where the
800
- // layer-scoped signal resets; re-provide it so their pending hover intent
801
- // dies with the root menu too.
1087
+ // layer-scoped signal resets; re-provide it, as whether every level above
1088
+ // them is open, so their pending hover intent dies with the root menu or
1089
+ // with this level: a hover-open that fired after this level closed would
1090
+ // leave a flyout with a header or footer shown beside the root's menu.
802
1091
  const nestedContent = (
803
- <DropdownMenuRootOpenContext value={isRootMenuOpen}>
804
- {nestedRows}
1092
+ <DropdownMenuRootOpenContext value={isRootMenuOpen && isOpen}>
1093
+ {hasRegions ? (
1094
+ <MenuRegionsContext value={true}>{nestedRows}</MenuRegionsContext>
1095
+ ) : (
1096
+ nestedRows
1097
+ )}
805
1098
  </DropdownMenuRootOpenContext>
806
1099
  );
807
1100
 
1101
+ // The menu of a sub-menu with a header or footer renders beside the root's
1102
+ // menu, outside its DOM: it names the root so the press model counts it as
1103
+ // nested there, as a flyout inside the root's menu is. Only this menu does:
1104
+ // a picker placed in its header or footer stays its own press root.
1105
+ const surfaceOwnerProps =
1106
+ hasRegions && rootSurface != null
1107
+ ? {[MENU_PRESS_SURFACE_ATTR]: rootSurface.ownerId}
1108
+ : undefined;
1109
+
1110
+ // With a header or footer, the dialog that holds them and the sub-menu's
1111
+ // menu. A drilled-in view is named as its list is, by the row's label; the
1112
+ // flyout by the row.
1113
+ const renderRegionsFrame = (
1114
+ menuElement: ReactNode,
1115
+ frameProps: React.HTMLAttributes<HTMLDivElement>,
1116
+ surface: 'flyout' | 'view',
1117
+ ): ReactNode => (
1118
+ <div
1119
+ ref={frameRef}
1120
+ id={frameId}
1121
+ role="dialog"
1122
+ aria-modal="true"
1123
+ // Read by the root's step-aside rule (a shown view) and by the flyout
1124
+ // that holds this row when it closes (an open flyout).
1125
+ data-astryx-submenu-regions={surface}
1126
+ {...frameProps}
1127
+ {...(surface === 'flyout'
1128
+ ? {
1129
+ onFocus: handleFrameFocus,
1130
+ onBlur: handleFrameBlur,
1131
+ onPointerEnter: handleFramePointerEnter,
1132
+ }
1133
+ : undefined)}
1134
+ {...(surface === 'flyout'
1135
+ ? // The flyout's frame paints its surface, so the menu target rides
1136
+ // on it; a drilled-in view sits inside the root's surface.
1137
+ mergeProps(
1138
+ themeProps('dropdown-menu'),
1139
+ stylex.props(regionStyles.frame, regionStyles.flyoutSurface),
1140
+ )
1141
+ : stylex.props(
1142
+ regionStyles.frame,
1143
+ rootSurface?.menuMaxHeight != null &&
1144
+ dynamicRegionStyles.viewMaxHeight(rootSurface.menuMaxHeight),
1145
+ frameProps.hidden === true && regionStyles.hidden,
1146
+ ))}>
1147
+ {isRenderable(header) && (
1148
+ <div
1149
+ ref={headerRef}
1150
+ onKeyDown={handleRegionKeyDown}
1151
+ {...stylex.props(regionStyles.region)}>
1152
+ {header}
1153
+ </div>
1154
+ )}
1155
+ {menuElement}
1156
+ {isRenderable(footer) && (
1157
+ <div
1158
+ ref={footerRef}
1159
+ onKeyDown={handleRegionKeyDown}
1160
+ {...stylex.props(regionStyles.region)}>
1161
+ {footer}
1162
+ </div>
1163
+ )}
1164
+ </div>
1165
+ );
1166
+ // A flyout with a header or footer renders into the root's surface host,
1167
+ // beside the root's menu rather than inside it; any other flyout renders
1168
+ // in place, inside the menu that holds its row.
1169
+ const renderFlyout = (flyout: ReactNode): ReactNode =>
1170
+ hasRegions && surfaceHost != null
1171
+ ? createPortal(flyout, surfaceHost)
1172
+ : flyout;
1173
+ // Between a header and a footer the rows are one Tab stop, so keyboard focus
1174
+ // on the menu element shows the ring the base style otherwise removes.
1175
+ const handleMenuFocus = (event: React.FocusEvent<HTMLDivElement>) => {
1176
+ if (hasRegions && event.target === event.currentTarget) {
1177
+ setIsMenuKeyboardFocused(getInteractionModality() === 'keyboard');
1178
+ }
1179
+ };
1180
+ const handleMenuBlur = (event: React.FocusEvent<HTMLDivElement>) => {
1181
+ if (hasRegions && event.target === event.currentTarget) {
1182
+ setIsMenuKeyboardFocused(false);
1183
+ }
1184
+ };
1185
+
808
1186
  return (
809
1187
  <>
810
1188
  <Item
@@ -816,9 +1194,9 @@ export function DropdownMenuSubMenu(
816
1194
  id={triggerId}
817
1195
  role="menuitem"
818
1196
  tabIndex={isDisabled ? undefined : -1}
819
- aria-haspopup="menu"
1197
+ aria-haspopup={hasRegions ? 'dialog' : 'menu'}
820
1198
  aria-expanded={isOpen}
821
- aria-controls={isOpen ? contentId : undefined}
1199
+ aria-controls={isOpen ? (hasRegions ? frameId : contentId) : undefined}
822
1200
  aria-disabled={isDisabled || undefined}
823
1201
  data-testid={testId}
824
1202
  onMouseEnter={composeEventHandlers(
@@ -856,66 +1234,145 @@ export function DropdownMenuSubMenu(
856
1234
  {activeDrillIn != null
857
1235
  ? isOnStack &&
858
1236
  activeDrillIn.host != null &&
859
- createPortal(
860
- <div
861
- ref={menuRef}
862
- id={contentId}
863
- role="menu"
864
- tabIndex={-1}
865
- // The drilled-in list is named by its row's label.
866
- aria-label={stringLabel}
867
- aria-labelledby={stringLabel == null ? triggerId : undefined}
868
- // A deeper view shows in its place; this one waits, hidden.
869
- hidden={!isTopView}
870
- inert={!isTopView}
871
- onKeyDown={handleContentKeyDown}
872
- data-testid={menuDataTestId}
873
- {...menuPress.menuProps}
874
- {...stylex.props(
875
- drillInStyles.list,
876
- !isTopView && drillInStyles.hidden,
877
- )}>
878
- {nestedContent}
879
- </div>,
880
- activeDrillIn.host,
881
- )
882
- : layer.render(
883
- <div
884
- ref={menuRef}
885
- id={contentId}
886
- role="menu"
887
- // Focusable as a fallback target so an empty/loading flyout can own
888
- // arrow/Escape keys. An overflowing flyout joins the Tab order so its
889
- // scrollable region is keyboard-accessible.
890
- tabIndex={hasOverflow ? 0 : -1}
891
- aria-labelledby={triggerId}
892
- onKeyDown={handleContentKeyDown}
893
- onMouseEnter={contentProps.onMouseEnter}
894
- onMouseLeave={contentProps.onMouseLeave}
895
- data-testid={menuDataTestId}
896
- {...menuPress.menuProps}
897
- {...mergeProps(
898
- themeProps('dropdown-menu'),
899
- stylex.props(
900
- flyoutStyles.menu,
901
- hasOverflow
902
- ? flyoutStyles.touchPanYPinchZoom
903
- : flyoutStyles.touchPinchZoom,
904
- hasOverflow && flyoutStyles.scrollable,
1237
+ (hasRegions
1238
+ ? // A view with a header or footer renders beside the root's menu,
1239
+ // in its surface host, while the root steps its menu aside.
1240
+ createPortal(
1241
+ renderRegionsFrame(
1242
+ <div
1243
+ ref={menuRef}
1244
+ id={contentId}
1245
+ role="menu"
1246
+ tabIndex={0}
1247
+ aria-label={stringLabel}
1248
+ aria-labelledby={
1249
+ stringLabel == null ? triggerId : undefined
1250
+ }
1251
+ onKeyDown={handleContentKeyDown}
1252
+ onFocus={handleMenuFocus}
1253
+ onBlur={handleMenuBlur}
1254
+ data-testid={menuDataTestId}
1255
+ {...menuPress.menuProps}
1256
+ {...surfaceOwnerProps}
1257
+ {...stylex.props(
1258
+ regionStyles.menu,
1259
+ hasOverflow && flyoutStyles.scrollable,
1260
+ hasOverflow
1261
+ ? flyoutStyles.touchPanYPinchZoom
1262
+ : flyoutStyles.touchPinchZoom,
1263
+ isMenuKeyboardFocused && focusOutlineStyles.focusVisible,
1264
+ )}>
1265
+ {nestedContent}
1266
+ </div>,
1267
+ {
1268
+ 'aria-label': stringLabel,
1269
+ 'aria-labelledby':
1270
+ stringLabel == null ? triggerId : undefined,
1271
+ // A deeper view shows in its place; this one waits, hidden.
1272
+ hidden: !isTopView,
1273
+ inert: !isTopView,
1274
+ },
1275
+ 'view',
905
1276
  ),
906
- )}>
907
- {nestedContent}
908
- </div>,
909
- {
910
- placement: 'end',
911
- alignment: 'start',
912
- offset: spacingVars['--spacing-1'],
913
- xstyle: [
914
- flyoutStyles.popoverViewport,
915
- popoverXstyle,
916
- layerAnimations.end,
917
- ],
918
- },
1277
+ surfaceHost ?? activeDrillIn.host,
1278
+ )
1279
+ : createPortal(
1280
+ <div
1281
+ ref={menuRef}
1282
+ id={contentId}
1283
+ role="menu"
1284
+ tabIndex={-1}
1285
+ // The drilled-in list is named by its row's label.
1286
+ aria-label={stringLabel}
1287
+ aria-labelledby={stringLabel == null ? triggerId : undefined}
1288
+ // A deeper view shows in its place; this one waits, hidden.
1289
+ hidden={!isTopView}
1290
+ inert={!isTopView}
1291
+ onKeyDown={handleContentKeyDown}
1292
+ data-testid={menuDataTestId}
1293
+ {...menuPress.menuProps}
1294
+ {...stylex.props(
1295
+ drillInStyles.list,
1296
+ !isTopView && drillInStyles.hidden,
1297
+ )}>
1298
+ {nestedContent}
1299
+ </div>,
1300
+ activeDrillIn.host,
1301
+ ))
1302
+ : renderFlyout(
1303
+ layer.render(
1304
+ hasRegions ? (
1305
+ renderRegionsFrame(
1306
+ <div
1307
+ ref={menuRef}
1308
+ id={contentId}
1309
+ role="menu"
1310
+ // One Tab stop between the regions.
1311
+ tabIndex={0}
1312
+ aria-labelledby={triggerId}
1313
+ onKeyDown={handleContentKeyDown}
1314
+ onFocus={handleMenuFocus}
1315
+ onBlur={handleMenuBlur}
1316
+ data-testid={menuDataTestId}
1317
+ {...menuPress.menuProps}
1318
+ {...surfaceOwnerProps}
1319
+ {...stylex.props(
1320
+ regionStyles.menu,
1321
+ hasOverflow
1322
+ ? flyoutStyles.touchPanYPinchZoom
1323
+ : flyoutStyles.touchPinchZoom,
1324
+ hasOverflow && flyoutStyles.scrollable,
1325
+ isMenuKeyboardFocused && focusOutlineStyles.focusVisible,
1326
+ )}>
1327
+ {nestedContent}
1328
+ </div>,
1329
+ {
1330
+ 'aria-labelledby': triggerId,
1331
+ // Hovering the regions is hovering the flyout.
1332
+ onMouseEnter: contentProps.onMouseEnter,
1333
+ onMouseLeave: contentProps.onMouseLeave,
1334
+ },
1335
+ 'flyout',
1336
+ )
1337
+ ) : (
1338
+ <div
1339
+ ref={menuRef}
1340
+ id={contentId}
1341
+ role="menu"
1342
+ // Focusable as a fallback target so an empty/loading flyout can own
1343
+ // arrow/Escape keys. An overflowing flyout joins the Tab order so its
1344
+ // scrollable region is keyboard-accessible.
1345
+ tabIndex={hasOverflow ? 0 : -1}
1346
+ aria-labelledby={triggerId}
1347
+ onKeyDown={handleContentKeyDown}
1348
+ onMouseEnter={contentProps.onMouseEnter}
1349
+ onMouseLeave={contentProps.onMouseLeave}
1350
+ data-testid={menuDataTestId}
1351
+ {...menuPress.menuProps}
1352
+ {...mergeProps(
1353
+ themeProps('dropdown-menu'),
1354
+ stylex.props(
1355
+ flyoutStyles.menu,
1356
+ hasOverflow
1357
+ ? flyoutStyles.touchPanYPinchZoom
1358
+ : flyoutStyles.touchPinchZoom,
1359
+ hasOverflow && flyoutStyles.scrollable,
1360
+ ),
1361
+ )}>
1362
+ {nestedContent}
1363
+ </div>
1364
+ ),
1365
+ {
1366
+ placement: 'end',
1367
+ alignment: 'start',
1368
+ offset: spacingVars['--spacing-1'],
1369
+ xstyle: [
1370
+ flyoutStyles.popoverViewport,
1371
+ popoverXstyle,
1372
+ layerAnimations.end,
1373
+ ],
1374
+ },
1375
+ ),
919
1376
  )}
920
1377
  </>
921
1378
  );