@radix-ng/primitives 1.0.0-beta.3 → 1.0.0-beta.5

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 (118) hide show
  1. package/README.md +1 -1
  2. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  3. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +3 -2
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-autocomplete.mjs +617 -659
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  9. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-checkbox.mjs +33 -18
  11. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-combobox.mjs +1305 -572
  13. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  15. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  17. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-core.mjs +1352 -64
  19. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  21. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-dialog.mjs +290 -120
  23. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  25. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  27. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-drawer.mjs +3 -3
  29. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-editable.mjs +12 -7
  31. package/fesm2022/radix-ng-primitives-editable.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  33. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +803 -0
  35. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  36. package/fesm2022/radix-ng-primitives-focus-scope.mjs +305 -70
  37. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  38. package/fesm2022/radix-ng-primitives-menu.mjs +893 -289
  39. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  40. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  41. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  42. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +144 -159
  43. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  44. package/fesm2022/radix-ng-primitives-number-field.mjs +7 -2
  45. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  46. package/fesm2022/radix-ng-primitives-popover.mjs +284 -212
  47. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  48. package/fesm2022/radix-ng-primitives-popper.mjs +94 -51
  49. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  50. package/fesm2022/radix-ng-primitives-presence.mjs +1 -1
  51. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  52. package/fesm2022/radix-ng-primitives-preview-card.mjs +141 -173
  53. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  54. package/fesm2022/radix-ng-primitives-radio.mjs +19 -14
  55. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  56. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  57. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  58. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  59. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  60. package/fesm2022/radix-ng-primitives-select.mjs +241 -164
  61. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  62. package/fesm2022/radix-ng-primitives-slider.mjs +262 -29
  63. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  64. package/fesm2022/radix-ng-primitives-stepper.mjs +16 -10
  65. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  66. package/fesm2022/radix-ng-primitives-switch.mjs +10 -5
  67. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  68. package/fesm2022/radix-ng-primitives-tabs.mjs +15 -10
  69. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  70. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  71. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  72. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  73. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  74. package/fesm2022/radix-ng-primitives-toggle-group.mjs +14 -7
  75. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  76. package/fesm2022/radix-ng-primitives-toggle.mjs +12 -6
  77. package/fesm2022/radix-ng-primitives-toggle.mjs.map +1 -1
  78. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  79. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  80. package/fesm2022/radix-ng-primitives-tooltip.mjs +251 -143
  81. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  82. package/package.json +10 -1
  83. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  84. package/types/radix-ng-primitives-autocomplete.d.ts +217 -152
  85. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  86. package/types/radix-ng-primitives-checkbox.d.ts +27 -15
  87. package/types/radix-ng-primitives-combobox.d.ts +672 -283
  88. package/types/radix-ng-primitives-config.d.ts +1 -1
  89. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  90. package/types/radix-ng-primitives-core.d.ts +764 -14
  91. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  92. package/types/radix-ng-primitives-dialog.d.ts +88 -32
  93. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  94. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  95. package/types/radix-ng-primitives-editable.d.ts +11 -5
  96. package/types/radix-ng-primitives-field.d.ts +1 -0
  97. package/types/radix-ng-primitives-floating-focus-manager.d.ts +272 -0
  98. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  99. package/types/radix-ng-primitives-menu.d.ts +192 -103
  100. package/types/radix-ng-primitives-navigation-menu.d.ts +37 -75
  101. package/types/radix-ng-primitives-number-field.d.ts +8 -3
  102. package/types/radix-ng-primitives-popover.d.ts +71 -92
  103. package/types/radix-ng-primitives-popper.d.ts +39 -9
  104. package/types/radix-ng-primitives-preview-card.d.ts +39 -72
  105. package/types/radix-ng-primitives-radio.d.ts +13 -6
  106. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  107. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  108. package/types/radix-ng-primitives-select.d.ts +142 -109
  109. package/types/radix-ng-primitives-slider.d.ts +64 -12
  110. package/types/radix-ng-primitives-stepper.d.ts +15 -7
  111. package/types/radix-ng-primitives-switch.d.ts +10 -4
  112. package/types/radix-ng-primitives-tabs.d.ts +12 -6
  113. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  114. package/types/radix-ng-primitives-toast.d.ts +7 -7
  115. package/types/radix-ng-primitives-toggle-group.d.ts +15 -8
  116. package/types/radix-ng-primitives-toggle.d.ts +10 -3
  117. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  118. package/types/radix-ng-primitives-tooltip.d.ts +61 -80
@@ -1,13 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, model, input, booleanAttribute, output, signal, computed, effect, untracked, Directive, ElementRef, DestroyRef, isDevMode, numberAttribute, PLATFORM_ID, afterNextRender, NgModule } from '@angular/core';
2
+ import { inject, ElementRef, model, input, booleanAttribute, output, signal, computed, effect, untracked, Directive, DestroyRef, isDevMode, Injector, afterNextRender, numberAttribute, PLATFORM_ID, NgModule } from '@angular/core';
3
3
  import * as i1 from '@radix-ng/primitives/popper';
4
- import { RdxPopper, RdxPopperContentWrapper, RdxPopperArrow, RdxPopperContent, provideRdxPopperContentConfig, RdxPopperAnchor } from '@radix-ng/primitives/popper';
5
- import { createContext, useTransitionStatus, getMaxTransitionDuration } from '@radix-ng/primitives/core';
4
+ import { RdxPopper, RdxPopperContentWrapper, RdxPopperArrow, RdxPopperContent, legacyPopperVars, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, RdxPopperAnchor } from '@radix-ng/primitives/popper';
5
+ import * as i2 from '@radix-ng/primitives/core';
6
+ import { createContext, createFloatingRootContext, useTransitionStatus, createCancelableChangeEventDetails, provideFloatingTree, provideFloatingRootContext, injectId, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, useAnchoredScrollLock, RdxFloatingNodeRegistration, rdxDevError, setupInternalBackdrop, getMaxTransitionDuration } from '@radix-ng/primitives/core';
7
+ import { injectDirection } from '@radix-ng/primitives/direction-provider';
8
+ import * as i3 from '@radix-ng/primitives/floating-focus-manager';
9
+ import { getInteractionTypeFromEvent, RdxFloatingFocusManager, provideFloatingFocusManagerConfig, createRdxTriggerInteraction, useTriggerFocusGuards } from '@radix-ng/primitives/floating-focus-manager';
6
10
  import { outputFromObservable, outputToObservable } from '@angular/core/rxjs-interop';
