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

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