@radix-ng/primitives 1.0.0-beta.2 → 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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +76 -6
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  4. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
  6. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
  8. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
  9. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  10. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
  12. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  14. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  18. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  20. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
  22. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
  28. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  30. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  32. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  34. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
  36. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  38. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
  40. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
  42. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
  44. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
  46. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
  48. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
  50. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  52. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  54. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-select.mjs +303 -234
  56. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  58. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  59. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  60. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  61. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  62. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  63. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  64. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  65. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  66. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  67. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  68. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  69. package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
  70. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  71. package/package.json +14 -1
  72. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  73. package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
  74. package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
  75. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  76. package/types/radix-ng-primitives-combobox.d.ts +727 -293
  77. package/types/radix-ng-primitives-config.d.ts +1 -1
  78. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  79. package/types/radix-ng-primitives-core.d.ts +762 -14
  80. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  81. package/types/radix-ng-primitives-dialog.d.ts +107 -55
  82. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  83. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  84. package/types/radix-ng-primitives-drawer.d.ts +49 -22
  85. package/types/radix-ng-primitives-field.d.ts +1 -0
  86. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  87. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  88. package/types/radix-ng-primitives-menu.d.ts +204 -112
  89. package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
  90. package/types/radix-ng-primitives-popover.d.ts +82 -115
  91. package/types/radix-ng-primitives-popper.d.ts +46 -10
  92. package/types/radix-ng-primitives-portal.d.ts +53 -8
  93. package/types/radix-ng-primitives-presence.d.ts +98 -17
  94. package/types/radix-ng-primitives-preview-card.d.ts +63 -95
  95. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  96. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  97. package/types/radix-ng-primitives-select.d.ts +192 -158
  98. package/types/radix-ng-primitives-slider.d.ts +5 -4
  99. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  100. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  101. package/types/radix-ng-primitives-toast.d.ts +7 -7
  102. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  103. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  104. package/types/radix-ng-primitives-tooltip.d.ts +48 -84
@@ -1,43 +1,57 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, model, input, booleanAttribute, output, signal, computed, effect, untracked, Directive, ElementRef, DestroyRef, 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
- import { RdxPortal } from '@radix-ng/primitives/portal';
14
+ import { RdxPortalPresence } from '@radix-ng/primitives/portal';
15
+ import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
13
16
  import { isPlatformBrowser } from '@angular/common';
14
17
 
15
18
  const [injectRdxMenuRootContext, provideRdxMenuRootContext] = createContext('RdxMenuRootContext', 'components/menu');
16
19
  function buildContext(instance) {
17
20
  return {
18
21
  isOpen: instance.open,
19
- disabled: instance.disabled,
20
- modal: instance.modal,
22
+ present: instance.present,
23
+ disabled: instance.effectiveDisabled,
24
+ modal: instance.effectiveModal,
21
25
  loopFocus: instance.loopFocus,
22
26
  highlightItemOnHover: instance.highlightItemOnHover,
23
27
  orientation: instance.orientation,
28
+ dir: instance.dir,
24
29
  closeParentOnEsc: instance.closeParentOnEsc,
25
30
  autoFocus: instance.autoFocus.asReadonly(),
26
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(),
27
38
  hasTriggerInteractionHandler: instance.hasTriggerInteractionHandler.asReadonly(),
28
39
  trigger: instance.trigger.asReadonly(),
29
40
  popupElement: instance.popupElement.asReadonly(),
30
41
  transitionStatus: instance.transitionStatus,
31
- close: () => instance.close(),
32
- toggle: () => instance.toggle(),
33
- show: (autoFocus) => instance.show(autoFocus),
34
- 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),
35
47
  registerTrigger: (el) => instance.registerTrigger(el),
36
48
  registerPopup: (el) => instance.registerPopup(el),
37
49
  registerTransitionElement: (el) => instance.registerTransitionElement(el),
38
50
  registerPopupArrowNavigationHandler: (handler) => instance.registerPopupArrowNavigationHandler(handler),
39
51
  registerTriggerInteractionHandler: (handler) => instance.registerTriggerInteractionHandler(handler),
40
52
  markAsSubmenu: () => instance.markAsSubmenu(),
53
+ markAsContextMenu: () => instance.markAsContextMenu(),
54
+ setAllowMouseUpTrigger: (value) => instance.setAllowMouseUpTrigger(value),
41
55
  closeParent: () => instance.closeParent(),
42
56
  handlePopupArrowNavigation: (offset) => instance.handlePopupArrowNavigation(offset),
43
57
  handleTriggerInteraction: (interaction) => instance.handleTriggerInteraction(interaction)
@@ -50,6 +64,17 @@ const contextFactory = () => buildContext(inject(RdxMenuRoot));
50
64
  class RdxMenuRoot {
51
65
  constructor() {
52
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
+ });
53
78
  /** Shared open/close transition state machine (completes on the real animationend). */
54
79
  this.transition = useTransitionStatus((open) => this.onOpenChangeComplete.emit(open));
55
80
  this.hasAppliedDefaultOpen = false;
@@ -59,14 +84,19 @@ class RdxMenuRoot {
59
84
  this.defaultOpen = input(false, { ...(ngDevMode ? { debugName: "defaultOpen" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
60
85
  /** Whether interactions with the menu are disabled. */
61
86
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
62
- /** Whether the menu should block outside interactions. */
63
- 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 });
64
92
  /** Whether keyboard navigation wraps at list boundaries. */
65
93
  this.loopFocus = input(true, { ...(ngDevMode ? { debugName: "loopFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
66
94
  /** Whether moving the pointer over items should highlight them. */
67
95
  this.highlightItemOnHover = input(true, { ...(ngDevMode ? { debugName: "highlightItemOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
68
96
  /** The menu orientation. */
69
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' });
70
100
  /** Whether pressing Escape inside a submenu closes the whole menu chain. */
71
101
  this.closeParentOnEsc = input(false, { ...(ngDevMode ? { debugName: "closeParentOnEsc" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
72
102
  /** Emits when the open state changes. */
@@ -79,8 +109,42 @@ class RdxMenuRoot {
79
109
  /** Whether the popup grabs focus when it opens. Set false for menubar hover-switching. */
80
110
  this.autoFocus = signal('first', ...(ngDevMode ? [{ debugName: "autoFocus" }] : /* istanbul ignore next */ []));
81
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 */ []));
82
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 */ []));
83
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 */ []));
84
148
  effect(() => {
85
149
  const defaultOpen = this.defaultOpen();
86
150
  if (!this.hasAppliedDefaultOpen && defaultOpen) {
@@ -89,6 +153,14 @@ class RdxMenuRoot {
89
153
  }
90
154
  });
91
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
+ });
92
164
  let previousOpen = this.open();
93
165
  effect(() => {
94
166
  const open = this.open();
@@ -98,37 +170,79 @@ class RdxMenuRoot {
98
170
  }
99
171
  });
100
172
  }
101
- show(autoFocus = 'first') {
102
- if (this.disabled()) {
173
+ show(autoFocus = 'first', reason = 'none', event) {
174
+ if (this.effectiveDisabled()) {
103
175
  return;
104
176
  }
105
177
  this.autoFocus.set(autoFocus === true ? 'first' : autoFocus);
106
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);
107
190
  this.open.set(true);
108
- 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 });
109
194
  }
110
195
  }
111
- close() {
196
+ close(reason = 'none', event) {
112
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());
113
207
  this.open.set(false);
114
- this.onOpenChange.emit(false);
208
+ this.floatingContext.events.emit('openchange', { open: false, reason, event: change.eventDetails.event });
115
209
  }
116
210
  }
117
- toggle() {
118
- if (this.disabled()) {
211
+ toggle(reason = 'trigger-press', event) {
212
+ if (this.effectiveDisabled()) {
119
213
  return;
120
214
  }
121
215
  if (this.open()) {
122
- this.close();
216
+ this.close(reason, event);
123
217
  }
124
218
  else {
125
- this.show();
219
+ this.show('first', reason, event);
126
220
  }
127
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
+ }
128
240
  registerTrigger(el) {
129
241
  this.registeredTrigger = el;
130
242
  this.trigger.set(el);
243
+ this.floatingContext.triggers.add(el);
131
244
  return () => {
245
+ this.floatingContext.triggers.delete(el);
132
246
  if (this.registeredTrigger === el) {
133
247
  this.registeredTrigger = undefined;
134
248
  this.trigger.set(undefined);
@@ -176,18 +290,44 @@ class RdxMenuRoot {
176
290
  closeParent() {
177
291
  this.trigger()?.dispatchEvent(new CustomEvent('rdx-menu-close-parent', { bubbles: true }));
178
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
+ }
179
307
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
180
- 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 }); }
181
315
  }
182
316
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRoot, decorators: [{
183
317
  type: Directive,
184
318
  args: [{
185
319
  selector: '[rdxMenuRoot],[rdxMenuSubmenuRoot]',
186
320
  exportAs: 'rdxMenuRoot',
187
- 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
+ ],
188
328
  hostDirectives: [RdxPopper]
189
329
  }]
190
- }], 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"] }] } });
191
331
 
192
332
  /**
193
333
  * An optional visual arrow connecting the popup to its trigger.
@@ -280,12 +420,13 @@ class RdxMenuCheckboxItem {
280
420
  /** Emits when the checked state changes. */
281
421
  this.onCheckedChange = output();
282
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 */ []));
283
424
  // Expose helpers for host bindings
284
425
  this.isIndeterminate = isIndeterminate;
285
426
  this.getCheckedState = getCheckedState;
286
427
  }
287
428
  onFocus() {
288
- if (!this.disabled()) {
429
+ if (!this.effectiveDisabled()) {
289
430
  this.isFocused.set(true);
290
431
  }
291
432
  }
@@ -293,13 +434,13 @@ class RdxMenuCheckboxItem {
293
434
  this.isFocused.set(false);
294
435
  }
295
436
  onPointerMove(event) {
296
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
437
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
297
438
  return;
298
439
  }
299
440
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
300
441
  return;
301
442
  }
302
- if (document.activeElement !== this.elementRef.nativeElement) {
443
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
303
444
  this.elementRef.nativeElement.focus({ preventScroll: true });
304
445
  }
305
446
  }
@@ -307,24 +448,31 @@ class RdxMenuCheckboxItem {
307
448
  if (event.pointerType !== 'mouse') {
308
449
  return;
309
450
  }
310
- if (document.activeElement === this.elementRef.nativeElement) {
451
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
311
452
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
312
453
  }
313
454
  }
314
455
  onItemClick() {
315
- if (this.disabled())
456
+ if (this.effectiveDisabled())
316
457
  return;
317
458
  this.toggleChecked();
318
459
  if (this.closeOnClick())
319
- 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();
320
468
  }
321
469
  onActivate(event) {
322
- if (this.disabled())
470
+ if (this.effectiveDisabled())
323
471
  return;
324
472
  event.preventDefault();
325
473
  this.toggleChecked();
326
474
  if (this.closeOnClick())
327
- this.rootContext?.close();
475
+ this.rootContext?.closeEntireMenu();
328
476
  }
329
477
  toggleChecked() {
330
478
  const next = isIndeterminate(this.checked()) ? true : !this.checked();
@@ -332,7 +480,7 @@ class RdxMenuCheckboxItem {
332
480
  this.onCheckedChange.emit(next);
333
481
  }
334
482
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuCheckboxItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
335
- 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 }); }
336
484
  }