7
- import * as i2 from '@radix-ng/primitives/dismissable-layer';
8
- import { RdxDismissableLayer, RdxDismissableLayersContextToken, provideRdxDismissableLayerConfig, RdxDismissableLayerBranch } from '@radix-ng/primitives/dismissable-layer';
9
- import * as i3 from '@radix-ng/primitives/focus-scope';
10
- import { RdxFocusScope, provideRdxFocusScopeConfig } from '@radix-ng/primitives/focus-scope';
11
+ import { RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
12
+ import { RdxFocusScope } from '@radix-ng/primitives/focus-scope';
11
13
  import * as i1$1 from '@radix-ng/primitives/portal';
12
14
  import { RdxPortalPresence } from '@radix-ng/primitives/portal';
13
15
  import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
@@ -17,28 +19,41 @@ const [injectRdxMenuRootContext, provideRdxMenuRootContext] = createContext('Rdx
17
19
  function buildContext(instance) {
18
20
  return {
19
21
  isOpen: instance.open,
20
- disabled: instance.disabled,
21
- modal: instance.modal,
22
+ present: instance.present,
23
+ disabled: instance.effectiveDisabled,
24
+ modal: instance.effectiveModal,
22
25
  loopFocus: instance.loopFocus,
23
26
  highlightItemOnHover: instance.highlightItemOnHover,
24
27
  orientation: instance.orientation,
28
+ dir: instance.dir,
25
29
  closeParentOnEsc: instance.closeParentOnEsc,
26
30
  autoFocus: instance.autoFocus.asReadonly(),
27
31
  isSubmenu: instance.isSubmenu.asReadonly(),
32
+ parentType: instance.parentType,
33
+ lastOpenChangeReason: instance.lastOpenChangeReason.asReadonly(),
34
+ allowMouseUpTrigger: instance.allowMouseUpTrigger,
35
+ openedByTouch: instance.openedByTouch.asReadonly(),
36
+ openInteractionType: instance.openInteractionType.asReadonly(),
37
+ closeInteractionType: instance.closeInteractionType.asReadonly(),
28
38
  hasTriggerInteractionHandler: instance.hasTriggerInteractionHandler.asReadonly(),
29
39
  trigger: instance.trigger.asReadonly(),
30
40
  popupElement: instance.popupElement.asReadonly(),
41
+ beforeContentFocusGuard: instance.beforeContentFocusGuard.asReadonly(),
31
42
  transitionStatus: instance.transitionStatus,
32
- close: () => instance.close(),
33
- toggle: () => instance.toggle(),
34
- show: (autoFocus) => instance.show(autoFocus),
35
- showWithoutAutoFocus: () => instance.show(false),
43
+ close: (reason, event) => instance.close(reason, event),
44
+ closeEntireMenu: (reason, event) => instance.closeEntireMenu(reason, event),
45
+ toggle: (reason, event) => instance.toggle(reason, event),
46
+ show: (autoFocus, reason, event) => instance.show(autoFocus, reason, event),
47
+ showWithoutAutoFocus: (reason, event) => instance.show(false, reason, event),
36
48
  registerTrigger: (el) => instance.registerTrigger(el),
37
49
  registerPopup: (el) => instance.registerPopup(el),
50
+ setBeforeContentFocusGuard: (element) => instance.setBeforeContentFocusGuard(element),
38
51
  registerTransitionElement: (el) => instance.registerTransitionElement(el),
39
52
  registerPopupArrowNavigationHandler: (handler) => instance.registerPopupArrowNavigationHandler(handler),
40
53
  registerTriggerInteractionHandler: (handler) => instance.registerTriggerInteractionHandler(handler),
41
54
  markAsSubmenu: () => instance.markAsSubmenu(),
55
+ markAsContextMenu: () => instance.markAsContextMenu(),
56
+ setAllowMouseUpTrigger: (value) => instance.setAllowMouseUpTrigger(value),
42
57
  closeParent: () => instance.closeParent(),
43
58
  handlePopupArrowNavigation: (offset) => instance.handlePopupArrowNavigation(offset),
44
59
  handleTriggerInteraction: (interaction) => instance.handleTriggerInteraction(interaction)
@@ -51,6 +66,17 @@ const contextFactory = () => buildContext(inject(RdxMenuRoot));
51
66
  class RdxMenuRoot {
52
67
  constructor() {
53
68
  this.popper = inject(RdxPopper);
69
+ this.parentRoot = inject(RdxMenuRoot, { optional: true, skipSelf: true });
70
+ this.providedDirection = injectDirection();
71
+ /**
72
+ * The shared per-popup floating context (ADR 0015 §1) — `open` mirrors this menu's open state, the
73
+ * trigger registry is bridged from {@link registerTrigger}, and the reference / floating elements are
74
+ * set by the trigger / popup. The new dismissal engine reads this once the popup migrates.
75
+ */
76
+ this.floatingContext = createFloatingRootContext({
77
+ ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
78
+ open: () => this.open()
79
+ });
54
80
  /** Shared open/close transition state machine (completes on the real animationend). */
55
81
  this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
56
82
  this.hasAppliedDefaultOpen = false;
@@ -60,14 +86,19 @@ class RdxMenuRoot {
60
86
  this.defaultOpen = input(false, { ...(ngDevMode ? { debugName: "defaultOpen" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
61
87
  /** Whether interactions with the menu are disabled. */
62
88
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
63
- /** Whether the menu should block outside interactions. */
64
- this.modal = input(false, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
89
+ /**
90
+ * Whether the menu should block outside interactions and page scrolling.
91
+ * Nested menus are always non-modal.
92
+ */
93
+ this.modal = input(true, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
65
94
  /** Whether keyboard navigation wraps at list boundaries. */
66
95
  this.loopFocus = input(true, { ...(ngDevMode ? { debugName: "loopFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
67
96
  /** Whether moving the pointer over items should highlight them. */
68
97
  this.highlightItemOnHover = input(true, { ...(ngDevMode ? { debugName: "highlightItemOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
69
98
  /** The menu orientation. */
70
99
  this.orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
100
+ /** Text direction for submenu arrow-key behavior. Inherited by nested submenu roots. */
101
+ this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
71
102
  /** Whether pressing Escape inside a submenu closes the whole menu chain. */
72
103
  this.closeParentOnEsc = input(false, { ...(ngDevMode ? { debugName: "closeParentOnEsc" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
73
104
  /** Emits when the open state changes. */
@@ -76,12 +107,47 @@ class RdxMenuRoot {
76
107
  this.onOpenChangeComplete = output();
77
108
  this.trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
78
109
  this.popupElement = signal(undefined, ...(ngDevMode ? [{ debugName: "popupElement" }] : /* istanbul ignore next */ []));
110
+ this.beforeContentFocusGuard = signal(null, ...(ngDevMode ? [{ debugName: "beforeContentFocusGuard" }] : /* istanbul ignore next */ []));
79
111
  this.transitionStatus = this.transition.status;
80
112
  /** Whether the popup grabs focus when it opens. Set false for menubar hover-switching. */
81
113
  this.autoFocus = signal('first', ...(ngDevMode ? [{ debugName: "autoFocus" }] : /* istanbul ignore next */ []));
82
114
  this.isSubmenu = signal(false, ...(ngDevMode ? [{ debugName: "isSubmenu" }] : /* istanbul ignore next */ []));
115
+ /** Set by `RdxContextMenuRoot` (it composes this root) — distinguishes a context menu from a dropdown. */
116
+ this.isContextMenu = signal(false, ...(ngDevMode ? [{ debugName: "isContextMenu" }] : /* istanbul ignore next */ []));
83
117
  this.hasTriggerInteractionHandler = signal(false, ...(ngDevMode ? [{ debugName: "hasTriggerInteractionHandler" }] : /* istanbul ignore next */ []));
118
+ this.preventUnmountOnClose = signal(false, ...(ngDevMode ? [{ debugName: "preventUnmountOnClose" }] : /* istanbul ignore next */ []));
119
+ /**
120
+ * What kind of parent this menu has (Base UI `MenuParent.type`). A submenu wins over everything (its
121
+ * parent is a menu); otherwise a context-menu marker, then a menubar (detected by the trigger
122
+ * interaction handler the menubar registers), else a standalone dropdown.
123
+ */
124
+ this.parentType = computed(() => {
125
+ if (this.isSubmenu()) {
126
+ return 'menu';
127
+ }
128
+ if (this.isContextMenu()) {
129
+ return 'context-menu';
130
+ }
131
+ if (this.hasTriggerInteractionHandler()) {
132
+ return 'menubar';
133
+ }
134
+ return undefined;
135
+ }, ...(ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []));
136
+ /** The reason for the most recent open-change (Base UI open-change `reason`), for the per-kind policy. */
137
+ this.lastOpenChangeReason = signal('none', ...(ngDevMode ? [{ debugName: "lastOpenChangeReason" }] : /* istanbul ignore next */ []));
138
+ this.localAllowMouseUpTrigger = signal(false, ...(ngDevMode ? [{ debugName: "localAllowMouseUpTrigger" }] : /* istanbul ignore next */ []));
139
+ this.allowMouseUpTrigger = computed(() => this.parentRoot?.allowMouseUpTrigger() ?? this.localAllowMouseUpTrigger(), ...(ngDevMode ? [{ debugName: "allowMouseUpTrigger" }] : /* istanbul ignore next */ []));
140
+ /** Whether the current open was initiated by **touch** (ADR 0016 §3 — gates the anchored scroll lock). */
141
+ this.openedByTouch = signal(false, ...(ngDevMode ? [{ debugName: "openedByTouch" }] : /* istanbul ignore next */ []));
142
+ this.openInteractionType = signal(null, ...(ngDevMode ? [{ debugName: "openInteractionType" }] : /* istanbul ignore next */ []));
143
+ this.closeInteractionType = signal(null, ...(ngDevMode ? [{ debugName: "closeInteractionType" }] : /* istanbul ignore next */ []));
144
+ this.effectiveDisabled = computed(() => this.disabled() || (this.parentRoot?.effectiveDisabled() ?? false), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
145
+ this.dir = computed(() => {
146
+ return this.dirInput() ?? this.parentRoot?.dir() ?? this.providedDirection();
147
+ }, ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
148
+ this.effectiveModal = computed(() => this.modal() && !this.isSubmenu(), ...(ngDevMode ? [{ debugName: "effectiveModal" }] : /* istanbul ignore next */ []));
84
149
  this.state = computed(() => (this.open() ? 'open' : 'closed'), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
150
+ this.present = computed(() => this.open() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
85
151
  effect(() => {
86
152
  const defaultOpen = this.defaultOpen();
87
153
  if (!this.hasAppliedDefaultOpen && defaultOpen) {
@@ -90,6 +156,14 @@ class RdxMenuRoot {
90
156
  }
91
157
  });
92
158
  effect(() => this.popper.anchorOverride.set(this.trigger()));
159
+ // Keep the dismissal reference (the active trigger) in sync so an outside-press / focus on the
160
+ // trigger counts as "inside" and never dismisses (ADR 0015).
161
+ effect(() => this.floatingContext.setReferenceElement(this.trigger() ?? null));
162
+ effect(() => {
163
+ if (this.open() && this.preventUnmountOnClose()) {
164
+ this.preventUnmountOnClose.set(false);
165
+ }
166
+ });
93
167
  let previousOpen = this.open();
94
168
  effect(() => {
95
169
  const open = this.open();
@@ -99,37 +173,79 @@ class RdxMenuRoot {
99
173
  }
100
174
  });
101
175
  }
102
- show(autoFocus = 'first') {
103
- if (this.disabled()) {
176
+ show(autoFocus = 'first', reason = 'none', event) {
177
+ if (this.effectiveDisabled()) {
104
178
  return;
105
179
  }
106
180
  this.autoFocus.set(autoFocus === true ? 'first' : autoFocus);
107
181
  if (!this.open()) {
182
+ const change = this.createOpenChangeEvent(true, reason, event);
183
+ this.onOpenChange.emit(change.payload);
184
+ if (change.eventDetails.isCanceled()) {
185
+ return;
186
+ }
187
+ this.lastOpenChangeReason.set(reason);
188
+ // Record whether this open came from touch (ADR 0016 §3). Hover / mouse / keyboard all resolve
189
+ // to false (no `'touch'` pointer type), so only a genuine touch open gates the anchored lock.
190
+ this.openedByTouch.set(event?.pointerType === 'touch');
191
+ this.openInteractionType.set(getInteractionTypeFromEvent(event));
192
+ this.preventUnmountOnClose.set(false);
108
193
  this.open.set(true);
109
- this.onOpenChange.emit(true);
194
+ // Publish reason + native event on the per-popup floating channel (Base UI open-change) so the
195
+ // dismissal / future focus policy can read why the menu opened (e.g. hover vs press).
196
+ this.floatingContext.events.emit('openchange', { open: true, reason, event: change.eventDetails.event });
110
197
  }
111
198
  }
112
- close() {
199
+ close(reason = 'none', event) {
113
200
  if (this.open()) {
201
+ const change = this.createOpenChangeEvent(false, reason, event);
202
+ this.onOpenChange.emit(change.payload);
203
+ if (change.eventDetails.isCanceled()) {
204
+ return;
205
+ }
206
+ this.setAllowMouseUpTrigger(false);
207
+ this.lastOpenChangeReason.set(reason);
208
+ this.closeInteractionType.set(getInteractionTypeFromEvent(event));
209
+ this.preventUnmountOnClose.set(change.shouldPreventUnmountOnClose());
114
210
  this.open.set(false);
115
- this.onOpenChange.emit(false);
211
+ this.floatingContext.events.emit('openchange', { open: false, reason, event: change.eventDetails.event });
116
212
  }
117
213
  }
118
- toggle() {
119
- if (this.disabled()) {
214
+ toggle(reason = 'trigger-press', event) {
215
+ if (this.effectiveDisabled()) {
120
216
  return;
121
217
  }
122
218
  if (this.open()) {
123
- this.close();
219
+ this.close(reason, event);
124
220
  }
125
221
  else {
126
- this.show();
222
+ this.show('first', reason, event);
127
223
  }
128
224
  }
225
+ markAsContextMenu() {
226
+ this.isContextMenu.set(true);
227
+ }
228
+ setAllowMouseUpTrigger(value) {
229
+ if (this.parentRoot) {
230
+ this.parentRoot.setAllowMouseUpTrigger(value);
231
+ return;
232
+ }
233
+ this.localAllowMouseUpTrigger.set(value);
234
+ }
235
+ /**
236
+ * Close this menu **and every ancestor menu** in the chain. Selecting an item dismisses the whole
237
+ * menu, not just the innermost submenu (a submenu's `close()` would leave its parents open).
238
+ */
239
+ closeEntireMenu(reason = 'none', event) {
240
+ this.close(reason, event);
241
+ this.parentRoot?.closeEntireMenu(reason, event);
242
+ }
129
243
  registerTrigger(el) {
130
244
  this.registeredTrigger = el;
131
245
  this.trigger.set(el);
246
+ this.floatingContext.triggers.add(el);
132
247
  return () => {
248
+ this.floatingContext.triggers.delete(el);
133
249
  if (this.registeredTrigger === el) {
134
250
  this.registeredTrigger = undefined;
135
251
  this.trigger.set(undefined);
@@ -144,6 +260,9 @@ class RdxMenuRoot {
144
260
  }
145
261
  };
146
262
  }
263
+ setBeforeContentFocusGuard(element) {
264
+ this.beforeContentFocusGuard.set(element);
265
+ }
147
266
  registerTransitionElement(element) {
148
267
  return this.transition.registerElement(element);
149
268
  }
@@ -177,18 +296,44 @@ class RdxMenuRoot {
177
296
  closeParent() {
178
297
  this.trigger()?.dispatchEvent(new CustomEvent('rdx-menu-close-parent', { bubbles: true }));
179
298
  }
299
+ createOpenChangeEvent(open, reason, event) {
300
+ const change = createCancelableChangeEventDetails(reason, event ?? new Event('menu.open-change'), this.trigger());
301
+ return {
302
+ eventDetails: change.eventDetails,
303
+ shouldPreventUnmountOnClose: change.shouldPreventUnmountOnClose,
304
+ payload: {
305
+ open,
306
+ trigger: change.eventDetails.trigger,
307
+ reason: change.eventDetails.reason,
308
+ event: change.eventDetails.event,
309
+ eventDetails: change.eventDetails
310
+ }
311
+ };
312
+ }
180
313
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
181
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuRoot, isStandalone: true, selector: "[rdxMenuRoot],[rdxMenuSubmenuRoot]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, highlightItemOnHover: { classPropertyName: "highlightItemOnHover", publicName: "highlightItemOnHover", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, closeParentOnEsc: { classPropertyName: "closeParentOnEsc", publicName: "closeParentOnEsc", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", onOpenChange: "onOpenChange", onOpenChangeComplete: "onOpenChangeComplete" }, providers: [provideRdxMenuRootContext(contextFactory)], exportAs: ["rdxMenuRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
314
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuRoot, isStandalone: true, selector: "[rdxMenuRoot],[rdxMenuSubmenuRoot]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaultOpen: { classPropertyName: "defaultOpen", publicName: "defaultOpen", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, loopFocus: { classPropertyName: "loopFocus", publicName: "loopFocus", isSignal: true, isRequired: false, transformFunction: null }, highlightItemOnHover: { classPropertyName: "highlightItemOnHover", publicName: "highlightItemOnHover", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, dirInput: { classPropertyName: "dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, closeParentOnEsc: { classPropertyName: "closeParentOnEsc", publicName: "closeParentOnEsc", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", onOpenChange: "onOpenChange", onOpenChangeComplete: "onOpenChangeComplete" }, providers: [
315
+ provideRdxMenuRootContext(contextFactory),
316
+ // New floating foundation (ADR 0015/0017). Inherit-or-create the tree so a submenu shares its
317
+ // parent menu's tree; the per-popup root context bridges open / triggers / reference.
318
+ provideFloatingTree(),
319
+ provideFloatingRootContext(() => inject(RdxMenuRoot).floatingContext)
320
+ ], exportAs: ["rdxMenuRoot"], hostDirectives: [{ directive: i1.RdxPopper }], ngImport: i0 }); }
182
321
  }
183
322
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRoot, decorators: [{
184
323
  type: Directive,
185
324
  args: [{
186
325
  selector: '[rdxMenuRoot],[rdxMenuSubmenuRoot]',
187
326
  exportAs: 'rdxMenuRoot',
188
- providers: [provideRdxMenuRootContext(contextFactory)],
327
+ providers: [
328
+ provideRdxMenuRootContext(contextFactory),
329
+ // New floating foundation (ADR 0015/0017). Inherit-or-create the tree so a submenu shares its
330
+ // parent menu's tree; the per-popup root context bridges open / triggers / reference.
331
+ provideFloatingTree(),
332
+ provideFloatingRootContext(() => inject(RdxMenuRoot).floatingContext)
333
+ ],
189
334
  hostDirectives: [RdxPopper]
190
335
  }]
191
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], highlightItemOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightItemOnHover", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], closeParentOnEsc: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeParentOnEsc", required: false }] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
336
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], defaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultOpen", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], loopFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "loopFocus", required: false }] }], highlightItemOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightItemOnHover", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], closeParentOnEsc: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeParentOnEsc", required: false }] }], onOpenChange: [{ type: i0.Output, args: ["onOpenChange"] }], onOpenChangeComplete: [{ type: i0.Output, args: ["onOpenChangeComplete"] }] } });
192
337
 
193
338
  /**
194
339
  * An optional visual arrow connecting the popup to its trigger.
@@ -281,12 +426,13 @@ class RdxMenuCheckboxItem {
281
426
  /** Emits when the checked state changes. */
282
427
  this.onCheckedChange = output();
283
428
  this.highlighted = computed(() => this.isFocused(), ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
429
+ this.effectiveDisabled = computed(() => this.disabled() || (this.rootContext?.disabled() ?? false), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
284
430
  // Expose helpers for host bindings
285
431
  this.isIndeterminate = isIndeterminate;
286
432
  this.getCheckedState = getCheckedState;
287
433
  }
288
434
  onFocus() {
289
- if (!this.disabled()) {
435
+ if (!this.effectiveDisabled()) {
290
436
  this.isFocused.set(true);
291
437
  }
292
438
  }
@@ -294,13 +440,13 @@ class RdxMenuCheckboxItem {
294
440
  this.isFocused.set(false);
295
441
  }
296
442
  onPointerMove(event) {
297
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
443
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
298
444
  return;
299
445
  }
300
446
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
301
447
  return;
302
448
  }
303
- if (document.activeElement !== this.elementRef.nativeElement) {
449
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
304
450
  this.elementRef.nativeElement.focus({ preventScroll: true });
305
451
  }
306
452
  }
@@ -308,24 +454,31 @@ class RdxMenuCheckboxItem {
308
454
  if (event.pointerType !== 'mouse') {
309
455
  return;
310
456
  }
311
- if (document.activeElement === this.elementRef.nativeElement) {
457
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
312
458
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
313
459
  }
314
460
  }
315
461
  onItemClick() {
316
- if (this.disabled())
462
+ if (this.effectiveDisabled())
317
463
  return;
318
464
  this.toggleChecked();
319
465
  if (this.closeOnClick())
320
- this.rootContext?.close();
466
+ this.rootContext?.closeEntireMenu();
467
+ }
468
+ onMouseUp(event) {
469
+ if (this.effectiveDisabled() || event.button !== 0 || !this.rootContext?.allowMouseUpTrigger()) {
470
+ return;
471
+ }
472
+ this.rootContext.setAllowMouseUpTrigger(false);
473
+ this.elementRef.nativeElement.click();
321
474
  }
322
475
  onActivate(event) {
323
- if (this.disabled())
476
+ if (this.effectiveDisabled())
324
477
  return;
325
478
  event.preventDefault();
326
479
  this.toggleChecked();
327
480
  if (this.closeOnClick())
328
- this.rootContext?.close();
481
+ this.rootContext?.closeEntireMenu();
329
482
  }
330
483
  toggleChecked() {
331
484
  const next = isIndeterminate(this.checked()) ? true : !this.checked();
@@ -333,7 +486,7 @@ class RdxMenuCheckboxItem {
333
486
  this.onCheckedChange.emit(next);
334
487
  }
335
488
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuCheckboxItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
336
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuCheckboxItem, isStandalone: true, selector: "[rdxMenuCheckboxItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", onCheckedChange: "onCheckedChange" }, host: { attributes: { "role": "menuitemcheckbox", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "click": "onItemClick()", "keydown.enter": "onActivate($event)", "keydown.space": "onActivate($event)" }, properties: { "attr.aria-checked": "isIndeterminate(checked()) ? \"mixed\" : checked()", "attr.data-state": "getCheckedState(checked())", "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.aria-disabled": "disabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, providers: [provideRdxMenuCheckboxItemContext(checkboxItemContextFactory)], exportAs: ["rdxMenuCheckboxItem"], ngImport: i0 }); }
489
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuCheckboxItem, isStandalone: true, selector: "[rdxMenuCheckboxItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", onCheckedChange: "onCheckedChange" }, host: { attributes: { "role": "menuitemcheckbox", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "mouseup": "onMouseUp($event)", "click": "onItemClick()", "keydown.enter": "onActivate($event)", "keydown.space": "onActivate($event)" }, properties: { "attr.aria-checked": "isIndeterminate(checked()) ? \"mixed\" : checked()", "attr.data-state": "getCheckedState(checked())", "attr.data-disabled": "effectiveDisabled() ? \"\" : undefined", "attr.aria-disabled": "effectiveDisabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, providers: [provideRdxMenuCheckboxItemContext(checkboxItemContextFactory)], exportAs: ["rdxMenuCheckboxItem"], ngImport: i0 }); }
337
490
  }
338
491
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuCheckboxItem, decorators: [{
339
492
  type: Directive,
@@ -346,14 +499,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
346
499
  tabindex: '-1',
347
500
  '[attr.aria-checked]': 'isIndeterminate(checked()) ? "mixed" : checked()',
348
501
  '[attr.data-state]': 'getCheckedState(checked())',
349
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
350
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
502
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
503
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
351
504
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
352
505
  '[attr.data-label]': 'label() ?? undefined',
353
506
  '(focus)': 'onFocus()',
354
507
  '(blur)': 'onBlur()',
355
508
  '(pointermove)': 'onPointerMove($event)',
356
509
  '(pointerleave)': 'onPointerLeave($event)',
510
+ '(mouseup)': 'onMouseUp($event)',
357
511
  '(click)': 'onItemClick()',
358
512
  '(keydown.enter)': 'onActivate($event)',
359
513
  '(keydown.space)': 'onActivate($event)'
@@ -390,20 +544,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
390
544
  }]
391
545
  }], propDecorators: { keepMounted: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepMounted", required: false }] }] } });
392
546
 
547
+ const [injectRdxMenuGroupContext, provideRdxMenuGroupContext] = createContext('RdxMenuGroupContext', 'components/menu');
548
+
549
+ const groupContextFactory$1 = () => {
550
+ const instance = inject(RdxMenuGroup);
551
+ return { labelId: instance.labelId };
552
+ };
393
553
  /**
394
554
  * Groups related menu items together.
395
555
  */
396
556
  class RdxMenuGroup {
557
+ constructor() {
558
+ this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
559
+ }
397
560
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
398
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuGroup, isStandalone: true, selector: "[rdxMenuGroup]", host: { attributes: { "role": "group" } }, exportAs: ["rdxMenuGroup"], ngImport: i0 }); }
561
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuGroup, isStandalone: true, selector: "[rdxMenuGroup]", host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId()" } }, providers: [provideRdxMenuGroupContext(groupContextFactory$1)], exportAs: ["rdxMenuGroup"], ngImport: i0 }); }
399
562
  }
400
563
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroup, decorators: [{
401
564
  type: Directive,
402
565
  args: [{
403
566
  selector: '[rdxMenuGroup]',
404
567
  exportAs: 'rdxMenuGroup',
568
+ providers: [provideRdxMenuGroupContext(groupContextFactory$1)],
405
569
  host: {
406
- role: 'group'
570
+ role: 'group',
571
+ '[attr.aria-labelledby]': 'labelId()'
407
572
  }
408
573
  }]
409
574
  }] });
@@ -412,16 +577,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
412
577
  * A label for a menu group.
413
578
  */
414
579
  class RdxMenuGroupLabel {
580
+ constructor() {
581
+ this.groupContext = injectRdxMenuGroupContext();
582
+ this.id = injectId('rdx-menu-group-label-');
583
+ this.groupContext.labelId.set(this.id);
584
+ inject(DestroyRef).onDestroy(() => {
585
+ if (this.groupContext.labelId() === this.id) {
586
+ this.groupContext.labelId.set(undefined);
587
+ }
588
+ });
589
+ }
415
590
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
416
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuGroupLabel, isStandalone: true, selector: "[rdxMenuGroupLabel]", exportAs: ["rdxMenuGroupLabel"], ngImport: i0 }); }
591
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuGroupLabel, isStandalone: true, selector: "[rdxMenuGroupLabel]", host: { properties: { "attr.id": "id" } }, exportAs: ["rdxMenuGroupLabel"], ngImport: i0 }); }
417
592
  }
418
593
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroupLabel, decorators: [{
419
594
  type: Directive,
420
595
  args: [{
421
596
  selector: '[rdxMenuGroupLabel]',
422
- exportAs: 'rdxMenuGroupLabel'
597
+ exportAs: 'rdxMenuGroupLabel',
598
+ host: {
599
+ '[attr.id]': 'id'
600
+ }
423
601
  }]
424
- }] });
602
+ }], ctorParameters: () => [] });
425
603
 
426
604
  /**
427
605
  * An individual menu item.
@@ -440,9 +618,10 @@ class RdxMenuItem {
440
618
  /** Emits when the item is selected. */
441
619
  this.onSelect = output();
442
620
  this.highlighted = computed(() => this.isFocused(), ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
621
+ this.effectiveDisabled = computed(() => this.disabled() || (this.rootContext?.disabled() ?? false), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
443
622
  }
444
623
  onFocus() {
445
- if (!this.disabled()) {
624
+ if (!this.effectiveDisabled()) {
446
625
  this.isFocused.set(true);
447
626
  }
448
627
  }
@@ -450,13 +629,13 @@ class RdxMenuItem {
450
629
  this.isFocused.set(false);
451
630
  }
452
631
  onPointerMove(event) {
453
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
632
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
454
633
  return;
455
634
  }
456
635
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
457
636
  return;
458
637
  }
459
- if (document.activeElement !== this.elementRef.nativeElement) {
638
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
460
639
  this.elementRef.nativeElement.focus({ preventScroll: true });
461
640
  }
462
641
  }
@@ -466,27 +645,34 @@ class RdxMenuItem {
466
645
  }
467
646
  // Clear highlight when the pointer leaves: move focus back to the popup. A subsequent
468
647
  // pointermove on a sibling item re-focuses it, so moving between items still works.
469
- if (document.activeElement === this.elementRef.nativeElement) {
648
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
470
649
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
471
650
  }
472
651
  }
473
652
  onItemClick() {
474
- if (this.disabled())
653
+ if (this.effectiveDisabled())
475
654
  return;
476
655
  this.onSelect.emit();
477
656
  if (this.closeOnClick())
478
- this.rootContext?.close();
657
+ this.rootContext?.closeEntireMenu();
658
+ }
659
+ onMouseUp(event) {
660
+ if (this.effectiveDisabled() || event.button !== 0 || !this.rootContext?.allowMouseUpTrigger()) {
661
+ return;
662
+ }
663
+ this.rootContext.setAllowMouseUpTrigger(false);
664
+ this.elementRef.nativeElement.click();
479
665
  }
480
666
  onActivate(event) {
481
- if (this.disabled())
667
+ if (this.effectiveDisabled())
482
668
  return;
483
669
  event.preventDefault();
484
670
  this.onSelect.emit();
485
671
  if (this.closeOnClick())
486
- this.rootContext?.close();
672
+ this.rootContext?.closeEntireMenu();
487
673
  }
488
674
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
489
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuItem, isStandalone: true, selector: "[rdxMenuItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect" }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "click": "onItemClick()", "keydown.enter": "onActivate($event)", "keydown.space": "onActivate($event)" }, properties: { "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.aria-disabled": "disabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, exportAs: ["rdxMenuItem"], ngImport: i0 }); }
675
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuItem, isStandalone: true, selector: "[rdxMenuItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect" }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "mouseup": "onMouseUp($event)", "click": "onItemClick()", "keydown.enter": "onActivate($event)", "keydown.space": "onActivate($event)" }, properties: { "attr.data-disabled": "effectiveDisabled() ? \"\" : undefined", "attr.aria-disabled": "effectiveDisabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, exportAs: ["rdxMenuItem"], ngImport: i0 }); }
490
676
  }
491
677
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuItem, decorators: [{
492
678
  type: Directive,
@@ -496,14 +682,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
496
682
  host: {
497
683
  role: 'menuitem',
498
684
  tabindex: '-1',
499
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
500
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
685
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
686
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
501
687
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
502
688
  '[attr.data-label]': 'label() ?? undefined',
503
689
  '(focus)': 'onFocus()',
504
690
  '(blur)': 'onBlur()',
505
691
  '(pointermove)': 'onPointerMove($event)',
506
692
  '(pointerleave)': 'onPointerLeave($event)',
693
+ '(mouseup)': 'onMouseUp($event)',
507
694
  '(click)': 'onItemClick()',
508
695
  '(keydown.enter)': 'onActivate($event)',
509
696
  '(keydown.space)': 'onActivate($event)'
@@ -528,9 +715,10 @@ class RdxMenuLinkItem {
528
715
  /** Emits when the item is selected. */
529
716
  this.onSelect = output();
530
717
  this.highlighted = computed(() => this.isFocused(), ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
718
+ this.effectiveDisabled = computed(() => this.disabled() || (this.rootContext?.disabled() ?? false), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
531
719
  }
532
720
  onFocus() {
533
- if (!this.disabled()) {
721
+ if (!this.effectiveDisabled()) {
534
722
  this.isFocused.set(true);
535
723
  }
536
724
  }
@@ -538,13 +726,13 @@ class RdxMenuLinkItem {
538
726
  this.isFocused.set(false);
539
727
  }
540
728
  onPointerMove(event) {
541
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
729
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
542
730
  return;
543
731
  }
544
732
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
545
733
  return;
546
734
  }
547
- if (document.activeElement !== this.elementRef.nativeElement) {
735
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
548
736
  this.elementRef.nativeElement.focus({ preventScroll: true });
549
737
  }
550
738
  }
@@ -552,30 +740,37 @@ class RdxMenuLinkItem {
552
740
  if (event.pointerType !== 'mouse') {
553
741
  return;
554
742
  }
555
- if (document.activeElement === this.elementRef.nativeElement) {
743
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
556
744
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
557
745
  }
558
746
  }
559
747
  onItemClick(event) {
560
- if (this.disabled()) {
748
+ if (this.effectiveDisabled()) {
561
749
  event.preventDefault();
562
750
  return;
563
751
  }
564
752
  this.onSelect.emit();
565
753
  if (this.closeOnClick())
566
- this.rootContext?.close();
754
+ this.rootContext?.closeEntireMenu();
755
+ }
756
+ onMouseUp(event) {
757
+ if (this.effectiveDisabled() || event.button !== 0 || !this.rootContext?.allowMouseUpTrigger()) {
758
+ return;
759
+ }
760
+ this.rootContext.setAllowMouseUpTrigger(false);
761
+ this.elementRef.nativeElement.click();
567
762
  }
568
763
  onActivate(event) {
569
- if (this.disabled()) {
764
+ if (this.effectiveDisabled()) {
570
765
  event.preventDefault();
571
766
  return;
572
767
  }
573
768
  this.onSelect.emit();
574
769
  if (this.closeOnClick())
575
- this.rootContext?.close();
770
+ this.rootContext?.closeEntireMenu();
576
771
  }
577
772
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuLinkItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
578
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuLinkItem, isStandalone: true, selector: "a[rdxMenuLinkItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect" }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "click": "onItemClick($event)", "keydown.enter": "onActivate($event)" }, properties: { "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.aria-disabled": "disabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, exportAs: ["rdxMenuLinkItem"], ngImport: i0 }); }
773
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuLinkItem, isStandalone: true, selector: "a[rdxMenuLinkItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect" }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "mouseup": "onMouseUp($event)", "click": "onItemClick($event)", "keydown.enter": "onActivate($event)" }, properties: { "attr.data-disabled": "effectiveDisabled() ? \"\" : undefined", "attr.aria-disabled": "effectiveDisabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, exportAs: ["rdxMenuLinkItem"], ngImport: i0 }); }
579
774
  }
580
775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuLinkItem, decorators: [{
581
776
  type: Directive,
@@ -585,62 +780,64 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
585
780
  host: {
586
781
  role: 'menuitem',
587
782
  tabindex: '-1',
588
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
589
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
783
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
784
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
590
785
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
591
786
  '[attr.data-label]': 'label() ?? undefined',
592
787
  '(focus)': 'onFocus()',
593
788
  '(blur)': 'onBlur()',
594
789
  '(pointermove)': 'onPointerMove($event)',
595
790
  '(pointerleave)': 'onPointerLeave($event)',
791
+ '(mouseup)': 'onMouseUp($event)',
596
792
  '(click)': 'onItemClick($event)',
597
793
  '(keydown.enter)': 'onActivate($event)'
598
794
  }
599
795
  }]
600
796
  }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], closeOnClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnClick", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], onSelect: [{ type: i0.Output, args: ["onSelect"] }] } });
601
797
 
602
- /** Selector for focusable menu items within the popup. */
603
- const ITEM_SELECTOR = [
798
+ /** Selector for focusable menu items within a popup. */
799
+ const RDX_MENU_ITEM_SELECTOR = [
604
800
  '[rdxMenuItem]:not([data-disabled])',
605
801
  '[rdxMenuCheckboxItem]:not([data-disabled])',
606
802
  '[rdxMenuRadioItem]:not([data-disabled])',
607
803
  '[rdxMenuLinkItem]:not([data-disabled])',
608
804
  '[rdxMenuSubTrigger]:not([data-disabled])'
609
805
  ].join(',');
610
- function getFocusableItems(popup) {
611
- // Exclude items that belong to a nested child popup (submenu).
612
- return Array.from(popup.querySelectorAll(ITEM_SELECTOR)).filter((item) => item.closest('[rdxMenuPopup]') === popup);
806
+ function getFocusableMenuItems(popup) {
807
+ return Array.from(popup.querySelectorAll(RDX_MENU_ITEM_SELECTOR)).filter((item) => item.closest('[rdxMenuPopup]') === popup);
613
808
  }
809
+
614
810
  /**
615
811
  * A container for the menu contents.
616
812
  */
617
813
  class RdxMenuPopup {
618
814
  constructor() {
619
815
  this.rootContext = injectRdxMenuRootContext();
620
- this.dismissableLayer = inject(RdxDismissableLayer);
816
+ this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
817
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
818
+ this.focusManager = inject(RdxFloatingFocusManager);
621
819
  this.focusScope = inject(RdxFocusScope);
622
820
  this.wrapper = inject(RdxPopperContentWrapper, { optional: true });
623
821
  this.elementRef = inject(ElementRef);
624
- this.dismissableLayersContext = inject(RdxDismissableLayersContextToken);
625
822
  this.search = '';
626
823
  this.align = computed(() => this.wrapper?.placedAlign(), ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
627
824
  this.side = computed(() => this.wrapper?.placedSide(), ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
628
825
  /**
629
826
  * Event handler called when the escape key is pressed. Can be prevented.
630
827
  */
631
- this.escapeKeyDown = outputFromObservable(outputToObservable(this.dismissableLayer.escapeKeyDown));
828
+ this.escapeKeyDown = output();
632
829
  /**
633
830
  * Event handler called when a pointerdown event happens outside of the popup. Can be prevented.
634
831
  */
635
- this.pointerDownOutside = outputFromObservable(outputToObservable(this.dismissableLayer.pointerDownOutside));
832
+ this.pointerDownOutside = output();
636
833
  /**
637
834
  * Event handler called when focus moves outside of the popup. Can be prevented.
638
835
  */
639
- this.focusOutside = outputFromObservable(outputToObservable(this.dismissableLayer.focusOutside));
836
+ this.focusOutside = output();
640
837
  /**
641
838
  * Event handler called when an interaction happens outside of the popup. Can be prevented.
642
839
  */
643
- this.interactOutside = outputFromObservable(outputToObservable(this.dismissableLayer.interactOutside));
840
+ this.interactOutside = output();
644
841
  /**
645
842
  * Event handler called before focus moves into the popup. Can be prevented.
646
843
  */
@@ -649,6 +846,27 @@ class RdxMenuPopup {
649
846
  * Event handler called before focus returns after the popup is removed. Can be prevented.
650
847
  */
651
848
  this.closeAutoFocus = outputFromObservable(outputToObservable(this.focusScope.unmountAutoFocus));
849
+ // Page scroll lock (Base UI `MenuPositioner`): only while **open** and **modal**, and a hover-open
850
+ // dropdown / context menu does NOT lock (a menubar menu always does when modal). A submenu never
851
+ // locks — its `modal` is already effectively false. For a **touch** open the anchored helper only
852
+ // locks when the popup is effectively viewport-width (ADR 0016 §3), so a small menu stays
853
+ // swipe-to-dismissable on mobile.
854
+ useAnchoredScrollLock(computed(() => {
855
+ if (!this.rootContext.isOpen() || !this.rootContext.modal()) {
856
+ return false;
857
+ }
858
+ if (this.rootContext.parentType() === 'menubar') {
859
+ return true;
860
+ }
861
+ return this.rootContext.lastOpenChangeReason() !== 'trigger-hover';
862
+ }), {
863
+ touchOpen: () => this.rootContext.openedByTouch(),
864
+ element: () => this.elementRef.nativeElement
865
+ });
866
+ // The popup is this layer's floating element (the inside-surface for containment checks). A
867
+ // submenu is a child node in the shared tree, so the capability's logical containment treats an
868
+ // open submenu as "inside" its parent automatically — replacing the legacy `branches` registry.
869
+ this.floatingContext.setFloatingElement(this.elementRef.nativeElement);
652
870
  const unregister = this.rootContext.registerTransitionElement(this.elementRef.nativeElement);
653
871
  const unregisterPopup = this.rootContext.registerPopup(this.elementRef.nativeElement);
654
872
  inject(DestroyRef).onDestroy(() => {
@@ -656,34 +874,52 @@ class RdxMenuPopup {
656
874
  unregisterPopup();
657
875
  clearTimeout(this.searchTimer);
658
876
  });
659
- effect((onCleanup) => {
660
- if (!this.rootContext.isSubmenu()) {
877
+ // Base UI moves focus into a keyboard-opened submenu via its list-navigation layer, not via the
878
+ // focus manager (`initialFocus={false}` for submenus). In Angular, the popup itself is the first
879
+ // point where the submenu DOM definitely exists, so complete the keyboard handoff here.
880
+ effect(() => {
881
+ if (!this.rootContext.isOpen() ||
882
+ this.rootContext.parentType() !== 'menu' ||
883
+ this.rootContext.openInteractionType() !== 'keyboard') {
661
884
  return;
662
885
  }
663
- const element = this.elementRef.nativeElement;
664
- this.dismissableLayersContext.branches.update((branches) => [...branches, element]);
665
- onCleanup(() => {
666
- this.dismissableLayersContext.branches.update((branches) => branches.filter((branch) => branch !== element));
667
- });
886
+ this.scheduleSubmenuKeyboardFocus();
668
887
  });
669
- this.dismissableLayer.dismiss.subscribe(() => {
670
- this.rootContext.close();
888
+ // Dismissal (ADR 0015): Escape, an outside press, or focus moving outside closes the menu.
889
+ // Escape is owned by the capability (a document-level listener — it works regardless of where
890
+ // focus currently sits, matching Base UI `useDismiss`). Deepest-first: a non-bubbling layer
891
+ // yields to an open child, so Escape closes only the innermost menu — unless `closeParentOnEsc`
892
+ // makes it bubble up the whole chain.
893
+ new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
894
+ // A disabled menu does not dismiss (Base UI `useDismiss({ enabled: !disabled })`): if an open
895
+ // menu becomes disabled it stays put rather than closing on Escape / outside-press / focus-out.
896
+ enabled: () => !this.rootContext.disabled(),
897
+ escapeKey: () => true,
898
+ escapeKeyBubbles: () => this.rootContext.closeParentOnEsc(),
899
+ outsidePress: () => true,
900
+ focusOutside: () => false,
901
+ onEscapeKeyDown: (event) => this.escapeKeyDown.emit(event),
902
+ onPointerDownOutside: (event) => {
903
+ this.pointerDownOutside.emit(event);
904
+ this.interactOutside.emit(event);
905
+ },
906
+ onDismiss: (reason, event) => {
907
+ // Forward the dismissal reason + native event into the menu's open-change channel.
908
+ const menuReason = reason === 'escape-key' ? 'escape-key' : 'outside-press';
909
+ this.rootContext.close(menuReason, event);
910
+ // Escape should restore focus synchronously to the trigger / submenu trigger so the
911
+ // menu chain remains keyboard-stable even before the scope's queued unmount return-focus.
912
+ if (reason === 'escape-key') {
913
+ this.rootContext.trigger()?.focus({ preventScroll: true });
914
+ }
915
+ }
671
916
  });
672
- // Move focus into the popup when the menu opens unless the opener suppressed it
673
- // (e.g. menubar hover-switching, where focus stays on the trigger).
674
- effect(() => {
675
- const autoFocus = this.rootContext.autoFocus();
676
- if (this.rootContext.isOpen() && autoFocus) {
677
- requestAnimationFrame(() => {
678
- // `'popup'` focuses the container without highlighting an item (pointer opening).
679
- if (autoFocus === 'popup') {
680
- this.elementRef.nativeElement.focus({ preventScroll: true });
681
- return;
682
- }
683
- const items = getFocusableItems(this.elementRef.nativeElement);
684
- const item = autoFocus === 'last' ? items[items.length - 1] : items[0];
685
- item?.focus({ preventScroll: true });
686
- });
917
+ // Focus-out close is owned by the floating focus manager, matching Base UI's MenuPopup.
918
+ this.focusManager.focusOut.subscribe((event) => {
919
+ this.focusOutside.emit(event);
920
+ this.interactOutside.emit(event);
921
+ if (!event.defaultPrevented) {
922
+ this.rootContext.close('focus-out', event);
687
923
  }
688
924
  });
689
925
  }
@@ -696,8 +932,8 @@ class RdxMenuPopup {
696
932
  }
697
933
  handleKeydown(event) {
698
934
  const el = this.elementRef.nativeElement;
699
- const items = getFocusableItems(el);
700
- const current = document.activeElement;
935
+ const items = getFocusableMenuItems(el);
936
+ const current = el.ownerDocument.activeElement;
701
937
  const currentIndex = items.indexOf(current);
702
938
  switch (event.key) {
703
939
  case 'ArrowDown': {
@@ -745,6 +981,13 @@ class RdxMenuPopup {
745
981
  }
746
982
  break;
747
983
  }
984
+ if (this.rootContext.dir() === 'rtl') {
985
+ if (this.rootContext.handlePopupArrowNavigation(-1)) {
986
+ event.preventDefault();
987
+ event.stopPropagation();
988
+ }
989
+ break;
990
+ }
748
991
  // Close this popup and return focus to the trigger (used by submenus).
749
992
  event.preventDefault();
750
993
  event.stopPropagation();
@@ -753,22 +996,23 @@ class RdxMenuPopup {
753
996
  break;
754
997
  }
755
998
  case 'ArrowRight': {
756
- if (this.rootContext.handlePopupArrowNavigation(1)) {
999
+ const trigger = this.rootContext.trigger();
1000
+ if (trigger?.hasAttribute('rdxMenuSubTrigger') && this.rootContext.dir() === 'rtl') {
757
1001
  event.preventDefault();
758
1002
  event.stopPropagation();
1003
+ this.rootContext.close();
1004
+ trigger.focus({ preventScroll: true });
1005
+ break;
759
1006
  }
760
- break;
761
- }
762
- case 'Escape': {
763
- event.preventDefault();
764
- event.stopPropagation();
765
- this.rootContext.close();
766
- if (this.rootContext.isSubmenu() && this.rootContext.closeParentOnEsc()) {
767
- this.rootContext.closeParent();
1007
+ if (this.rootContext.handlePopupArrowNavigation(1)) {
1008
+ event.preventDefault();
1009
+ event.stopPropagation();
768
1010
  }
769
- this.rootContext.trigger()?.focus({ preventScroll: true });
770
1011
  break;
771
1012
  }
1013
+ // Escape is owned by the dismissal capability (a document-level listener that works
1014
+ // regardless of focus position); it closes the menu, restores focus to the trigger, and
1015
+ // cascades up the chain when `closeParentOnEsc` is set.
772
1016
  case 'Tab': {
773
1017
  // Close on tab to allow natural tab navigation
774
1018
  this.rootContext.close();
@@ -798,35 +1042,136 @@ class RdxMenuPopup {
798
1042
  }
799
1043
  }
800
1044
  }
1045
+ scheduleSubmenuKeyboardFocus(attempt = 0) {
1046
+ const view = this.elementRef.nativeElement.ownerDocument.defaultView ?? globalThis;
1047
+ view.requestAnimationFrame(() => this.applySubmenuKeyboardFocus(attempt));
1048
+ }
1049
+ applySubmenuKeyboardFocus(attempt) {
1050
+ const maxAttempts = 10;
1051
+ const popup = this.elementRef.nativeElement;
1052
+ if (!this.rootContext.isOpen() || this.rootContext.parentType() !== 'menu') {
1053
+ return;
1054
+ }
1055
+ const activeElement = popup.ownerDocument.activeElement;
1056
+ if (activeElement && popup.contains(activeElement)) {
1057
+ return;
1058
+ }
1059
+ const items = getFocusableMenuItems(popup);
1060
+ if (items.length === 0) {
1061
+ if (attempt < maxAttempts) {
1062
+ this.scheduleSubmenuKeyboardFocus(attempt + 1);
1063
+ }
1064
+ return;
1065
+ }
1066
+ items[0]?.focus({ preventScroll: true });
1067
+ }
801
1068
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
802
1069
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuPopup, isStandalone: true, selector: "[rdxMenuPopup]", outputs: { escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", openAutoFocus: "openAutoFocus", closeAutoFocus: "closeAutoFocus" }, host: { attributes: { "role": "menu", "tabindex": "-1" }, listeners: { "keydown": "handleKeydown($event)", "rdx-menu-close-parent": "handleCloseParent($event)" }, properties: { "attr.aria-orientation": "rootContext.orientation()", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"", "attr.data-starting-style": "rootContext.transitionStatus() === \"starting\" ? \"\" : undefined", "attr.data-ending-style": "rootContext.transitionStatus() === \"ending\" ? \"\" : undefined", "attr.data-align": "align()", "attr.data-side": "side()" } }, providers: [
803
- provideRdxDismissableLayerConfig(() => {
1070
+ provideFloatingFocusManagerConfig(() => {
804
1071
  const rootContext = injectRdxMenuRootContext();
1072
+ const popup = inject(ElementRef).nativeElement;
805
1073
  return {
806
- disableOutsidePointerEvents: computed(() => rootContext.modal())
1074
+ // Only a (modal) **context menu** traps focus — Base UI's `FloatingFocusManager modal` is
1075
+ // true for `context-menu` alone. Other menus stay non-modal and close on focus-out.
1076
+ modal: () => rootContext.parentType() === 'context-menu',
1077
+ // The manager follows mounted/open lifecycle, not menu disabled state. Dismissal remains
1078
+ // disabled separately below; focus/marker policy should not disappear if a menu becomes
1079
+ // disabled while open, or if `preventUnmountOnClose()` keeps it mounted after close.
1080
+ enabled: () => rootContext.present(),
1081
+ restoreFocus: () => true,
1082
+ previousFocusableElement: () => rootContext.trigger() ?? null,
1083
+ beforeContentFocusGuardRef: () => (element) => rootContext.setBeforeContentFocusGuard(element),
1084
+ // Base UI's submenu policy: a submenu mount does not steal focus from its trigger.
1085
+ // Root menus still choose first / last item vs popup container from the menu's own
1086
+ // open policy (`autoFocus`), but the decision now lives in the focus manager instead
1087
+ // of a separate popup effect.
1088
+ initialFocus: () => {
1089
+ if (!rootContext.isOpen() || rootContext.parentType() === 'menu') {
1090
+ return false;
1091
+ }
1092
+ const autoFocus = rootContext.autoFocus();
1093
+ if (autoFocus === false) {
1094
+ return false;
1095
+ }
1096
+ if (autoFocus === 'popup') {
1097
+ return popup;
1098
+ }
1099
+ return () => {
1100
+ const items = getFocusableMenuItems(popup);
1101
+ return autoFocus === 'last' ? (items.at(-1) ?? popup) : (items[0] ?? popup);
1102
+ };
1103
+ },
1104
+ returnFocus: () => {
1105
+ const parentType = rootContext.parentType();
1106
+ if (rootContext.trigger() || parentType === undefined || parentType === 'context-menu') {
1107
+ return true;
1108
+ }
1109
+ if (parentType === 'menubar' && rootContext.lastOpenChangeReason() !== 'outside-press') {
1110
+ return true;
1111
+ }
1112
+ return false;
1113
+ },
1114
+ openInteractionType: () => rootContext.openInteractionType(),
1115
+ closeInteractionType: () => rootContext.closeInteractionType()
807
1116
  };
808
- }),
809
- provideRdxFocusScopeConfig(() => ({
810
- trapped: signal(false)
811
- }))
812
- ], exportAs: ["rdxMenuPopup"], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i2.RdxDismissableLayer }, { directive: i3.RdxFocusScope }], ngImport: i0 }); }
1117
+ })
1118
+ ], exportAs: ["rdxMenuPopup"], hostDirectives: [{ directive: i1.RdxPopperContent }, { directive: i2.RdxFloatingNodeRegistration }, { directive: i3.RdxFloatingFocusManager }], ngImport: i0 }); }
813
1119
  }
814
1120
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPopup, decorators: [{
815
1121
  type: Directive,
816
1122
  args: [{
817
1123
  selector: '[rdxMenuPopup]',
818
1124
  exportAs: 'rdxMenuPopup',
819
- hostDirectives: [RdxPopperContent, RdxDismissableLayer, RdxFocusScope],
1125
+ hostDirectives: [RdxPopperContent, RdxFloatingNodeRegistration, RdxFloatingFocusManager],
820
1126
  providers: [
821
- provideRdxDismissableLayerConfig(() => {
1127
+ provideFloatingFocusManagerConfig(() => {
822
1128
  const rootContext = injectRdxMenuRootContext();
1129
+ const popup = inject(ElementRef).nativeElement;
823
1130
  return {
824
- disableOutsidePointerEvents: computed(() => rootContext.modal())
1131
+ // Only a (modal) **context menu** traps focus — Base UI's `FloatingFocusManager modal` is
1132
+ // true for `context-menu` alone. Other menus stay non-modal and close on focus-out.
1133
+ modal: () => rootContext.parentType() === 'context-menu',
1134
+ // The manager follows mounted/open lifecycle, not menu disabled state. Dismissal remains
1135
+ // disabled separately below; focus/marker policy should not disappear if a menu becomes
1136
+ // disabled while open, or if `preventUnmountOnClose()` keeps it mounted after close.
1137
+ enabled: () => rootContext.present(),
1138
+ restoreFocus: () => true,
1139
+ previousFocusableElement: () => rootContext.trigger() ?? null,
1140
+ beforeContentFocusGuardRef: () => (element) => rootContext.setBeforeContentFocusGuard(element),
1141
+ // Base UI's submenu policy: a submenu mount does not steal focus from its trigger.
1142
+ // Root menus still choose first / last item vs popup container from the menu's own
1143
+ // open policy (`autoFocus`), but the decision now lives in the focus manager instead
1144
+ // of a separate popup effect.
1145
+ initialFocus: () => {
1146
+ if (!rootContext.isOpen() || rootContext.parentType() === 'menu') {
1147
+ return false;
1148
+ }
1149
+ const autoFocus = rootContext.autoFocus();
1150
+ if (autoFocus === false) {
1151
+ return false;
1152
+ }
1153
+ if (autoFocus === 'popup') {
1154
+ return popup;
1155
+ }
1156
+ return () => {
1157
+ const items = getFocusableMenuItems(popup);
1158
+ return autoFocus === 'last' ? (items.at(-1) ?? popup) : (items[0] ?? popup);
1159
+ };
1160
+ },
1161
+ returnFocus: () => {
1162
+ const parentType = rootContext.parentType();
1163
+ if (rootContext.trigger() || parentType === undefined || parentType === 'context-menu') {
1164
+ return true;
1165
+ }
1166
+ if (parentType === 'menubar' && rootContext.lastOpenChangeReason() !== 'outside-press') {
1167
+ return true;
1168
+ }
1169
+ return false;
1170
+ },
1171
+ openInteractionType: () => rootContext.openInteractionType(),
1172
+ closeInteractionType: () => rootContext.closeInteractionType()
825
1173
  };
826
- }),
827
- provideRdxFocusScopeConfig(() => ({
828
- trapped: signal(false)
829
- }))
1174
+ })
830
1175
  ],
831
1176
  host: {
832
1177
  role: 'menu',
@@ -856,7 +1201,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
856
1201
  */
857
1202
  class RdxMenuPortal {
858
1203
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
859
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuPortal, isStandalone: true, selector: "ng-template[rdxMenuPortal]", providers: [provideRdxPresenceContext(() => ({ present: injectRdxMenuRootContext().isOpen }))], exportAs: ["rdxMenuPortal"], hostDirectives: [{ directive: i1$1.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
1204
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuPortal, isStandalone: true, selector: "ng-template[rdxMenuPortal]", providers: [provideRdxPresenceContext(() => ({ present: injectRdxMenuRootContext().present }))], exportAs: ["rdxMenuPortal"], hostDirectives: [{ directive: i1$1.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
860
1205
  }
861
1206
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPortal, decorators: [{
862
1207
  type: Directive,
@@ -864,7 +1209,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
864
1209
  selector: 'ng-template[rdxMenuPortal]',
865
1210
  exportAs: 'rdxMenuPortal',
866
1211
  hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
867
- providers: [provideRdxPresenceContext(() => ({ present: injectRdxMenuRootContext().isOpen }))]
1212
+ providers: [provideRdxPresenceContext(() => ({ present: injectRdxMenuRootContext().present }))]
868
1213
  }]
869
1214
  }] });
870
1215
  /**
@@ -874,9 +1219,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
874
1219
  class RdxMenuPortalMisuseGuard {
875
1220
  constructor() {
876
1221
  if (isDevMode()) {
877
- throw new Error('[rdxMenuPortal] is a structural directive. ' +
878
- 'Use `*rdxMenuPortal` on the positioner element or `<ng-template rdxMenuPortal>`. ' +
879
- 'See https://radix-ng.com/components/menu.md');
1222
+ rdxDevError('menu/portal-on-element', '`rdxMenuPortal` is a structural directive. ' +
1223
+ 'Use `*rdxMenuPortal` on the positioner element or `<ng-template rdxMenuPortal>`.', 'components/menu');
880
1224
  }
881
1225
  }
882
1226
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
@@ -889,74 +1233,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
889
1233
  }]
890
1234
  }], ctorParameters: () => [] });
891
1235
 
1236
+ /** Marker attribute on the menu's internal backdrop element. */
1237
+ const MENU_INTERNAL_BACKDROP_ATTR = 'data-rdx-menu-internal-backdrop';
1238
+ /** The element that stays interactive through the backdrop (Base UI `backdropCutout`). */
1239
+ function cutoutElement(rootContext) {
1240
+ const type = rootContext.parentType();
1241
+ const trigger = rootContext.trigger() ?? null;
1242
+ if (type === 'menubar') {
1243
+ // Keep the whole menubar interactive so hover/click switching between its menus still works.
1244
+ return trigger?.closest('[rdxMenubarRoot]') ?? trigger;
1245
+ }
1246
+ if (type === 'context-menu') {
1247
+ return null; // right-click anywhere — no cutout
1248
+ }
1249
+ return trigger; // standalone dropdown — keep its trigger clickable (toggle-close)
1250
+ }
1251
+ /**
1252
+ * The menu's modal **internal backdrop** (finding #1) — a thin wrapper over the shared
1253
+ * {@link setupInternalBackdrop}. Rendered for a modal Menu / Context Menu / Menubar menu (Base UI
1254
+ * `MenuPositioner.tsx`): never for a submenu, and not for a hover-opened dropdown / context menu.
1255
+ */
1256
+ function setupMenuInternalBackdrop(positioner, rootContext, injector) {
1257
+ setupInternalBackdrop(positioner, injector, {
1258
+ marker: MENU_INTERNAL_BACKDROP_ATTR,
1259
+ isOpen: () => rootContext.isOpen(),
1260
+ cutout: () => cutoutElement(rootContext),
1261
+ shouldRender: () => {
1262
+ const type = rootContext.parentType();
1263
+ if (type === 'menu' || !rootContext.modal()) {
1264
+ return false; // submenus and non-modal menus get no backdrop
1265
+ }
1266
+ if (type === 'menubar') {
1267
+ return true; // a modal menubar menu always gets one (even on hover-switch)
1268
+ }
1269
+ // standalone / context menu: suppressed for a hover-open (Base UI excludes `triggerHover`).
1270
+ return rootContext.lastOpenChangeReason() !== 'trigger-hover';
1271
+ }
1272
+ });
1273
+ }
1274
+
892
1275
  /**
893
1276
  * Positions the menu against its trigger.
1277
+ *
1278
+ * A "thin" positioner (ADR 0012): it inherits the popper positioning surface (inputs, `placed`
1279
+ * output, unified vars + placement attrs) from {@link RdxPopperContentWrapper} and adds the menu
1280
+ * defaults, the open/closed state attributes, and the deprecated `--radix-menu-*` aliases.
894
1281
  */
895
- class RdxMenuPositioner {
1282
+ class RdxMenuPositioner extends RdxPopperContentWrapper {
896
1283
  constructor() {
1284
+ super();
897
1285
  this.rootContext = injectRdxMenuRootContext();
898
- this.wrapper = inject(RdxPopperContentWrapper);
899
- /**
900
- * An element to position the popup against. Defaults to the trigger.
901
- */
902
- this.anchor = input(...(ngDevMode ? [undefined, { debugName: "anchor" }] : /* istanbul ignore next */ []));
903
- /**
904
- * The preferred side of the trigger to render against when open.
905
- */
906
- this.side = input('bottom', ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
907
- /**
908
- * Distance between the trigger and the popup in pixels.
909
- */
910
- this.sideOffset = input(0, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
911
- /**
912
- * How to align the popup relative to the specified side.
913
- */
914
- this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
915
- /**
916
- * An offset in pixels from the `start` or `end` alignment options.
917
- */
918
- this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
919
- /**
920
- * Minimum distance to maintain between the arrow and the edges of the popup.
921
- */
922
- this.arrowPadding = input(5, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
923
- /**
924
- * Whether to override side and alignment preferences to prevent collisions.
925
- */
926
- this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
927
- /**
928
- * The element used as the collision boundary.
929
- */
930
- this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
931
- /**
932
- * Distance in pixels from the boundary edges where collision detection should occur.
933
- */
934
- this.collisionPadding = input(5, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
935
- /**
936
- * The sticky behavior on the alignment axis.
937
- */
938
- this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
939
- /**
940
- * Whether to hide the popup when the trigger becomes fully occluded.
941
- */
942
- this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
943
- /**
944
- * The CSS position strategy used by Floating UI.
945
- */
946
- this.positionStrategy = input('fixed', ...(ngDevMode ? [{ debugName: "positionStrategy" }] : /* istanbul ignore next */ []));
947
- /**
948
- * Whether to update position on every animation frame.
949
- */
950
- this.updatePositionStrategy = input('always', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
951
- /**
952
- * Emits when the popup has been placed.
953
- */
954
- this.placed = outputFromObservable(outputToObservable(inject(RdxPopperContentWrapper).placed));
1286
+ this.legacyVars = legacyPopperVars('menu');
1287
+ const injector = inject(Injector);
1288
+ const host = inject(ElementRef).nativeElement;
1289
+ // After the structural portal has relocated this positioner into the portal container, set up the
1290
+ // modal internal backdrop (finding #1) as a sibling before it.
1291
+ afterNextRender(() => setupMenuInternalBackdrop(host, this.rootContext, injector));
955
1292
  }
956
1293
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
957
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuPositioner, isStandalone: true, selector: "[rdxMenuPositioner]", inputs: { anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, sideOffset: { classPropertyName: "sideOffset", publicName: "sideOffset", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, alignOffset: { classPropertyName: "alignOffset", publicName: "alignOffset", isSignal: true, isRequired: false, transformFunction: null }, arrowPadding: { classPropertyName: "arrowPadding", publicName: "arrowPadding", isSignal: true, isRequired: false, transformFunction: null }, avoidCollisions: { classPropertyName: "avoidCollisions", publicName: "avoidCollisions", isSignal: true, isRequired: false, transformFunction: null }, collisionBoundary: { classPropertyName: "collisionBoundary", publicName: "collisionBoundary", isSignal: true, isRequired: false, transformFunction: null }, collisionPadding: { classPropertyName: "collisionPadding", publicName: "collisionPadding", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, hideWhenDetached: { classPropertyName: "hideWhenDetached", publicName: "hideWhenDetached", isSignal: true, isRequired: false, transformFunction: null }, positionStrategy: { classPropertyName: "positionStrategy", publicName: "positionStrategy", isSignal: true, isRequired: false, transformFunction: null }, updatePositionStrategy: { classPropertyName: "updatePositionStrategy", publicName: "updatePositionStrategy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { placed: "placed" }, host: { properties: { "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-anchor-hidden": "wrapper.anchorHidden() ? \"\" : undefined", "attr.data-align": "wrapper.placedAlign()", "attr.data-side": "wrapper.placedSide()", "style": "{\n '--anchor-width': 'var(--radix-popper-anchor-width)',\n '--anchor-height': 'var(--radix-popper-anchor-height)',\n '--available-width': 'var(--radix-popper-available-width)',\n '--available-height': 'var(--radix-popper-available-height)',\n '--positioner-width': 'var(--radix-popper-content-wrapper-width)',\n '--positioner-height': 'var(--radix-popper-content-wrapper-height)',\n '--transform-origin': 'var(--radix-popper-transform-origin)',\n '--radix-menu-content-transform-origin': 'var(--radix-popper-transform-origin)',\n '--radix-menu-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-menu-content-available-height': 'var(--radix-popper-available-height)',\n '--radix-menu-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-menu-trigger-height': 'var(--radix-popper-anchor-height)'\n }" } }, providers: [
1294
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuPositioner, isStandalone: true, selector: "[rdxMenuPositioner]", host: { properties: { "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "style": "legacyVars" } }, providers: [
1295
+ ...provideRdxPopperContentWrapper(RdxMenuPositioner),
958
1296
  provideRdxPopperContentConfig({ arrowPadding: 5, collisionPadding: 5, updatePositionStrategy: 'always' })
959
- ], exportAs: ["rdxMenuPositioner"], hostDirectives: [{ directive: i1.RdxPopperContentWrapper, inputs: ["anchor", "anchor", "side", "side", "sideOffset", "sideOffset", "align", "align", "alignOffset", "alignOffset", "arrowPadding", "arrowPadding", "avoidCollisions", "avoidCollisions", "collisionBoundary", "collisionBoundary", "collisionPadding", "collisionPadding", "sticky", "sticky", "hideWhenDetached", "hideWhenDetached", "positionStrategy", "positionStrategy", "updatePositionStrategy", "updatePositionStrategy"] }], ngImport: i0 }); }
1297
+ ], exportAs: ["rdxMenuPositioner"], usesInheritance: true, ngImport: i0 }); }
960
1298
  }
961
1299
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPositioner, decorators: [{
962
1300
  type: Directive,
@@ -964,92 +1302,88 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
964
1302
  selector: '[rdxMenuPositioner]',
965
1303
  exportAs: 'rdxMenuPositioner',
966
1304
  providers: [
1305
+ ...provideRdxPopperContentWrapper(RdxMenuPositioner),
967
1306
  provideRdxPopperContentConfig({ arrowPadding: 5, collisionPadding: 5, updatePositionStrategy: 'always' })
968
1307
  ],
969
- hostDirectives: [
970
- {
971
- directive: RdxPopperContentWrapper,
972
- inputs: [
973
- 'anchor',
974
- 'side',
975
- 'sideOffset',
976
- 'align',
977
- 'alignOffset',
978
- 'arrowPadding',
979
- 'avoidCollisions',
980
- 'collisionBoundary',
981
- 'collisionPadding',
982
- 'sticky',
983
- 'hideWhenDetached',
984
- 'positionStrategy',
985
- 'updatePositionStrategy'
986
- ]
987
- }
988
- ],
989
1308
  host: {
990
1309
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
991
1310
  '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
992
- '[attr.data-anchor-hidden]': 'wrapper.anchorHidden() ? "" : undefined',
993
- '[attr.data-align]': 'wrapper.placedAlign()',
994
- '[attr.data-side]': 'wrapper.placedSide()',
995
- '[style]': `{
996
- '--anchor-width': 'var(--radix-popper-anchor-width)',
997
- '--anchor-height': 'var(--radix-popper-anchor-height)',
998
- '--available-width': 'var(--radix-popper-available-width)',
999
- '--available-height': 'var(--radix-popper-available-height)',
1000
- '--positioner-width': 'var(--radix-popper-content-wrapper-width)',
1001
- '--positioner-height': 'var(--radix-popper-content-wrapper-height)',
1002
- '--transform-origin': 'var(--radix-popper-transform-origin)',
1003
- '--radix-menu-content-transform-origin': 'var(--radix-popper-transform-origin)',
1004
- '--radix-menu-content-available-width': 'var(--radix-popper-available-width)',
1005
- '--radix-menu-content-available-height': 'var(--radix-popper-available-height)',
1006
- '--radix-menu-trigger-width': 'var(--radix-popper-anchor-width)',
1007
- '--radix-menu-trigger-height': 'var(--radix-popper-anchor-height)'
1008
- }`
1311
+ // `data-side`/`data-align`/`data-anchor-hidden` and the unified vars come from the inherited
1312
+ // wrapper (ADR 0012); only the deprecated `--radix-menu-*` aliases remain, for back-compat.
1313
+ '[style]': 'legacyVars'
1009
1314
  }
1010
1315
  }]
1011
- }], propDecorators: { anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], sideOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOffset", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], alignOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignOffset", required: false }] }], arrowPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPadding", required: false }] }], avoidCollisions: [{ type: i0.Input, args: [{ isSignal: true, alias: "avoidCollisions", required: false }] }], collisionBoundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionBoundary", required: false }] }], collisionPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "collisionPadding", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], hideWhenDetached: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideWhenDetached", required: false }] }], positionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionStrategy", required: false }] }], updatePositionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "updatePositionStrategy", required: false }] }], placed: [{ type: i0.Output, args: ["placed"] }] } });
1316
+ }], ctorParameters: () => [] });
1012
1317
 
