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