337
485
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuCheckboxItem, decorators: [{
338
486
  type: Directive,
@@ -345,14 +493,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
345
493
  tabindex: '-1',
346
494
  '[attr.aria-checked]': 'isIndeterminate(checked()) ? "mixed" : checked()',
347
495
  '[attr.data-state]': 'getCheckedState(checked())',
348
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
349
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
496
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
497
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
350
498
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
351
499
  '[attr.data-label]': 'label() ?? undefined',
352
500
  '(focus)': 'onFocus()',
353
501
  '(blur)': 'onBlur()',
354
502
  '(pointermove)': 'onPointerMove($event)',
355
503
  '(pointerleave)': 'onPointerLeave($event)',
504
+ '(mouseup)': 'onMouseUp($event)',
356
505
  '(click)': 'onItemClick()',
357
506
  '(keydown.enter)': 'onActivate($event)',
358
507
  '(keydown.space)': 'onActivate($event)'
@@ -389,20 +538,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
389
538
  }]
390
539
  }], propDecorators: { keepMounted: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepMounted", required: false }] }] } });
391
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
+ };
392
547
  /**
393
548
  * Groups related menu items together.
394
549
  */
395
550
  class RdxMenuGroup {
551
+ constructor() {
552
+ this.labelId = signal(undefined, ...(ngDevMode ? [{ debugName: "labelId" }] : /* istanbul ignore next */ []));
553
+ }
396
554
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
397
- 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 }); }
398
556
  }
399
557
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroup, decorators: [{
400
558
  type: Directive,
401
559
  args: [{
402
560
  selector: '[rdxMenuGroup]',
403
561
  exportAs: 'rdxMenuGroup',
562
+ providers: [provideRdxMenuGroupContext(groupContextFactory$1)],
404
563
  host: {
405
- role: 'group'
564
+ role: 'group',
565
+ '[attr.aria-labelledby]': 'labelId()'
406
566
  }
407
567
  }]
408
568
  }] });
@@ -411,16 +571,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
411
571
  * A label for a menu group.
412
572
  */
413
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
+ }
414
584
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroupLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
415
- 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 }); }
416
586
  }
417
587
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuGroupLabel, decorators: [{
418
588
  type: Directive,
419
589
  args: [{
420
590
  selector: '[rdxMenuGroupLabel]',
421
- exportAs: 'rdxMenuGroupLabel'
591
+ exportAs: 'rdxMenuGroupLabel',
592
+ host: {
593
+ '[attr.id]': 'id'
594
+ }
422
595
  }]
423
- }] });
596
+ }], ctorParameters: () => [] });
424
597
 
425
598
  /**
426
599
  * An individual menu item.
@@ -439,9 +612,10 @@ class RdxMenuItem {
439
612
  /** Emits when the item is selected. */
440
613
  this.onSelect = output();
441
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 */ []));
442
616
  }
443
617
  onFocus() {
444
- if (!this.disabled()) {
618
+ if (!this.effectiveDisabled()) {
445
619
  this.isFocused.set(true);
446
620
  }
447
621
  }
@@ -449,13 +623,13 @@ class RdxMenuItem {
449
623
  this.isFocused.set(false);
450
624
  }
451
625
  onPointerMove(event) {
452
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
626
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
453
627
  return;
454
628
  }
455
629
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
456
630
  return;
457
631
  }
458
- if (document.activeElement !== this.elementRef.nativeElement) {
632
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
459
633
  this.elementRef.nativeElement.focus({ preventScroll: true });
460
634
  }
461
635
  }
@@ -465,27 +639,34 @@ class RdxMenuItem {
465
639
  }
466
640
  // Clear highlight when the pointer leaves: move focus back to the popup. A subsequent