1013
1318
  const [injectRdxMenuRadioGroupContext, provideRdxMenuRadioGroupContext] = createContext('RdxMenuRadioGroupContext', 'components/menu');
1014
1319
  const radioGroupContextFactory = () => {
1015
1320
  const instance = inject(RdxMenuRadioGroup);
1016
1321
  return {
1017
1322
  value: instance.value,
1323
+ disabled: instance.disabled,
1018
1324
  selectValue: (v) => instance.selectValue(v)
1019
1325
  };
1020
1326
  };
1327
+ const groupContextFactory = () => {
1328
+ const instance = inject(RdxMenuRadioGroup);
1329
+ return { labelId: instance.labelId };
1330
+ };
1021
1331
  /**
1022
1332
  * Groups radio items in a menu.
1023
1333
  */
1024
1334
  class RdxMenuRadioGroup {
1025
1335
  constructor() {
1336
+ this.hasAppliedDefaultValue = false;
1026
1337
  /**
1027
1338
  * The currently selected value.
1028
1339
  */
1029
1340
  this.value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1341
+ /** The initially selected value for uncontrolled usage. */
1342
+ this.defaultValue = input(undefined, ...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
1343
+ /** Whether all radio items in the group are disabled. */
1344
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1030
1345
  /**
1031
1346
  * Emits when the selected value changes.
1032
1347
  */
1033
1348
  this.onValueChange = output();
1349
+ this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
1350
+ effect(() => {
1351
+ const defaultValue = this.defaultValue();
1352
+ if (!this.hasAppliedDefaultValue && defaultValue !== undefined) {
1353
+ this.hasAppliedDefaultValue = true;
1354
+ this.value.set(defaultValue);
1355
+ }
1356
+ });
1034
1357
  }
1035
1358
  selectValue(newValue) {
1359
+ if (this.disabled()) {
1360
+ return;
1361
+ }
1036
1362
  this.value.set(newValue);
1037
1363
  this.onValueChange.emit(newValue);
1038
1364
  }
1039
1365
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1040
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuRadioGroup, isStandalone: true, selector: "[rdxMenuRadioGroup]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onValueChange: "onValueChange" }, host: { attributes: { "role": "group" } }, providers: [provideRdxMenuRadioGroupContext(radioGroupContextFactory)], exportAs: ["rdxMenuRadioGroup"], ngImport: i0 }); }
1366
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuRadioGroup, isStandalone: true, selector: "[rdxMenuRadioGroup]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onValueChange: "onValueChange" }, host: { attributes: { "role": "group" }, properties: { "attr.aria-labelledby": "labelId()", "attr.data-disabled": "disabled() ? \"\" : undefined" } }, providers: [
1367
+ provideRdxMenuRadioGroupContext(radioGroupContextFactory),
1368
+ provideRdxMenuGroupContext(groupContextFactory)
1369
+ ], exportAs: ["rdxMenuRadioGroup"], ngImport: i0 }); }
1041
1370
  }