467
641
  // pointermove on a sibling item re-focuses it, so moving between items still works.
468
- if (document.activeElement === this.elementRef.nativeElement) {
642
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
469
643
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
470
644
  }
471
645
  }
472
646
  onItemClick() {
473
- if (this.disabled())
647
+ if (this.effectiveDisabled())
474
648
  return;
475
649
  this.onSelect.emit();
476
650
  if (this.closeOnClick())
477
- 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();
478
659
  }
479
660
  onActivate(event) {
480
- if (this.disabled())
661
+ if (this.effectiveDisabled())
481
662
  return;
482
663
  event.preventDefault();
483
664
  this.onSelect.emit();
484
665
  if (this.closeOnClick())
485
- this.rootContext?.close();
666
+ this.rootContext?.closeEntireMenu();
486
667
  }
487
668
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
488
- 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 }); }
489
670
  }
490
671
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuItem, decorators: [{
491
672
  type: Directive,
@@ -495,14 +676,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
495
676
  host: {
496
677
  role: 'menuitem',
497
678
  tabindex: '-1',
498
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
499
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
679
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
680
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
500
681
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
501
682
  '[attr.data-label]': 'label() ?? undefined',
502
683
  '(focus)': 'onFocus()',
503
684
  '(blur)': 'onBlur()',
504
685
  '(pointermove)': 'onPointerMove($event)',
505
686
  '(pointerleave)': 'onPointerLeave($event)',
687
+ '(mouseup)': 'onMouseUp($event)',
506
688
  '(click)': 'onItemClick()',
507
689
  '(keydown.enter)': 'onActivate($event)',
508
690
  '(keydown.space)': 'onActivate($event)'
@@ -527,9 +709,10 @@ class RdxMenuLinkItem {
527
709
  /** Emits when the item is selected. */
528
710
  this.onSelect = output();
529
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 */ []));
530
713
  }
531
714
  onFocus() {
532
- if (!this.disabled()) {
715
+ if (!this.effectiveDisabled()) {
533
716
  this.isFocused.set(true);
534
717
  }
535
718
  }
@@ -537,13 +720,13 @@ class RdxMenuLinkItem {
537
720
  this.isFocused.set(false);
538
721
  }
539
722
  onPointerMove(event) {
540
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
723
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
541
724
  return;
542
725
  }
543
726
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
544
727
  return;
545
728
  }
546
- if (document.activeElement !== this.elementRef.nativeElement) {
729
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
547
730
  this.elementRef.nativeElement.focus({ preventScroll: true });
548
731
  }
549
732
  }
@@ -551,30 +734,37 @@ class RdxMenuLinkItem {
551
734
  if (event.pointerType !== 'mouse') {
552
735
  return;
553
736
  }
554
- if (document.activeElement === this.elementRef.nativeElement) {
737
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
555
738
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
556
739
  }
557
740
  }
558
741
  onItemClick(event) {
559
- if (this.disabled()) {
742
+ if (this.effectiveDisabled()) {
560
743
  event.preventDefault();
561
744
  return;
562
745
  }
563
746
  this.onSelect.emit();
564
747
  if (this.closeOnClick())
565
- 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();
566
756
  }
567
757
  onActivate(event) {
568
- if (this.disabled()) {
758
+ if (this.effectiveDisabled()) {
569
759
  event.preventDefault();
570
760
  return;
571
761
  }
572
762
  this.onSelect.emit();
573
763
  if (this.closeOnClick())
574
- this.rootContext?.close();
764
+ this.rootContext?.closeEntireMenu();
575
765
  }
576
766
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuLinkItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
577
- 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 }); }
578
768
  }
579
769
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuLinkItem, decorators: [{
580
770
  type: Directive,
@@ -584,62 +774,64 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
584
774
  host: {
585
775
  role: 'menuitem',
586
776
  tabindex: '-1',
587
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
588
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
777
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
778
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
589
779
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
590
780
  '[attr.data-label]': 'label() ?? undefined',
591
781
  '(focus)': 'onFocus()',
592
782
  '(blur)': 'onBlur()',
593
783
  '(pointermove)': 'onPointerMove($event)',
594
784
  '(pointerleave)': 'onPointerLeave($event)',
785
+ '(mouseup)': 'onMouseUp($event)',
595
786
  '(click)': 'onItemClick($event)',
596
787
  '(keydown.enter)': 'onActivate($event)'
597
788
  }
598
789
  }]
599
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"] }] } });
600
791
 
601
- /** Selector for focusable menu items within the popup. */
602
- const ITEM_SELECTOR = [
792
+ /** Selector for focusable menu items within a popup. */
793
+ const RDX_MENU_ITEM_SELECTOR = [
603
794
  '[rdxMenuItem]:not([data-disabled])',
604
795
  '[rdxMenuCheckboxItem]:not([data-disabled])',
605
796
  '[rdxMenuRadioItem]:not([data-disabled])',
606
797
  '[rdxMenuLinkItem]:not([data-disabled])',
607
798
  '[rdxMenuSubTrigger]:not([data-disabled])'
608
799
  ].join(',');
609
- function getFocusableItems(popup) {
610
- // Exclude items that belong to a nested child popup (submenu).
611
- 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);
612
802
  }
803
+
613
804
  /**
614
805
  * A container for the menu contents.
615
806
  */
616
807
  class RdxMenuPopup {
617
808
  constructor() {
618
809
  this.rootContext = injectRdxMenuRootContext();
619
- 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);
620
813
  this.focusScope = inject(RdxFocusScope);
621
814
  this.wrapper = inject(RdxPopperContentWrapper, { optional: true });
622
815
  this.elementRef = inject(ElementRef);
623
- this.dismissableLayersContext = inject(RdxDismissableLayersContextToken);
624
816
  this.search = '';
625
817
  this.align = computed(() => this.wrapper?.placedAlign(), ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
626
818
  this.side = computed(() => this.wrapper?.placedSide(), ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
627
819
  /**
628
820
  * Event handler called when the escape key is pressed. Can be prevented.
629
821
  */
630
- this.escapeKeyDown = outputFromObservable(outputToObservable(this.dismissableLayer.escapeKeyDown));
822
+ this.escapeKeyDown = output();
631
823
  /**
632
824
  * Event handler called when a pointerdown event happens outside of the popup. Can be prevented.
633
825
  */
634
- this.pointerDownOutside = outputFromObservable(outputToObservable(this.dismissableLayer.pointerDownOutside));
826
+ this.pointerDownOutside = output();
635
827
  /**
636
828
  * Event handler called when focus moves outside of the popup. Can be prevented.
637
829
  */
638
- this.focusOutside = outputFromObservable(outputToObservable(this.dismissableLayer.focusOutside));
830
+ this.focusOutside = output();
639
831
  /**
640
832
  * Event handler called when an interaction happens outside of the popup. Can be prevented.
641
833
  */
642
- this.interactOutside = outputFromObservable(outputToObservable(this.dismissableLayer.interactOutside));
834
+ this.interactOutside = output();
643
835
  /**
644
836
  * Event handler called before focus moves into the popup. Can be prevented.
645
837
  */
@@ -648,6 +840,27 @@ class RdxMenuPopup {
648
840
  * Event handler called before focus returns after the popup is removed. Can be prevented.
649
841
  */
650
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);
651
864
  const unregister = this.rootContext.registerTransitionElement(this.elementRef.nativeElement);
652
865
  const unregisterPopup = this.rootContext.registerPopup(this.elementRef.nativeElement);
653
866
  inject(DestroyRef).onDestroy(() => {
@@ -655,34 +868,52 @@ class RdxMenuPopup {
655
868
  unregisterPopup();
656
869
  clearTimeout(this.searchTimer);
657
870
  });
658
- effect((onCleanup) => {
659
- 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') {
660
878
  return;
661
879
  }
662
- const element = this.elementRef.nativeElement;
663
- this.dismissableLayersContext.branches.update((branches) => [...branches, element]);
664
- onCleanup(() => {
665
- this.dismissableLayersContext.branches.update((branches) => branches.filter((branch) => branch !== element));
666
- });
880
+ this.scheduleSubmenuKeyboardFocus();
667
881
  });
668
- this.dismissableLayer.dismiss.subscribe(() => {
669
- 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
+ }
670
910
  });
671
- // Move focus into the popup when the menu opens unless the opener suppressed it
672
- // (e.g. menubar hover-switching, where focus stays on the trigger).
673
- effect(() => {
674
- const autoFocus = this.rootContext.autoFocus();
675
- if (this.rootContext.isOpen() && autoFocus) {
676
- requestAnimationFrame(() => {
677
- // `'popup'` focuses the container without highlighting an item (pointer opening).
678
- if (autoFocus === 'popup') {
679
- this.elementRef.nativeElement.focus({ preventScroll: true });
680
- return;
681
- }
682
- const items = getFocusableItems(this.elementRef.nativeElement);
683
- const item = autoFocus === 'last' ? items[items.length - 1] : items[0];
684
- item?.focus({ preventScroll: true });
685
- });
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);
686
917
  }
687
918
  });
688
919
  }