1042
1371
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioGroup, decorators: [{
1043
1372
  type: Directive,
1044
1373
  args: [{
1045
1374
  selector: '[rdxMenuRadioGroup]',
1046
1375
  exportAs: 'rdxMenuRadioGroup',
1047
- providers: [provideRdxMenuRadioGroupContext(radioGroupContextFactory)],
1376
+ providers: [
1377
+ provideRdxMenuRadioGroupContext(radioGroupContextFactory),
1378
+ provideRdxMenuGroupContext(groupContextFactory)
1379
+ ],
1048
1380
  host: {
1049
- role: 'group'
1381
+ role: 'group',
1382
+ '[attr.aria-labelledby]': 'labelId()',
1383
+ '[attr.data-disabled]': 'disabled() ? "" : undefined'
1050
1384
  }
1051
1385
  }]
1052
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }] } });
1386
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], onValueChange: [{ type: i0.Output, args: ["onValueChange"] }] } });
1053
1387
 
1054
1388
  const [injectRdxMenuRadioItemContext, provideRdxMenuRadioItemContext] = createContext('RdxMenuRadioItemContext', 'components/menu');
1055
1389
  const radioItemContextFactory = () => {
@@ -1079,10 +1413,11 @@ class RdxMenuRadioItem {
1079
1413
  this.onSelect = output();
1080
1414
  this.checked = computed(() => this.radioGroupContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
1081
1415
  this.highlighted = computed(() => this.isFocused(), ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
1416
+ this.effectiveDisabled = computed(() => this.disabled() || this.radioGroupContext.disabled() || (this.rootContext?.disabled() ?? false), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
1082
1417
  this.getCheckedState = getCheckedState;
1083
1418
  }
1084
1419
  onFocus() {
1085
- if (!this.disabled()) {
1420
+ if (!this.effectiveDisabled()) {
1086
1421
  this.isFocused.set(true);
1087
1422
  }
1088
1423
  }
@@ -1090,13 +1425,13 @@ class RdxMenuRadioItem {
1090
1425
  this.isFocused.set(false);
1091
1426
  }
1092
1427
  onPointerMove(event) {
1093
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
1428
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
1094
1429
  return;
1095
1430
  }
1096
1431
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
1097
1432
  return;
1098
1433
  }
1099
- if (document.activeElement !== this.elementRef.nativeElement) {
1434
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
1100
1435
  this.elementRef.nativeElement.focus({ preventScroll: true });
1101
1436
  }
1102
1437
  }
@@ -1104,18 +1439,25 @@ class RdxMenuRadioItem {
1104
1439
  if (event.pointerType !== 'mouse') {
1105
1440
  return;
1106
1441
  }
1107
- if (document.activeElement === this.elementRef.nativeElement) {
1442
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
1108
1443
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
1109
1444
  }
1110
1445
  }
1111
1446
  onItemClick() {
1112
- if (this.disabled()) {
1447
+ if (this.effectiveDisabled()) {
1113
1448
  return;
1114
1449
  }
1115
1450
  this.selectItem();
1116
1451
  }
1452
+ onMouseUp(event) {
1453
+ if (this.effectiveDisabled() || event.button !== 0 || !this.rootContext?.allowMouseUpTrigger()) {
1454
+ return;
1455
+ }
1456
+ this.rootContext.setAllowMouseUpTrigger(false);
1457
+ this.elementRef.nativeElement.click();
1458
+ }
1117
1459
  onActivate(event) {
1118
- if (this.disabled()) {
1460
+ if (this.effectiveDisabled()) {
1119
1461
  return;
1120
1462
  }
1121
1463
  event.preventDefault();
@@ -1126,10 +1468,10 @@ class RdxMenuRadioItem {
1126
1468
  this.radioGroupContext.selectValue(v);
1127
1469
  this.onSelect.emit(v);
1128
1470
  if (this.closeOnClick())
1129
- this.rootContext?.close();
1471
+ this.rootContext?.closeEntireMenu();
1130
1472
  }
1131
1473
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1132
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuRadioItem, isStandalone: true, selector: "[rdxMenuRadioItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect" }, host: { attributes: { "role": "menuitemradio", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "click": "onItemClick()", "keydown.enter": "onActivate($event)", "keydown.space": "onActivate($event)" }, properties: { "attr.aria-checked": "checked()", "attr.data-state": "getCheckedState(checked())", "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.aria-disabled": "disabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, providers: [provideRdxMenuRadioItemContext(radioItemContextFactory)], exportAs: ["rdxMenuRadioItem"], ngImport: i0 }); }
1474
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuRadioItem, isStandalone: true, selector: "[rdxMenuRadioItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnClick: { classPropertyName: "closeOnClick", publicName: "closeOnClick", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect" }, host: { attributes: { "role": "menuitemradio", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "mouseup": "onMouseUp($event)", "click": "onItemClick()", "keydown.enter": "onActivate($event)", "keydown.space": "onActivate($event)" }, properties: { "attr.aria-checked": "checked()", "attr.data-state": "getCheckedState(checked())", "attr.data-disabled": "effectiveDisabled() ? \"\" : undefined", "attr.aria-disabled": "effectiveDisabled() ? true : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, providers: [provideRdxMenuRadioItemContext(radioItemContextFactory)], exportAs: ["rdxMenuRadioItem"], ngImport: i0 }); }
1133
1475
  }
1134
1476
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioItem, decorators: [{
1135
1477
  type: Directive,
@@ -1142,14 +1484,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1142
1484
  tabindex: '-1',
1143
1485
  '[attr.aria-checked]': 'checked()',
1144
1486
  '[attr.data-state]': 'getCheckedState(checked())',
1145
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
1146
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
1487
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
1488
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
1147
1489
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
1148
1490
  '[attr.data-label]': 'label() ?? undefined',
1149
1491
  '(focus)': 'onFocus()',
1150
1492
  '(blur)': 'onBlur()',
1151
1493
  '(pointermove)': 'onPointerMove($event)',
1152
1494
  '(pointerleave)': 'onPointerLeave($event)',
1495
+ '(mouseup)': 'onMouseUp($event)',
1153
1496
  '(click)': 'onItemClick()',
1154
1497
  '(keydown.enter)': 'onActivate($event)',
1155
1498
  '(keydown.space)': 'onActivate($event)'
@@ -1557,6 +1900,7 @@ class RdxMenuSubTrigger {
1557
1900
  this.lastPointer = null;
1558
1901
  /** Whether the current open was initiated by hover (vs keyboard / click). */
1559
1902
  this.openedByHover = false;
1903
+ this.ignoreNextKeyboardClick = false;
1560
1904
  /** Whether this trigger (and therefore the submenu) is disabled. */
1561
1905
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1562
1906
  /** Whether this trigger should be treated as a native button. Auto-detected for `<button>`. */
@@ -1571,6 +1915,7 @@ class RdxMenuSubTrigger {
1571
1915
  this.label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1572
1916
  /** Highlighted when focused OR while the submenu is open. */
1573
1917
  this.highlighted = computed(() => this.isFocused() || this.submenuContext.isOpen(), ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
1918
+ this.effectiveDisabled = computed(() => this.disabled() || this.submenuContext.disabled(), ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
1574
1919
  this.nativeButtonState = computed(() => this.nativeButton() || this.elementRef.nativeElement.tagName === 'BUTTON', ...(ngDevMode ? [{ debugName: "nativeButtonState" }] : /* istanbul ignore next */ []));
1575
1920
  this.submenuContext.markAsSubmenu();
1576
1921
  effect((onCleanup) => {
@@ -1599,7 +1944,8 @@ class RdxMenuSubTrigger {
1599
1944
  return;
1600
1945
  }
1601
1946
  const reference = this.elementRef.nativeElement;
1602
- const scope = reference.closest('[rdxMenuPopup]') ?? document.body;
1947
+ const ownerDocument = reference.ownerDocument;
1948
+ const scope = reference.closest('[rdxMenuPopup]') ?? ownerDocument.body;
1603
1949
  const unregisterOpen = registerOpenSubmenu(reference, popup);
1604
1950
  let removeTunnel = applyPointerTunnel(scope, reference, popup);
1605
1951
  const { handler, dispose } = createSafePolygonHandler({
@@ -1617,9 +1963,9 @@ class RdxMenuSubTrigger {
1617
1963
  removeTunnel = undefined;
1618
1964
  }
1619
1965
  });
1620
- document.addEventListener('mousemove', handler);
1966
+ ownerDocument.addEventListener('mousemove', handler);
1621
1967
  onCleanup(() => {
1622
- document.removeEventListener('mousemove', handler);
1968
+ ownerDocument.removeEventListener('mousemove', handler);
1623
1969
  dispose();
1624
1970
  removeTunnel?.();
1625
1971
  unregisterOpen();
@@ -1642,7 +1988,7 @@ class RdxMenuSubTrigger {
1642
1988
  }
1643
1989
  }
1644
1990
  onFocus() {
1645
- if (!this.disabled()) {
1991
+ if (!this.effectiveDisabled()) {
1646
1992
  this.clearSiblingHighlights();
1647
1993
  this.isFocused.set(true);
1648
1994
  }
@@ -1650,18 +1996,55 @@ class RdxMenuSubTrigger {
1650
1996
  onBlur() {
1651
1997
  this.isFocused.set(false);
1652
1998
  }
1653
- onClick() {
1654
- if (this.disabled())
1999
+ onClick(event) {
2000
+ if (this.effectiveDisabled())
2001
+ return;
2002
+ if (this.ignoreNextKeyboardClick && event.detail === 0) {
2003
+ this.ignoreNextKeyboardClick = false;
1655
2004
  return;
2005
+ }
2006
+ const wasOpen = this.submenuContext.isOpen();
2007
+ // When the submenu opens on hover (default), hover owns its open/close, so a real **mouse** click
2008
+ // is ignored — otherwise it would toggle a just-hover-opened submenu shut (a visible flicker).
2009
+ // Base UI: `ignoreMouse: openOnHover`. A keyboard-activated click (`detail === 0`) still opens.
2010
+ const isMouseClick = event.detail > 0;
2011
+ if (this.openOnHover() && isMouseClick) {
2012
+ return;
2013
+ }
2014
+ this.openedByHover = false;
2015
+ this.clearSiblingHighlights();
2016
+ if (this.submenuContext.isOpen()) {
2017
+ // Toggle (close) only for a click-driven submenu (Base UI `toggle: !openOnHover`).
2018
+ if (!this.openOnHover()) {
2019
+ this.submenuContext.close();
2020
+ }
2021
+ return;
2022
+ }
2023
+ this.closeSiblingSubmenus();
2024
+ this.submenuContext.show('first', 'none', event);
2025
+ if (event.detail === 0 && !wasOpen && this.submenuContext.isOpen()) {
2026
+ this.focusFirstSubmenuItem();
2027
+ }
2028
+ }
2029
+ onEnter(event) {
2030
+ if (this.effectiveDisabled())
2031
+ return;
2032
+ event.preventDefault();
2033
+ event.stopPropagation();
2034
+ this.ignoreNextKeyboardClick = true;
1656
2035
  this.openedByHover = false;
1657
2036
  this.clearSiblingHighlights();
1658
2037
  if (!this.submenuContext.isOpen()) {
1659
2038
  this.closeSiblingSubmenus();
2039
+ this.submenuContext.show('first', 'none', event);
1660
2040
  }
1661
- this.submenuContext.toggle();
2041
+ this.focusFirstSubmenuItem();
1662
2042
  }
1663
2043
  onArrowRight(event) {
1664
- if (this.disabled())
2044
+ if (this.submenuContext.dir() === 'rtl') {
2045
+ return;
2046
+ }
2047
+ if (this.effectiveDisabled())
1665
2048
  return;
1666
2049
  event.preventDefault();
1667
2050
  event.stopPropagation();
@@ -1669,23 +2052,41 @@ class RdxMenuSubTrigger {
1669
2052
  this.clearSiblingHighlights();
1670
2053
  if (!this.submenuContext.isOpen()) {
1671
2054
  this.closeSiblingSubmenus();
1672
- this.submenuContext.show();
2055
+ this.submenuContext.show('first', 'none', event);
2056
+ this.focusFirstSubmenuItem();
2057
+ }
2058
+ }
2059
+ onArrowLeft(event) {
2060
+ if (this.submenuContext.dir() !== 'rtl') {
2061
+ return;
2062
+ }
2063
+ if (this.effectiveDisabled())
2064
+ return;
2065
+ event.preventDefault();
2066
+ event.stopPropagation();
2067
+ this.openedByHover = false;
2068
+ this.clearSiblingHighlights();
2069
+ if (!this.submenuContext.isOpen()) {
2070
+ this.closeSiblingSubmenus();
2071
+ this.submenuContext.show('first', 'none', event);
2072
+ this.focusFirstSubmenuItem();
1673
2073
  }
1674
2074
  }
1675
2075
  onPointerMove(event) {
1676
- if (event.pointerType !== 'mouse' || this.disabled() || !this.openOnHover())
2076
+ if (event.pointerType !== 'mouse' || this.effectiveDisabled() || !this.openOnHover())
1677
2077
  return;
1678
2078
  this.lastPointer = { x: event.clientX, y: event.clientY };
1679
2079
  this.clearSiblingHighlights();
1680
- if (this.submenuContext.highlightItemOnHover() && document.activeElement !== this.elementRef.nativeElement) {
1681
- this.elementRef.nativeElement.focus({ preventScroll: true });
2080
+ const el = this.elementRef.nativeElement;
2081
+ if (this.submenuContext.highlightItemOnHover() && el.ownerDocument.activeElement !== el) {
2082
+ el.focus({ preventScroll: true });
1682
2083
  }
1683
2084
  if (!this.submenuContext.isOpen()) {
1684
2085
  clearTimeout(this.openTimer);
1685
2086
  this.closeSiblingSubmenus();
1686
2087
  this.openTimer = setTimeout(() => {
1687
2088
  this.openedByHover = true;
1688
- this.submenuContext.show(false);
2089
+ this.submenuContext.show(false, 'trigger-hover');
1689
2090
  }, this.delay() ?? 100);
1690
2091
  }
1691
2092
  }
@@ -1720,31 +2121,66 @@ class RdxMenuSubTrigger {
1720
2121
  trigger.dispatchEvent(new CustomEvent('rdx-menu-subtrigger-clear-highlight'));
1721
2122
  });
1722
2123
  }
2124
+ focusFirstSubmenuItem(attempt = 0) {
2125
+ const maxAttempts = 10;
2126
+ const ownerDocument = this.elementRef.nativeElement.ownerDocument;
2127
+ const run = () => {
2128
+ if (!this.submenuContext.isOpen()) {
2129
+ return;
2130
+ }
2131
+ const popup = this.submenuContext.popupElement();
2132
+ if (!popup) {
2133
+ if (attempt < maxAttempts) {
2134
+ this.focusFirstSubmenuItem(attempt + 1);
2135
+ }
2136
+ return;
2137
+ }
2138
+ const items = getFocusableMenuItems(popup);
2139
+ if (items.length === 0) {
2140
+ if (attempt < maxAttempts) {
2141
+ this.focusFirstSubmenuItem(attempt + 1);
2142
+ }
2143
+ return;
2144
+ }
2145
+ const firstItem = items[0];
2146
+ if (ownerDocument.activeElement !== firstItem) {
2147
+ firstItem?.focus({ preventScroll: true });
2148
+ }
2149
+ };
2150
+ if (this.isBrowser) {
2151
+ requestAnimationFrame(run);
2152
+ }
2153
+ else {
2154
+ setTimeout(run);
2155
+ }
2156
+ }
1723
2157
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuSubTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1724
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuSubTrigger, isStandalone: true, selector: "[rdxMenuSubTrigger]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, nativeButton: { classPropertyName: "nativeButton", publicName: "nativeButton", isSignal: true, isRequired: false, transformFunction: null }, openOnHover: { classPropertyName: "openOnHover", publicName: "openOnHover", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "delay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "click": "onClick()", "keydown.arrowright": "onArrowRight($event)", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave()", "rdx-menu-subtrigger-clear-highlight": "clearHighlight()" }, properties: { "attr.type": "nativeButtonState() ? \"button\" : undefined", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "submenuContext.isOpen()", "attr.aria-disabled": "disabled() ? true : undefined", "attr.disabled": "nativeButtonState() && disabled() ? \"\" : undefined", "attr.data-state": "submenuContext.isOpen() ? \"open\" : \"closed\"", "attr.data-popup-open": "submenuContext.isOpen() ? \"\" : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, exportAs: ["rdxMenuSubTrigger"], hostDirectives: [{ directive: i1.RdxPopperAnchor }, { directive: i2.RdxDismissableLayerBranch }], ngImport: i0 }); }
2158
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuSubTrigger, isStandalone: true, selector: "[rdxMenuSubTrigger]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, nativeButton: { classPropertyName: "nativeButton", publicName: "nativeButton", isSignal: true, isRequired: false, transformFunction: null }, openOnHover: { classPropertyName: "openOnHover", publicName: "openOnHover", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "delay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, listeners: { "focus": "onFocus()", "blur": "onBlur()", "click": "onClick($event)", "keydown.enter": "onEnter($event)", "keydown.arrowleft": "onArrowLeft($event)", "keydown.arrowright": "onArrowRight($event)", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave()", "rdx-menu-subtrigger-clear-highlight": "clearHighlight()" }, properties: { "attr.type": "nativeButtonState() ? \"button\" : undefined", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "submenuContext.isOpen()", "attr.aria-disabled": "effectiveDisabled() ? true : undefined", "attr.disabled": "nativeButtonState() && effectiveDisabled() ? \"\" : undefined", "attr.data-state": "submenuContext.isOpen() ? \"open\" : \"closed\"", "attr.data-popup-open": "submenuContext.isOpen() ? \"\" : undefined", "attr.data-highlighted": "highlighted() ? \"\" : undefined", "attr.data-disabled": "effectiveDisabled() ? \"\" : undefined", "attr.data-label": "label() ?? undefined" } }, exportAs: ["rdxMenuSubTrigger"], hostDirectives: [{ directive: i1.RdxPopperAnchor }], ngImport: i0 }); }
1725
2159
  }
1726
2160
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuSubTrigger, decorators: [{
1727
2161
  type: Directive,
1728
2162
  args: [{
1729
2163
  selector: '[rdxMenuSubTrigger]',
1730
2164
  exportAs: 'rdxMenuSubTrigger',
1731
- hostDirectives: [RdxPopperAnchor, RdxDismissableLayerBranch],
2165
+ hostDirectives: [RdxPopperAnchor],
1732
2166
  host: {
1733
2167
  '[attr.type]': 'nativeButtonState() ? "button" : undefined',
1734
2168
  role: 'menuitem',
1735
2169
  tabindex: '-1',
1736
2170
  '[attr.aria-haspopup]': '"menu"',
1737
2171
  '[attr.aria-expanded]': 'submenuContext.isOpen()',
1738
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
1739
- '[attr.disabled]': 'nativeButtonState() && disabled() ? "" : undefined',
2172
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
2173
+ '[attr.disabled]': 'nativeButtonState() && effectiveDisabled() ? "" : undefined',
1740
2174
  '[attr.data-state]': 'submenuContext.isOpen() ? "open" : "closed"',
1741
2175
  '[attr.data-popup-open]': 'submenuContext.isOpen() ? "" : undefined',
1742
2176
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
1743
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
2177
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
1744
2178
  '[attr.data-label]': 'label() ?? undefined',
1745
2179
  '(focus)': 'onFocus()',
1746
2180
  '(blur)': 'onBlur()',
1747
- '(click)': 'onClick()',
2181
+ '(click)': 'onClick($event)',
2182
+ '(keydown.enter)': 'onEnter($event)',
2183
+ '(keydown.arrowleft)': 'onArrowLeft($event)',
1748
2184
  '(keydown.arrowright)': 'onArrowRight($event)',
1749
2185
  '(pointermove)': 'onPointerMove($event)',
1750
2186
  '(pointerleave)': 'onPointerLeave()',
@@ -1762,7 +2198,23 @@ class RdxMenuTrigger {
1762
2198
  this.rootContext = injectRdxMenuRootContext();
1763
2199
  this.elementRef = inject(ElementRef);
1764
2200
  this.destroyRef = inject(DestroyRef);
1765
- this.dismissableLayersContext = inject(RdxDismissableLayersContextToken);
2201
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2202
+ this.lastPointer = null;
2203
+ this.openedByHover = false;
2204
+ this.ignoreNextClick = null;
2205
+ this.handleDocumentMouseUp = (event) => {
2206
+ this.allowMouseUpTriggerTimer = undefined;
2207
+ this.rootContext.setAllowMouseUpTrigger(false);
2208
+ const trigger = this.elementRef.nativeElement;
2209
+ const target = event.target;
2210
+ const popup = this.rootContext.popupElement();
2211
+ if (target && (trigger.contains(target) || popup?.contains(target))) {
2212
+ return;
2213
+ }
2214
+ if (this.rootContext.isOpen()) {
2215
+ this.rootContext.close('cancel-open', event);
2216
+ }
2217
+ };
1766
2218
  /** Whether this trigger should be treated as a native button. Auto-detected for `<button>`. */
1767
2219
  this.nativeButton = input(false, { ...(ngDevMode ? { debugName: "nativeButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1768
2220
  /** Whether this trigger is disabled. */
@@ -1770,43 +2222,123 @@ class RdxMenuTrigger {
1770
2222
  /** Whether hovering the trigger opens the menu. */
1771
2223
  this.openOnHover = input(false, { ...(ngDevMode ? { debugName: "openOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1772
2224
  /** Delay before hover opens the menu, in milliseconds. */
1773
- this.delay = input(undefined, { ...(ngDevMode ? { debugName: "delay" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
2225
+ this.delay = input(100, { ...(ngDevMode ? { debugName: "delay" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
1774
2226
  /** Delay before hover leave closes the menu, in milliseconds. */
1775
2227
  this.closeDelay = input(undefined, { ...(ngDevMode ? { debugName: "closeDelay" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
1776
2228
  this.nativeButtonState = computed(() => this.nativeButton() || this.elementRef.nativeElement.tagName === 'BUTTON', ...(ngDevMode ? [{ debugName: "nativeButtonState" }] : /* istanbul ignore next */ []));
1777
2229
  this.isDisabled = computed(() => this.rootContext.disabled() || this.disabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
2230
+ this.triggerInteraction = createRdxTriggerInteraction({
2231
+ trigger: () => this.elementRef.nativeElement,
2232
+ activeTrigger: () => this.rootContext.trigger(),
2233
+ open: () => this.rootContext.isOpen(),
2234
+ disabled: () => this.isDisabled()
2235
+ });
1778
2236
  effect((onCleanup) => {
1779
2237
  const el = this.elementRef.nativeElement;
1780
2238
  const unregister = this.rootContext.registerTrigger(el);
1781
2239
  onCleanup(unregister);
1782
2240
  });
1783
- // When a coordinator (e.g. the menubar) drives this trigger, hover-switching focuses the
1784
- // trigger and opens the popup without pulling focus inside it. Register the trigger as a
1785
- // dismissable-layer branch so that focus/pointer interactions on it are treated as "inside"
1786
- // and do not dismiss the just-opened popup.
1787
2241
  effect((onCleanup) => {
1788
- if (!this.rootContext.hasTriggerInteractionHandler()) {
2242
+ const open = this.rootContext.isOpen();
2243
+ const popup = this.rootContext.popupElement();
2244
+ if (!open) {
2245
+ this.openedByHover = false;
2246
+ this.lastPointer = null;
1789
2247
  return;
1790
2248
  }
1791
- const el = this.elementRef.nativeElement;
1792
- this.dismissableLayersContext.branches.update((branches) => [...branches, el]);
2249
+ if (!popup || !this.openedByHover || !this.lastPointer || !this.isBrowser) {
2250
+ return;
2251
+ }
2252
+ const trigger = this.elementRef.nativeElement;
2253
+ const ownerDocument = trigger.ownerDocument;
2254
+ let removeTunnel = applyPointerTunnel(ownerDocument.body, trigger, popup);
2255
+ const { handler, dispose } = createSafePolygonHandler({
2256
+ reference: trigger,
2257
+ floating: popup,
2258
+ side: () => popup.getAttribute('data-side') ?? 'bottom',
2259
+ x: this.lastPointer.x,
2260
+ y: this.lastPointer.y,
2261
+ onClose: () => this.scheduleClose(),
2262
+ cancelClose: () => this.clearCloseTimer(),
2263
+ hasOpenChild: () => hasOpenChildSubmenu(trigger, popup),
2264
+ onLanded: () => {
2265
+ removeTunnel?.();
2266
+ removeTunnel = undefined;
2267
+ }
2268
+ });
2269
+ ownerDocument.addEventListener('mousemove', handler);
1793
2270
  onCleanup(() => {
1794
- this.dismissableLayersContext.branches.update((branches) => branches.filter((b) => b !== el));
2271
+ ownerDocument.removeEventListener('mousemove', handler);
2272
+ dispose();
2273
+ removeTunnel?.();
2274
+ this.clearCloseTimer();
1795
2275
  });
1796
2276
  });
2277
+ // (A press/focus on the trigger no longer needs a dismissable-layer branch to avoid
2278
+ // self-dismissal: the trigger is registered in the menu's floating context, so the dismissal
2279
+ // capability already treats it as "inside" — ADR 0015 trigger registry replaces the branch.)
2280
+ useTriggerFocusGuards({
2281
+ trigger: () => this.elementRef.nativeElement,
2282
+ close: (event) => this.rootContext.close('focus-out', event),
2283
+ beforeContentFocusGuard: () => this.rootContext.beforeContentFocusGuard(),
2284
+ enabled: () => this.triggerInteraction.isActive(),
2285
+ popupElement: () => this.rootContext.popupElement()
2286
+ });
1797
2287
  this.destroyRef.onDestroy(() => {
1798
2288
  this.clearOpenTimer();
1799
2289
  this.clearCloseTimer();
2290
+ this.clearMouseUpGuard();
1800
2291
  });
1801
2292
  }
1802
- handleClick() {
2293
+ handleMouseDown(event) {
2294
+ if (this.isDisabled() || event.button !== 0) {
2295
+ return;
2296
+ }
2297
+ if (this.rootContext.hasTriggerInteractionHandler()) {
2298
+ return;
2299
+ }
2300
+ if (this.openOnHover() &&
2301
+ this.rootContext.isOpen() &&
2302
+ this.rootContext.lastOpenChangeReason() === 'trigger-hover') {
2303
+ return;
2304
+ }
2305
+ const wasOpen = this.rootContext.isOpen();
2306
+ this.clearMouseUpGuard();
2307
+ this.ignoreNextClick = 'mouse';
2308
+ this.openedByHover = false;
2309
+ this.rootContext.toggle('trigger-press', event);
2310
+ if (!wasOpen && this.rootContext.isOpen()) {
2311
+ this.armMouseUpGuard(event.currentTarget);
2312
+ }
2313
+ }
2314
+ handlePointerDown(event) {
2315
+ this.triggerInteraction.recordPointerDown(event);
2316
+ }
2317
+ handleClick(event) {
1803
2318
  if (this.isDisabled()) {
1804
2319
  return;
1805
2320
  }
2321
+ const wasOpen = this.rootContext.isOpen();
1806
2322
  if (this.rootContext.handleTriggerInteraction({ type: 'click' })) {
2323
+ if (event.detail === 0 && !wasOpen && this.rootContext.isOpen()) {
2324
+ this.restoreKeyboardPopupFocus();
2325
+ }
1807
2326
  return;
1808
2327
  }
1809
- this.rootContext.toggle();
2328
+ if (this.ignoreNextClick &&
2329
+ ((this.ignoreNextClick === 'mouse' && event.detail > 0) ||
2330
+ (this.ignoreNextClick === 'keyboard' && event.detail === 0))) {
2331
+ if (this.ignoreNextClick === 'keyboard') {
2332
+ this.restoreKeyboardPopupFocus();
2333
+ }
2334
+ this.ignoreNextClick = null;
2335
+ return;
2336
+ }
2337
+ this.openedByHover = false;
2338
+ this.rootContext.toggle('trigger-press', event);
2339
+ if (event.detail === 0 && !wasOpen && this.rootContext.isOpen()) {
2340
+ this.restoreKeyboardPopupFocus();
2341
+ }
1810
2342
  }
1811
2343
  handleArrowDown(event) {
1812
2344
  if (this.rootContext.handleTriggerInteraction({ type: 'arrowdown', event })) {
@@ -1842,11 +2374,27 @@ class RdxMenuTrigger {
1842
2374
  this.rootContext.handleTriggerInteraction({ type: 'escape', event });
1843
2375
  }
1844
2376
  handleKeyboardToggle(event) {
1845
- if (this.nativeButtonState()) {
2377
+ const wasOpen = this.rootContext.isOpen();
2378
+ const interactionType = event instanceof KeyboardEvent && event.key === ' ' ? 'space' : 'enter';
2379
+ if (this.nativeButtonState() && !this.rootContext.hasTriggerInteractionHandler()) {
2380
+ return;
2381
+ }
2382
+ if (this.rootContext.handleTriggerInteraction({ type: interactionType, event })) {
2383
+ event.preventDefault();
2384
+ this.ignoreNextClick = this.nativeButtonState() ? 'keyboard' : null;
2385
+ this.openedByHover = false;
2386
+ if (!wasOpen && this.rootContext.isOpen()) {
2387
+ this.restoreKeyboardPopupFocus();
2388
+ }
1846
2389
  return;
1847
2390
  }
1848
2391
  event.preventDefault();
1849
- this.handleClick();
2392
+ this.ignoreNextClick = this.nativeButtonState() ? 'keyboard' : null;
2393
+ this.openedByHover = false;
2394
+ this.rootContext.toggle('trigger-press', event);
2395
+ if (!wasOpen && this.rootContext.isOpen()) {
2396
+ this.restoreKeyboardPopupFocus();
2397
+ }
1850
2398
  }
1851
2399
  handlePointerEnter(event) {
1852
2400
  if (this.rootContext.handleTriggerInteraction({ type: 'pointerenter', event })) {
@@ -1857,14 +2405,17 @@ class RdxMenuTrigger {
1857
2405
  }
1858
2406
  this.clearCloseTimer();
1859
2407
  this.clearOpenTimer();
1860
- const delay = this.delay() ?? 0;
2408
+ this.lastPointer = { x: event.clientX, y: event.clientY };
2409
+ const delay = this.delay() ?? 100;
1861
2410
  if (delay <= 0) {
1862
- this.rootContext.show();
2411
+ this.openedByHover = true;
2412
+ this.rootContext.show('first', 'trigger-hover');
1863
2413
  return;
1864
2414
  }
1865
2415
  this.openTimer = setTimeout(() => {
1866
2416
  this.openTimer = undefined;
1867
- this.rootContext.show();
2417
+ this.openedByHover = true;
2418
+ this.rootContext.show('first', 'trigger-hover');
1868
2419
  }, delay);
1869
2420
  }
1870
2421
  handlePointerLeave(event) {
@@ -1872,12 +2423,19 @@ class RdxMenuTrigger {
1872
2423
  return;
1873
2424
  }
1874
2425
  this.clearOpenTimer();
2426
+ this.lastPointer = { x: event.clientX, y: event.clientY };
2427
+ if (!this.rootContext.isOpen() || !this.openedByHover) {
2428
+ this.scheduleClose();
2429
+ }
2430
+ }
2431
+ handlePointerMove(event) {
2432
+ if (event.pointerType !== 'touch' && this.openOnHover()) {
2433
+ this.lastPointer = { x: event.clientX, y: event.clientY };
2434
+ }
2435
+ }
2436
+ scheduleClose() {
1875
2437
  this.clearCloseTimer();
1876
2438
  const closeDelay = this.closeDelay() ?? 0;
1877
- if (closeDelay <= 0) {
1878
- this.rootContext.close();
1879
- return;
1880
- }
1881
2439
  this.closeTimer = setTimeout(() => {
1882
2440
  this.closeTimer = undefined;
1883
2441
  this.rootContext.close();
@@ -1891,8 +2449,50 @@ class RdxMenuTrigger {
1891
2449
  clearTimeout(this.closeTimer);
1892
2450
  this.closeTimer = undefined;
1893
2451
  }
2452
+ armMouseUpGuard(trigger) {
2453
+ this.rootContext.setAllowMouseUpTrigger(false);
2454
+ this.allowMouseUpTriggerTimer = setTimeout(() => {
2455
+ this.allowMouseUpTriggerTimer = undefined;
2456
+ this.rootContext.setAllowMouseUpTrigger(true);
2457
+ }, 200);
2458
+ trigger.ownerDocument.addEventListener('mouseup', this.handleDocumentMouseUp, { once: true });
2459
+ }
2460
+ clearMouseUpGuard() {
2461
+ clearTimeout(this.allowMouseUpTriggerTimer);
2462
+ this.allowMouseUpTriggerTimer = undefined;
2463
+ this.rootContext.setAllowMouseUpTrigger(false);
2464
+ this.elementRef.nativeElement.ownerDocument.removeEventListener('mouseup', this.handleDocumentMouseUp);
2465
+ }
2466
+ restoreKeyboardPopupFocus(attempt = 0) {
2467
+ const maxAttempts = 3;
2468
+ const ownerDocument = this.elementRef.nativeElement.ownerDocument;
2469
+ const run = () => {
2470
+ if (!this.rootContext.isOpen()) {
2471
+ return;
2472
+ }
2473
+ const popup = this.rootContext.popupElement();
2474
+ const activeElement = ownerDocument.activeElement;
2475
+ if (!popup || getFocusableMenuItems(popup).length === 0) {
2476
+ if (attempt < maxAttempts) {
2477
+ this.restoreKeyboardPopupFocus(attempt + 1);
2478
+ }
2479
+ return;
2480
+ }
2481
+ if (activeElement && popup.contains(activeElement)) {
2482
+ return;
2483
+ }
2484
+ const firstItem = getFocusableMenuItems(popup)[0];
2485
+ (firstItem ?? popup).focus({ preventScroll: true });
2486
+ };
2487
+ if (this.isBrowser) {
2488
+ requestAnimationFrame(run);
2489
+ }
2490
+ else {
2491
+ setTimeout(run);
2492
+ }
2493
+ }
1894
2494
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1895
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuTrigger, isStandalone: true, selector: "[rdxMenuTrigger]", inputs: { nativeButton: { classPropertyName: "nativeButton", publicName: "nativeButton", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, openOnHover: { classPropertyName: "openOnHover", publicName: "openOnHover", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "delay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "handleClick()", "pointerenter": "handlePointerEnter($event)", "pointerleave": "handlePointerLeave($event)", "keydown.arrowdown": "handleArrowDown($event)", "keydown.arrowup": "handleArrowUp($event)", "keydown.arrowleft": "handleArrowLeft($event)", "keydown.arrowright": "handleArrowRight($event)", "keydown.home": "handleHome($event)", "keydown.end": "handleEnd($event)", "keydown.escape": "handleEscape($event)", "keydown.enter": "handleKeyboardToggle($event)", "keydown.space": "handleKeyboardToggle($event)" }, properties: { "attr.type": "nativeButtonState() ? \"button\" : undefined", "attr.role": "rootContext.hasTriggerInteractionHandler() ? \"menuitem\" : nativeButtonState() ? undefined : \"button\"", "attr.tabindex": "rootContext.hasTriggerInteractionHandler() ? \"-1\" : undefined", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "rootContext.isOpen()", "attr.aria-disabled": "isDisabled() ? true : undefined", "attr.disabled": "nativeButtonState() && isDisabled() ? \"\" : undefined", "attr.data-state": "rootContext.isOpen() ? \"open\" : \"closed\"", "attr.data-disabled": "isDisabled() ? \"\" : undefined", "attr.data-popup-open": "rootContext.isOpen() ? \"\" : undefined" } }, exportAs: ["rdxMenuTrigger"], hostDirectives: [{ directive: i1.RdxPopperAnchor }], ngImport: i0 }); }
2495
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxMenuTrigger, isStandalone: true, selector: "[rdxMenuTrigger]", inputs: { nativeButton: { classPropertyName: "nativeButton", publicName: "nativeButton", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, openOnHover: { classPropertyName: "openOnHover", publicName: "openOnHover", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "delay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "handlePointerDown($event)", "mousedown": "handleMouseDown($event)", "click": "handleClick($event)", "pointerenter": "handlePointerEnter($event)", "pointermove": "handlePointerMove($event)", "pointerleave": "handlePointerLeave($event)", "keydown.arrowdown": "handleArrowDown($event)", "keydown.arrowup": "handleArrowUp($event)", "keydown.arrowleft": "handleArrowLeft($event)", "keydown.arrowright": "handleArrowRight($event)", "keydown.home": "handleHome($event)", "keydown.end": "handleEnd($event)", "keydown.escape": "handleEscape($event)", "keydown.enter": "handleKeyboardToggle($event)", "keydown.space": "handleKeyboardToggle($event)" }, properties: { "attr.type": "nativeButtonState() ? \"button\" : undefined", "attr.role": "rootContext.hasTriggerInteractionHandler() ? \"menuitem\" : nativeButtonState() ? undefined : \"button\"", "attr.tabindex": "rootContext.hasTriggerInteractionHandler() ? \"-1\" : undefined", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "triggerInteraction.ariaExpanded()", "attr.aria-disabled": "isDisabled() ? true : undefined", "attr.disabled": "nativeButtonState() && isDisabled() ? \"\" : undefined", "attr.data-state": "triggerInteraction.dataState()", "attr.data-disabled": "isDisabled() ? \"\" : undefined", "attr.data-popup-open": "triggerInteraction.dataPopupOpen()", "style.pointer-events": "rootContext.isOpen() && rootContext.modal() ? \"auto\" : undefined" } }, exportAs: ["rdxMenuTrigger"], hostDirectives: [{ directive: i1.RdxPopperAnchor }], ngImport: i0 }); }
1896
2496
  }
1897
2497
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuTrigger, decorators: [{
1898
2498
  type: Directive,
@@ -1905,14 +2505,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1905
2505
  '[attr.role]': 'rootContext.hasTriggerInteractionHandler() ? "menuitem" : nativeButtonState() ? undefined : "button"',
1906
2506
  '[attr.tabindex]': 'rootContext.hasTriggerInteractionHandler() ? "-1" : undefined',
1907
2507
  '[attr.aria-haspopup]': '"menu"',
1908
- '[attr.aria-expanded]': 'rootContext.isOpen()',
2508
+ '[attr.aria-expanded]': 'triggerInteraction.ariaExpanded()',
1909
2509
  '[attr.aria-disabled]': 'isDisabled() ? true : undefined',
1910
2510
  '[attr.disabled]': 'nativeButtonState() && isDisabled() ? "" : undefined',
1911
- '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"',
2511
+ '[attr.data-state]': 'triggerInteraction.dataState()',
1912
2512
  '[attr.data-disabled]': 'isDisabled() ? "" : undefined',
1913
- '[attr.data-popup-open]': 'rootContext.isOpen() ? "" : undefined',
1914
- '(click)': 'handleClick()',
2513
+ '[attr.data-popup-open]': 'triggerInteraction.dataPopupOpen()',
2514
+ '[style.pointer-events]': 'rootContext.isOpen() && rootContext.modal() ? "auto" : undefined',
2515
+ '(pointerdown)': 'handlePointerDown($event)',
2516
+ '(mousedown)': 'handleMouseDown($event)',
2517
+ '(click)': 'handleClick($event)',
1915
2518
  '(pointerenter)': 'handlePointerEnter($event)',
2519
+ '(pointermove)': 'handlePointerMove($event)',
1916
2520
  '(pointerleave)': 'handlePointerLeave($event)',
1917
2521
  '(keydown.arrowdown)': 'handleArrowDown($event)',
1918
2522
  '(keydown.arrowup)': 'handleArrowUp($event)',
@@ -2080,5 +2684,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2080
2684
  * Generated bundle index. Do not edit.
2081
2685
  */
2082
2686
 
2083
- export { RdxMenuArrow, RdxMenuBackdrop, RdxMenuCheckboxItem, RdxMenuCheckboxItemIndicator, RdxMenuGroup, RdxMenuGroupLabel, RdxMenuItem, RdxMenuLinkItem, RdxMenuModule, RdxMenuPopup, RdxMenuPortal, RdxMenuPortalMisuseGuard, RdxMenuPositioner, RdxMenuRadioGroup, RdxMenuRadioItem, RdxMenuRadioItemIndicator, RdxMenuRoot, RdxMenuSeparator, RdxMenuSubTrigger, RdxMenuTrigger, RdxMenuViewport, getCheckedState, injectRdxMenuCheckboxItemContext, injectRdxMenuRadioGroupContext, injectRdxMenuRadioItemContext, injectRdxMenuRootContext, isIndeterminate, provideRdxMenuCheckboxItemContext, provideRdxMenuRadioGroupContext, provideRdxMenuRadioItemContext, provideRdxMenuRootContext };
2687
+ export { RdxMenuArrow, RdxMenuBackdrop, RdxMenuCheckboxItem, RdxMenuCheckboxItemIndicator, RdxMenuGroup, RdxMenuGroupLabel, RdxMenuItem, RdxMenuLinkItem, RdxMenuModule, RdxMenuPopup, RdxMenuPortal, RdxMenuPortalMisuseGuard, RdxMenuPositioner, RdxMenuRadioGroup, RdxMenuRadioItem, RdxMenuRadioItemIndicator, RdxMenuRoot, RdxMenuSeparator, RdxMenuSubTrigger, RdxMenuTrigger, RdxMenuViewport, getCheckedState, injectRdxMenuCheckboxItemContext, injectRdxMenuGroupContext, injectRdxMenuRadioGroupContext, injectRdxMenuRadioItemContext, injectRdxMenuRootContext, isIndeterminate, provideRdxMenuCheckboxItemContext, provideRdxMenuGroupContext, provideRdxMenuRadioGroupContext, provideRdxMenuRadioItemContext, provideRdxMenuRootContext };
2084
2688
  //# sourceMappingURL=radix-ng-primitives-menu.mjs.map