@@ -695,8 +926,8 @@ class RdxMenuPopup {
695
926
  }
696
927
  handleKeydown(event) {
697
928
  const el = this.elementRef.nativeElement;
698
- const items = getFocusableItems(el);
699
- const current = document.activeElement;
929
+ const items = getFocusableMenuItems(el);
930
+ const current = el.ownerDocument.activeElement;
700
931
  const currentIndex = items.indexOf(current);
701
932
  switch (event.key) {
702
933
  case 'ArrowDown': {
@@ -744,6 +975,13 @@ class RdxMenuPopup {
744
975
  }
745
976
  break;
746
977
  }
978
+ if (this.rootContext.dir() === 'rtl') {
979
+ if (this.rootContext.handlePopupArrowNavigation(-1)) {
980
+ event.preventDefault();
981
+ event.stopPropagation();
982
+ }
983
+ break;
984
+ }
747
985
  // Close this popup and return focus to the trigger (used by submenus).
748
986
  event.preventDefault();
749
987
  event.stopPropagation();
@@ -752,22 +990,23 @@ class RdxMenuPopup {
752
990
  break;
753
991
  }
754
992
  case 'ArrowRight': {
755
- if (this.rootContext.handlePopupArrowNavigation(1)) {
993
+ const trigger = this.rootContext.trigger();
994
+ if (trigger?.hasAttribute('rdxMenuSubTrigger') && this.rootContext.dir() === 'rtl') {
756
995
  event.preventDefault();
757
996
  event.stopPropagation();
997
+ this.rootContext.close();
998
+ trigger.focus({ preventScroll: true });
999
+ break;
758
1000
  }
759
- break;
760
- }
761
- case 'Escape': {
762
- event.preventDefault();
763
- event.stopPropagation();
764
- this.rootContext.close();
765
- if (this.rootContext.isSubmenu() && this.rootContext.closeParentOnEsc()) {
766
- this.rootContext.closeParent();
1001
+ if (this.rootContext.handlePopupArrowNavigation(1)) {
1002
+ event.preventDefault();
1003
+ event.stopPropagation();
767
1004
  }
768
- this.rootContext.trigger()?.focus({ preventScroll: true });
769
1005
  break;
770
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.
771
1010
  case 'Tab': {
772
1011
  // Close on tab to allow natural tab navigation
773
1012
  this.rootContext.close();
@@ -797,35 +1036,130 @@ class RdxMenuPopup {
797
1036
  }
798
1037
  }
799
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
+ }
800
1062
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
801
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: [
802
- provideRdxDismissableLayerConfig(() => {
1064
+ provideFloatingFocusManagerConfig(() => {
803
1065
  const rootContext = injectRdxMenuRootContext();
1066
+ const popup = inject(ElementRef).nativeElement;
804
1067
  return {
805
- 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()
806
1107
  };
807
- }),
808
- provideRdxFocusScopeConfig(() => ({
809
- trapped: signal(false)
810
- }))
811
- ], 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 }); }
812
1110
  }
813
1111
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPopup, decorators: [{
814
1112
  type: Directive,
815
1113
  args: [{
816
1114
  selector: '[rdxMenuPopup]',
817
1115
  exportAs: 'rdxMenuPopup',
818
- hostDirectives: [RdxPopperContent, RdxDismissableLayer, RdxFocusScope],
1116
+ hostDirectives: [RdxPopperContent, RdxFloatingNodeRegistration, RdxFloatingFocusManager],
819
1117
  providers: [
820
- provideRdxDismissableLayerConfig(() => {
1118
+ provideFloatingFocusManagerConfig(() => {
821
1119
  const rootContext = injectRdxMenuRootContext();
1120
+ const popup = inject(ElementRef).nativeElement;
822
1121
  return {
823
- 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()
824
1161
  };
825
- }),
826
- provideRdxFocusScopeConfig(() => ({
827
- trapped: signal(false)
828
- }))
1162
+ })
829
1163
  ],
830
1164
  host: {
831
1165
  role: 'menu',
@@ -845,101 +1179,110 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
845
1179
  }], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], openAutoFocus: [{ type: i0.Output, args: ["openAutoFocus"] }], closeAutoFocus: [{ type: i0.Output, args: ["closeAutoFocus"] }] } });
846
1180
 
847
1181
  /**
848
- * Moves the menu to a different part of the DOM.
849
- * Applied on ng-template no host bindings (ng-template is not a real DOM node).
1182
+ * Structural directive that teleports the menu popup into a container (default `document.body`) while
1183
+ * the menu is open, and keeps it mounted until any CSS exit `@keyframes` finishes.
1184
+ *
1185
+ * This replaces the consumer-owned `@if (root.open())` mount: it adds both teleporting *and*
1186
+ * exit-animation support. Apply it with the `*` microsyntax on the positioner —
1187
+ * `<div *rdxMenuPortal rdxMenuPositioner>` — or as an explicit `<ng-template rdxMenuPortal>`. For a
1188
+ * custom container, or a backdrop alongside the positioner (multi-root), use the explicit form.
850
1189
  */
851
1190
  class RdxMenuPortal {
852
- constructor() {
853
- /**
854
- * Optional container to portal the content into. Defaults to `document.body`.
855
- */
856
- this.container = input(...(ngDevMode ? [undefined, { debugName: "container" }] : /* istanbul ignore next */ []));
857
- }
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: "17.1.0", version: "21.2.9", type: RdxMenuPortal, isStandalone: true, selector: "[rdxMenuPortal]", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["rdxMenuPortal"], hostDirectives: [{ directive: i1$1.RdxPortal, 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,
863
1196
  args: [{
864
- selector: '[rdxMenuPortal]',
1197
+ selector: 'ng-template[rdxMenuPortal]',
865
1198
  exportAs: 'rdxMenuPortal',
866
- hostDirectives: [
867
- {
868
- directive: RdxPortal,
869
- inputs: ['container']
870
- }
871
- ]
1199
+ hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
1200
+ providers: [provideRdxPresenceContext(() => ({ present: injectRdxMenuRootContext().present }))]
872
1201
  }]
873
- }], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
1202
+ }] });
1203
+ /**
1204
+ * Dev-mode guard: `rdxMenuPortal` is a structural directive. The old `<div rdxMenuPortal>` markup
1205
+ * would silently stop portaling — fail loudly instead.
1206
+ */
1207
+ class RdxMenuPortalMisuseGuard {
1208
+ constructor() {
1209
+ if (isDevMode()) {
1210
+ rdxDevError('menu/portal-on-element', '`rdxMenuPortal` is a structural directive. ' +
1211
+ 'Use `*rdxMenuPortal` on the positioner element or `<ng-template rdxMenuPortal>`.', 'components/menu');
1212
+ }
1213
+ }
1214
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1215
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxMenuPortalMisuseGuard, isStandalone: true, selector: "[rdxMenuPortal]:not(ng-template)", ngImport: i0 }); }
1216
+ }
1217
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPortalMisuseGuard, decorators: [{
1218
+ type: Directive,
1219
+ args: [{
1220
+ selector: '[rdxMenuPortal]:not(ng-template)'
1221
+ }]
1222
+ }], ctorParameters: () => [] });
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
+ }
874
1262
 
875
1263
  /**
876
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.
877
1269
  */
878
- class RdxMenuPositioner {
1270
+ class RdxMenuPositioner extends RdxPopperContentWrapper {
879
1271
  constructor() {
1272
+ super();
880
1273
  this.rootContext = injectRdxMenuRootContext();
881
- this.wrapper = inject(RdxPopperContentWrapper);
882
- /**
883
- * An element to position the popup against. Defaults to the trigger.
884
- */
885
- this.anchor = input(...(ngDevMode ? [undefined, { debugName: "anchor" }] : /* istanbul ignore next */ []));
886
- /**
887
- * The preferred side of the trigger to render against when open.
888
- */
889
- this.side = input('bottom', ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
890
- /**
891
- * Distance between the trigger and the popup in pixels.
892
- */
893
- this.sideOffset = input(0, { ...(ngDevMode ? { debugName: "sideOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
894
- /**
895
- * How to align the popup relative to the specified side.
896
- */
897
- this.align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
898
- /**
899
- * An offset in pixels from the `start` or `end` alignment options.
900
- */
901
- this.alignOffset = input(0, { ...(ngDevMode ? { debugName: "alignOffset" } : /* istanbul ignore next */ {}), transform: numberAttribute });
902
- /**
903
- * Minimum distance to maintain between the arrow and the edges of the popup.
904
- */
905
- this.arrowPadding = input(5, { ...(ngDevMode ? { debugName: "arrowPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
906
- /**
907
- * Whether to override side and alignment preferences to prevent collisions.
908
- */
909
- this.avoidCollisions = input(true, { ...(ngDevMode ? { debugName: "avoidCollisions" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
910
- /**
911
- * The element used as the collision boundary.
912
- */
913
- this.collisionBoundary = input(...(ngDevMode ? [undefined, { debugName: "collisionBoundary" }] : /* istanbul ignore next */ []));
914
- /**
915
- * Distance in pixels from the boundary edges where collision detection should occur.
916
- */
917
- this.collisionPadding = input(5, ...(ngDevMode ? [{ debugName: "collisionPadding" }] : /* istanbul ignore next */ []));
918
- /**
919
- * The sticky behavior on the alignment axis.
920
- */
921
- this.sticky = input('partial', ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
922
- /**
923
- * Whether to hide the popup when the trigger becomes fully occluded.
924
- */
925
- this.hideWhenDetached = input(false, { ...(ngDevMode ? { debugName: "hideWhenDetached" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
926
- /**
927
- * The CSS position strategy used by Floating UI.
928
- */
929
- this.positionStrategy = input('fixed', ...(ngDevMode ? [{ debugName: "positionStrategy" }] : /* istanbul ignore next */ []));
930
- /**
931
- * Whether to update position on every animation frame.
932
- */
933
- this.updatePositionStrategy = input('always', ...(ngDevMode ? [{ debugName: "updatePositionStrategy" }] : /* istanbul ignore next */ []));
934
- /**
935
- * Emits when the popup has been placed.
936
- */
937
- 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));
938
1280
  }
939
1281
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
940
- 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),
941
1284
  provideRdxPopperContentConfig({ arrowPadding: 5, collisionPadding: 5, updatePositionStrategy: 'always' })
942
- ], 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 }); }
943
1286
  }
944
1287
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuPositioner, decorators: [{
945
1288
  type: Directive,
@@ -947,92 +1290,88 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
947
1290
  selector: '[rdxMenuPositioner]',
948
1291
  exportAs: 'rdxMenuPositioner',
949
1292
  providers: [
1293
+ ...provideRdxPopperContentWrapper(RdxMenuPositioner),
950
1294
  provideRdxPopperContentConfig({ arrowPadding: 5, collisionPadding: 5, updatePositionStrategy: 'always' })
951
1295
  ],
952
- hostDirectives: [
953
- {
954
- directive: RdxPopperContentWrapper,
955
- inputs: [
956
- 'anchor',
957
- 'side',
958
- 'sideOffset',
959
- 'align',
960
- 'alignOffset',
961
- 'arrowPadding',
962
- 'avoidCollisions',
963
- 'collisionBoundary',
964
- 'collisionPadding',
965
- 'sticky',
966
- 'hideWhenDetached',
967
- 'positionStrategy',
968
- 'updatePositionStrategy'
969
- ]
970
- }
971
- ],
972
1296
  host: {
973
1297
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
974
1298
  '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
975
- '[attr.data-anchor-hidden]': 'wrapper.anchorHidden() ? "" : undefined',
976
- '[attr.data-align]': 'wrapper.placedAlign()',
977
- '[attr.data-side]': 'wrapper.placedSide()',
978
- '[style]': `{
979
- '--anchor-width': 'var(--radix-popper-anchor-width)',
980
- '--anchor-height': 'var(--radix-popper-anchor-height)',
981
- '--available-width': 'var(--radix-popper-available-width)',
982
- '--available-height': 'var(--radix-popper-available-height)',
983
- '--positioner-width': 'var(--radix-popper-content-wrapper-width)',
984
- '--positioner-height': 'var(--radix-popper-content-wrapper-height)',
985
- '--transform-origin': 'var(--radix-popper-transform-origin)',
986
- '--radix-menu-content-transform-origin': 'var(--radix-popper-transform-origin)',
987
- '--radix-menu-content-available-width': 'var(--radix-popper-available-width)',
988
- '--radix-menu-content-available-height': 'var(--radix-popper-available-height)',
989
- '--radix-menu-trigger-width': 'var(--radix-popper-anchor-width)',
990
- '--radix-menu-trigger-height': 'var(--radix-popper-anchor-height)'
991
- }`
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'
992
1302
  }
993
1303
  }]
994
- }], 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: () => [] });
995
1305
 
996
1306
  const [injectRdxMenuRadioGroupContext, provideRdxMenuRadioGroupContext] = createContext('RdxMenuRadioGroupContext', 'components/menu');
997
1307
  const radioGroupContextFactory = () => {
998
1308
  const instance = inject(RdxMenuRadioGroup);
999
1309
  return {
1000
1310
  value: instance.value,
1311
+ disabled: instance.disabled,
1001
1312
  selectValue: (v) => instance.selectValue(v)
1002
1313
  };
1003
1314
  };
1315
+ const groupContextFactory = () => {
1316
+ const instance = inject(RdxMenuRadioGroup);
1317
+ return { labelId: instance.labelId };
1318
+ };
1004
1319
  /**
1005
1320
  * Groups radio items in a menu.
1006
1321
  */
1007
1322
  class RdxMenuRadioGroup {
1008
1323
  constructor() {
1324
+ this.hasAppliedDefaultValue = false;
1009
1325
  /**
1010
1326
  * The currently selected value.
1011
1327
  */
1012
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 });
1013
1333
  /**
1014
1334
  * Emits when the selected value changes.
1015
1335
  */
1016
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
+ });
1017
1345
  }
1018
1346
  selectValue(newValue) {
1347
+ if (this.disabled()) {
1348
+ return;
1349
+ }
1019
1350
  this.value.set(newValue);
1020
1351
  this.onValueChange.emit(newValue);
1021
1352
  }
1022
1353
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1023
- 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 }); }
1024
1358
  }
1025
1359
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioGroup, decorators: [{
1026
1360
  type: Directive,
1027
1361
  args: [{
1028
1362
  selector: '[rdxMenuRadioGroup]',
1029
1363
  exportAs: 'rdxMenuRadioGroup',
1030
- providers: [provideRdxMenuRadioGroupContext(radioGroupContextFactory)],
1364
+ providers: [
1365
+ provideRdxMenuRadioGroupContext(radioGroupContextFactory),
1366
+ provideRdxMenuGroupContext(groupContextFactory)
1367
+ ],
1031
1368
  host: {
1032
- role: 'group'
1369
+ role: 'group',
1370
+ '[attr.aria-labelledby]': 'labelId()',
1371
+ '[attr.data-disabled]': 'disabled() ? "" : undefined'
1033
1372
  }
1034
1373
  }]
1035
- }], 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"] }] } });
1036
1375
 
1037
1376
  const [injectRdxMenuRadioItemContext, provideRdxMenuRadioItemContext] = createContext('RdxMenuRadioItemContext', 'components/menu');
1038
1377
  const radioItemContextFactory = () => {
@@ -1062,10 +1401,11 @@ class RdxMenuRadioItem {
1062
1401
  this.onSelect = output();
1063
1402
  this.checked = computed(() => this.radioGroupContext.value() === this.value(), ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
1064
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 */ []));
1065
1405
  this.getCheckedState = getCheckedState;
1066
1406
  }
1067
1407
  onFocus() {
1068
- if (!this.disabled()) {
1408
+ if (!this.effectiveDisabled()) {
1069
1409
  this.isFocused.set(true);
1070
1410
  }
1071
1411
  }
@@ -1073,13 +1413,13 @@ class RdxMenuRadioItem {
1073
1413
  this.isFocused.set(false);
1074
1414
  }
1075
1415
  onPointerMove(event) {
1076
- if (event.defaultPrevented || event.pointerType !== 'mouse' || this.disabled()) {
1416
+ if (event.defaultPrevented || event.pointerType !== 'mouse' || this.effectiveDisabled()) {
1077
1417
  return;
1078
1418
  }
1079
1419
  if (this.rootContext && !this.rootContext.highlightItemOnHover()) {
1080
1420
  return;
1081
1421
  }
1082
- if (document.activeElement !== this.elementRef.nativeElement) {
1422
+ if (this.elementRef.nativeElement.ownerDocument.activeElement !== this.elementRef.nativeElement) {
1083
1423
  this.elementRef.nativeElement.focus({ preventScroll: true });
1084
1424
  }
1085
1425
  }
@@ -1087,18 +1427,25 @@ class RdxMenuRadioItem {
1087
1427
  if (event.pointerType !== 'mouse') {
1088
1428
  return;
1089
1429
  }
1090
- if (document.activeElement === this.elementRef.nativeElement) {
1430
+ if (this.elementRef.nativeElement.ownerDocument.activeElement === this.elementRef.nativeElement) {
1091
1431
  this.elementRef.nativeElement.closest('[rdxMenuPopup]')?.focus({ preventScroll: true });
1092
1432
  }
1093
1433
  }
1094
1434
  onItemClick() {
1095
- if (this.disabled()) {
1435
+ if (this.effectiveDisabled()) {
1096
1436
  return;
1097
1437
  }
1098
1438
  this.selectItem();
1099
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
+ }
1100
1447
  onActivate(event) {
1101
- if (this.disabled()) {
1448
+ if (this.effectiveDisabled()) {
1102
1449
  return;
1103
1450
  }
1104
1451
  event.preventDefault();
@@ -1109,10 +1456,10 @@ class RdxMenuRadioItem {
1109
1456
  this.radioGroupContext.selectValue(v);
1110
1457
  this.onSelect.emit(v);
1111
1458
  if (this.closeOnClick())
1112
- this.rootContext?.close();
1459
+ this.rootContext?.closeEntireMenu();
1113
1460
  }
1114
1461
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1115
- 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 }); }
1116
1463
  }
1117
1464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuRadioItem, decorators: [{
1118
1465
  type: Directive,
@@ -1125,14 +1472,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1125
1472
  tabindex: '-1',
1126
1473
  '[attr.aria-checked]': 'checked()',
1127
1474
  '[attr.data-state]': 'getCheckedState(checked())',
1128
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
1129
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
1475
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
1476
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
1130
1477
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
1131
1478
  '[attr.data-label]': 'label() ?? undefined',
1132
1479
  '(focus)': 'onFocus()',
1133
1480
  '(blur)': 'onBlur()',
1134
1481
  '(pointermove)': 'onPointerMove($event)',
1135
1482
  '(pointerleave)': 'onPointerLeave($event)',
1483
+ '(mouseup)': 'onMouseUp($event)',
1136
1484
  '(click)': 'onItemClick()',
1137
1485
  '(keydown.enter)': 'onActivate($event)',
1138
1486
  '(keydown.space)': 'onActivate($event)'
@@ -1540,6 +1888,7 @@ class RdxMenuSubTrigger {
1540
1888
  this.lastPointer = null;
1541
1889
  /** Whether the current open was initiated by hover (vs keyboard / click). */
1542
1890
  this.openedByHover = false;
1891
+ this.ignoreNextKeyboardClick = false;
1543
1892
  /** Whether this trigger (and therefore the submenu) is disabled. */
1544
1893
  this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1545
1894
  /** Whether this trigger should be treated as a native button. Auto-detected for `<button>`. */
@@ -1554,6 +1903,7 @@ class RdxMenuSubTrigger {
1554
1903
  this.label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1555
1904
  /** Highlighted when focused OR while the submenu is open. */
1556
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 */ []));
1557
1907
  this.nativeButtonState = computed(() => this.nativeButton() || this.elementRef.nativeElement.tagName === 'BUTTON', ...(ngDevMode ? [{ debugName: "nativeButtonState" }] : /* istanbul ignore next */ []));
1558
1908
  this.submenuContext.markAsSubmenu();
1559
1909
  effect((onCleanup) => {
@@ -1582,7 +1932,8 @@ class RdxMenuSubTrigger {
1582
1932
  return;
1583
1933
  }
1584
1934
  const reference = this.elementRef.nativeElement;
1585
- const scope = reference.closest('[rdxMenuPopup]') ?? document.body;
1935
+ const ownerDocument = reference.ownerDocument;
1936
+ const scope = reference.closest('[rdxMenuPopup]') ?? ownerDocument.body;
1586
1937
  const unregisterOpen = registerOpenSubmenu(reference, popup);
1587
1938
  let removeTunnel = applyPointerTunnel(scope, reference, popup);
1588
1939
  const { handler, dispose } = createSafePolygonHandler({
@@ -1600,9 +1951,9 @@ class RdxMenuSubTrigger {
1600
1951
  removeTunnel = undefined;
1601
1952
  }
1602
1953
  });
1603
- document.addEventListener('mousemove', handler);
1954
+ ownerDocument.addEventListener('mousemove', handler);
1604
1955
  onCleanup(() => {
1605
- document.removeEventListener('mousemove', handler);
1956
+ ownerDocument.removeEventListener('mousemove', handler);
1606
1957
  dispose();
1607
1958
  removeTunnel?.();
1608
1959
  unregisterOpen();
@@ -1625,7 +1976,7 @@ class RdxMenuSubTrigger {
1625
1976
  }
1626
1977
  }
1627
1978
  onFocus() {
1628
- if (!this.disabled()) {
1979
+ if (!this.effectiveDisabled()) {
1629
1980
  this.clearSiblingHighlights();
1630
1981
  this.isFocused.set(true);
1631
1982
  }
@@ -1633,18 +1984,71 @@ class RdxMenuSubTrigger {
1633
1984
  onBlur() {
1634
1985
  this.isFocused.set(false);
1635
1986
  }
1636
- onClick() {
1637
- if (this.disabled())
1987
+ onClick(event) {
1988
+ if (this.effectiveDisabled())
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())
1638
2019
  return;
2020
+ event.preventDefault();
2021
+ event.stopPropagation();
2022
+ this.ignoreNextKeyboardClick = true;
1639
2023
  this.openedByHover = false;
1640
2024
  this.clearSiblingHighlights();
1641
2025
  if (!this.submenuContext.isOpen()) {
1642
2026
  this.closeSiblingSubmenus();
2027
+ this.submenuContext.show('first', 'none', event);
1643
2028
  }
1644
- this.submenuContext.toggle();
2029
+ this.focusFirstSubmenuItem();
1645
2030
  }
1646
2031
  onArrowRight(event) {
1647
- if (this.disabled())
2032
+ if (this.submenuContext.dir() === 'rtl') {
2033
+ return;
2034
+ }
2035
+ if (this.effectiveDisabled())
2036
+ return;
2037
+ event.preventDefault();
2038
+ event.stopPropagation();
2039
+ this.openedByHover = false;
2040
+ this.clearSiblingHighlights();
2041
+ if (!this.submenuContext.isOpen()) {
2042
+ this.closeSiblingSubmenus();
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())
1648
2052
  return;
1649
2053
  event.preventDefault();
1650
2054
  event.stopPropagation();
@@ -1652,23 +2056,25 @@ class RdxMenuSubTrigger {
1652
2056
  this.clearSiblingHighlights();
1653
2057
  if (!this.submenuContext.isOpen()) {
1654
2058
  this.closeSiblingSubmenus();
1655
- this.submenuContext.show();
2059
+ this.submenuContext.show('first', 'none', event);
2060
+ this.focusFirstSubmenuItem();
1656
2061
  }
1657
2062
  }
1658
2063
  onPointerMove(event) {
1659
- if (event.pointerType !== 'mouse' || this.disabled() || !this.openOnHover())
2064
+ if (event.pointerType !== 'mouse' || this.effectiveDisabled() || !this.openOnHover())
1660
2065
  return;
1661
2066
  this.lastPointer = { x: event.clientX, y: event.clientY };
1662
2067
  this.clearSiblingHighlights();
1663
- if (this.submenuContext.highlightItemOnHover() && document.activeElement !== this.elementRef.nativeElement) {
1664
- 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 });
1665
2071
  }
1666
2072
  if (!this.submenuContext.isOpen()) {
1667
2073
  clearTimeout(this.openTimer);
1668
2074
  this.closeSiblingSubmenus();
1669
2075
  this.openTimer = setTimeout(() => {
1670
2076
  this.openedByHover = true;
1671
- this.submenuContext.show(false);
2077
+ this.submenuContext.show(false, 'trigger-hover');
1672
2078
  }, this.delay() ?? 100);
1673
2079
  }
1674
2080
  }
@@ -1703,31 +2109,66 @@ class RdxMenuSubTrigger {
1703
2109
  trigger.dispatchEvent(new CustomEvent('rdx-menu-subtrigger-clear-highlight'));
1704
2110
  });
1705
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
+ }
1706
2145
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuSubTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1707
- 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 }); }
1708
2147
  }
1709
2148
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuSubTrigger, decorators: [{
1710
2149
  type: Directive,
1711
2150
  args: [{
1712
2151
  selector: '[rdxMenuSubTrigger]',
1713
2152
  exportAs: 'rdxMenuSubTrigger',
1714
- hostDirectives: [RdxPopperAnchor, RdxDismissableLayerBranch],
2153
+ hostDirectives: [RdxPopperAnchor],
1715
2154
  host: {
1716
2155
  '[attr.type]': 'nativeButtonState() ? "button" : undefined',
1717
2156
  role: 'menuitem',
1718
2157
  tabindex: '-1',
1719
2158
  '[attr.aria-haspopup]': '"menu"',
1720
2159
  '[attr.aria-expanded]': 'submenuContext.isOpen()',
1721
- '[attr.aria-disabled]': 'disabled() ? true : undefined',
1722
- '[attr.disabled]': 'nativeButtonState() && disabled() ? "" : undefined',
2160
+ '[attr.aria-disabled]': 'effectiveDisabled() ? true : undefined',
2161
+ '[attr.disabled]': 'nativeButtonState() && effectiveDisabled() ? "" : undefined',
1723
2162
  '[attr.data-state]': 'submenuContext.isOpen() ? "open" : "closed"',
1724
2163
  '[attr.data-popup-open]': 'submenuContext.isOpen() ? "" : undefined',
1725
2164
  '[attr.data-highlighted]': 'highlighted() ? "" : undefined',
1726
- '[attr.data-disabled]': 'disabled() ? "" : undefined',
2165
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : undefined',
1727
2166
  '[attr.data-label]': 'label() ?? undefined',
1728
2167
  '(focus)': 'onFocus()',
1729
2168
  '(blur)': 'onBlur()',
1730
- '(click)': 'onClick()',
2169
+ '(click)': 'onClick($event)',
2170
+ '(keydown.enter)': 'onEnter($event)',
2171
+ '(keydown.arrowleft)': 'onArrowLeft($event)',
1731
2172
  '(keydown.arrowright)': 'onArrowRight($event)',
1732
2173
  '(pointermove)': 'onPointerMove($event)',
1733
2174
  '(pointerleave)': 'onPointerLeave()',
@@ -1745,7 +2186,23 @@ class RdxMenuTrigger {
1745
2186
  this.rootContext = injectRdxMenuRootContext();
1746
2187
  this.elementRef = inject(ElementRef);
1747
2188
  this.destroyRef = inject(DestroyRef);
1748
- 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
+ };
1749
2206
  /** Whether this trigger should be treated as a native button. Auto-detected for `<button>`. */
1750
2207
  this.nativeButton = input(false, { ...(ngDevMode ? { debugName: "nativeButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1751
2208
  /** Whether this trigger is disabled. */
@@ -1753,7 +2210,7 @@ class RdxMenuTrigger {
1753
2210
  /** Whether hovering the trigger opens the menu. */
1754
2211
  this.openOnHover = input(false, { ...(ngDevMode ? { debugName: "openOnHover" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1755
2212
  /** Delay before hover opens the menu, in milliseconds. */
1756
- 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 });
1757
2214
  /** Delay before hover leave closes the menu, in milliseconds. */
1758
2215
  this.closeDelay = input(undefined, { ...(ngDevMode ? { debugName: "closeDelay" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
1759
2216
  this.nativeButtonState = computed(() => this.nativeButton() || this.elementRef.nativeElement.tagName === 'BUTTON', ...(ngDevMode ? [{ debugName: "nativeButtonState" }] : /* istanbul ignore next */ []));
@@ -1763,33 +2220,97 @@ class RdxMenuTrigger {
1763
2220
  const unregister = this.rootContext.registerTrigger(el);
1764
2221
  onCleanup(unregister);
1765
2222
  });
1766
- // When a coordinator (e.g. the menubar) drives this trigger, hover-switching focuses the
1767
- // trigger and opens the popup without pulling focus inside it. Register the trigger as a
1768
- // dismissable-layer branch so that focus/pointer interactions on it are treated as "inside"
1769
- // and do not dismiss the just-opened popup.
1770
2223
  effect((onCleanup) => {
1771
- 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;
1772
2229
  return;
1773
2230
  }
1774
- const el = this.elementRef.nativeElement;
1775
- 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);
1776
2252
  onCleanup(() => {
1777
- this.dismissableLayersContext.branches.update((branches) => branches.filter((b) => b !== el));
2253
+ ownerDocument.removeEventListener('mousemove', handler);
2254
+ dispose();
2255
+ removeTunnel?.();
2256
+ this.clearCloseTimer();
1778
2257
  });
1779
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.)
1780
2262
  this.destroyRef.onDestroy(() => {
1781
2263
  this.clearOpenTimer();
1782
2264
  this.clearCloseTimer();
2265
+ this.clearMouseUpGuard();
1783
2266
  });
1784
2267
  }
1785
- 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) {
1786
2290
  if (this.isDisabled()) {
1787
2291
  return;
1788
2292
  }
2293
+ const wasOpen = this.rootContext.isOpen();
1789
2294
  if (this.rootContext.handleTriggerInteraction({ type: 'click' })) {
2295
+ if (event.detail === 0 && !wasOpen && this.rootContext.isOpen()) {
2296
+ this.restoreKeyboardPopupFocus();
2297
+ }
2298
+ return;
2299
+ }
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;
1790
2307
  return;
1791
2308
  }
1792
- this.rootContext.toggle();
2309
+ this.openedByHover = false;
2310
+ this.rootContext.toggle('trigger-press', event);
2311
+ if (event.detail === 0 && !wasOpen && this.rootContext.isOpen()) {
2312
+ this.restoreKeyboardPopupFocus();
2313
+ }
1793
2314
  }
1794
2315
  handleArrowDown(event) {
1795
2316
  if (this.rootContext.handleTriggerInteraction({ type: 'arrowdown', event })) {
@@ -1825,11 +2346,27 @@ class RdxMenuTrigger {
1825
2346
  this.rootContext.handleTriggerInteraction({ type: 'escape', event });
1826
2347
  }
1827
2348
  handleKeyboardToggle(event) {
1828
- 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
+ }
1829
2361
  return;
1830
2362
  }
1831
2363
  event.preventDefault();
1832
- 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
+ }
1833
2370
  }
1834
2371
  handlePointerEnter(event) {
1835
2372
  if (this.rootContext.handleTriggerInteraction({ type: 'pointerenter', event })) {
@@ -1840,14 +2377,17 @@ class RdxMenuTrigger {
1840
2377
  }
1841
2378
  this.clearCloseTimer();
1842
2379
  this.clearOpenTimer();
1843
- const delay = this.delay() ?? 0;
2380
+ this.lastPointer = { x: event.clientX, y: event.clientY };
2381
+ const delay = this.delay() ?? 100;
1844
2382
  if (delay <= 0) {
1845
- this.rootContext.show();
2383
+ this.openedByHover = true;
2384
+ this.rootContext.show('first', 'trigger-hover');
1846
2385
  return;
1847
2386
  }
1848
2387
  this.openTimer = setTimeout(() => {
1849
2388
  this.openTimer = undefined;
1850
- this.rootContext.show();
2389
+ this.openedByHover = true;
2390
+ this.rootContext.show('first', 'trigger-hover');
1851
2391
  }, delay);
1852
2392
  }
1853
2393
  handlePointerLeave(event) {
@@ -1855,12 +2395,19 @@ class RdxMenuTrigger {
1855
2395
  return;
1856
2396
  }
1857
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() {
1858
2409
  this.clearCloseTimer();
1859
2410
  const closeDelay = this.closeDelay() ?? 0;
1860
- if (closeDelay <= 0) {
1861
- this.rootContext.close();
1862
- return;
1863
- }
1864
2411
  this.closeTimer = setTimeout(() => {
1865
2412
  this.closeTimer = undefined;
1866
2413
  this.rootContext.close();
@@ -1874,8 +2421,50 @@ class RdxMenuTrigger {
1874
2421
  clearTimeout(this.closeTimer);
1875
2422
  this.closeTimer = undefined;
1876
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
+ }
1877
2466
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1878
- 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 }); }
1879
2468
  }
1880
2469
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxMenuTrigger, decorators: [{
1881
2470
  type: Directive,
@@ -1894,8 +2483,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1894
2483
  '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"',
1895
2484
  '[attr.data-disabled]': 'isDisabled() ? "" : undefined',
1896
2485
  '[attr.data-popup-open]': 'rootContext.isOpen() ? "" : undefined',
1897
- '(click)': 'handleClick()',
2486
+ '[style.pointer-events]': 'rootContext.isOpen() && rootContext.modal() ? "auto" : undefined',
2487
+ '(mousedown)': 'handleMouseDown($event)',
2488
+ '(click)': 'handleClick($event)',
1898
2489
  '(pointerenter)': 'handlePointerEnter($event)',
2490
+ '(pointermove)': 'handlePointerMove($event)',
1899
2491
  '(pointerleave)': 'handlePointerLeave($event)',
1900
2492
  '(keydown.arrowdown)': 'handleArrowDown($event)',
1901
2493
  '(keydown.arrowup)': 'handleArrowUp($event)',
@@ -1991,6 +2583,7 @@ const menuImports = [
1991
2583
  RdxMenuTrigger,
1992
2584
  RdxMenuSubTrigger,
1993
2585
  RdxMenuPortal,
2586
+ RdxMenuPortalMisuseGuard,
1994
2587
  RdxMenuPositioner,
1995
2588
  RdxMenuPopup,
1996
2589
  RdxMenuViewport,
@@ -2013,6 +2606,7 @@ class RdxMenuModule {
2013
2606
  RdxMenuTrigger,
2014
2607
  RdxMenuSubTrigger,
2015
2608
  RdxMenuPortal,
2609
+ RdxMenuPortalMisuseGuard,
2016
2610
  RdxMenuPositioner,
2017
2611
  RdxMenuPopup,
2018
2612
  RdxMenuViewport,
@@ -2031,6 +2625,7 @@ class RdxMenuModule {
2031
2625
  RdxMenuTrigger,
2032
2626
  RdxMenuSubTrigger,
2033
2627
  RdxMenuPortal,
2628
+ RdxMenuPortalMisuseGuard,
2034
2629
  RdxMenuPositioner,
2035
2630
  RdxMenuPopup,
2036
2631
  RdxMenuViewport,
@@ -2060,5 +2655,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2060
2655
  * Generated bundle index. Do not edit.
2061
2656
  */
2062
2657
 
2063
- export { RdxMenuArrow, RdxMenuBackdrop, RdxMenuCheckboxItem, RdxMenuCheckboxItemIndicator, RdxMenuGroup, RdxMenuGroupLabel, RdxMenuItem, RdxMenuLinkItem, RdxMenuModule, RdxMenuPopup, RdxMenuPortal, 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 };
2064
2659
  //# sourceMappingURL=radix-ng-primitives-menu.mjs.map