ngx-com 0.0.1 → 0.0.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/fesm2022/ngx-com-components-avatar.mjs +772 -0
- package/fesm2022/ngx-com-components-avatar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-badge.mjs +138 -0
- package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
- package/fesm2022/ngx-com-components-button.mjs +146 -0
- package/fesm2022/ngx-com-components-button.mjs.map +1 -0
- package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
- package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
- package/fesm2022/ngx-com-components-card.mjs +590 -0
- package/fesm2022/ngx-com-components-card.mjs.map +1 -0
- package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
- package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
- package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
- package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
- package/fesm2022/ngx-com-components-confirm.mjs +562 -0
- package/fesm2022/ngx-com-components-confirm.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
- package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
- package/fesm2022/ngx-com-components-dropdown.mjs +2692 -0
- package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
- package/fesm2022/ngx-com-components-empty-state.mjs +382 -0
- package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -0
- package/fesm2022/ngx-com-components-form-field.mjs +924 -0
- package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
- package/fesm2022/ngx-com-components-icon.mjs +183 -0
- package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
- package/fesm2022/ngx-com-components-item.mjs +578 -0
- package/fesm2022/ngx-com-components-item.mjs.map +1 -0
- package/fesm2022/ngx-com-components-menu.mjs +1200 -0
- package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
- package/fesm2022/ngx-com-components-paginator.mjs +823 -0
- package/fesm2022/ngx-com-components-paginator.mjs.map +1 -0
- package/fesm2022/ngx-com-components-popover.mjs +901 -0
- package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
- package/fesm2022/ngx-com-components-radio.mjs +621 -0
- package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs +538 -0
- package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -0
- package/fesm2022/ngx-com-components-sort.mjs +368 -0
- package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
- package/fesm2022/ngx-com-components-spinner.mjs +189 -0
- package/fesm2022/ngx-com-components-spinner.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
- package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
- package/fesm2022/ngx-com-components-tooltip.mjs +625 -0
- package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -0
- package/fesm2022/ngx-com-components.mjs +17 -0
- package/fesm2022/ngx-com-components.mjs.map +1 -0
- package/fesm2022/ngx-com-tokens.mjs +12 -0
- package/fesm2022/ngx-com-tokens.mjs.map +1 -0
- package/fesm2022/ngx-com-utils.mjs +601 -0
- package/fesm2022/ngx-com-utils.mjs.map +1 -0
- package/fesm2022/ngx-com.mjs +9 -23
- package/fesm2022/ngx-com.mjs.map +1 -1
- package/package.json +105 -1
- package/types/ngx-com-components-avatar.d.ts +409 -0
- package/types/ngx-com-components-badge.d.ts +97 -0
- package/types/ngx-com-components-button.d.ts +69 -0
- package/types/ngx-com-components-calendar.d.ts +1665 -0
- package/types/ngx-com-components-card.d.ts +373 -0
- package/types/ngx-com-components-checkbox.d.ts +116 -0
- package/types/ngx-com-components-collapsible.d.ts +379 -0
- package/types/ngx-com-components-confirm.d.ts +160 -0
- package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
- package/types/ngx-com-components-dropdown.d.ts +938 -0
- package/types/ngx-com-components-empty-state.d.ts +269 -0
- package/types/ngx-com-components-form-field.d.ts +531 -0
- package/types/ngx-com-components-icon.d.ts +94 -0
- package/types/ngx-com-components-item.d.ts +336 -0
- package/types/ngx-com-components-menu.d.ts +479 -0
- package/types/ngx-com-components-paginator.d.ts +265 -0
- package/types/ngx-com-components-popover.d.ts +309 -0
- package/types/ngx-com-components-radio.d.ts +258 -0
- package/types/ngx-com-components-segmented-control.d.ts +274 -0
- package/types/ngx-com-components-sort.d.ts +133 -0
- package/types/ngx-com-components-spinner.d.ts +120 -0
- package/types/ngx-com-components-tabs.d.ts +396 -0
- package/types/ngx-com-components-tooltip.d.ts +200 -0
- package/types/ngx-com-components.d.ts +12 -0
- package/types/ngx-com-tokens.d.ts +7 -0
- package/types/ngx-com-utils.d.ts +424 -0
- package/types/ngx-com.d.ts +10 -7
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, ElementRef, ViewContainerRef, Injector, DestroyRef, Renderer2, computed, input, booleanAttribute, model, output, Directive, signal, contentChildren, effect, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { Overlay } from '@angular/cdk/overlay';
|
|
5
|
+
import { TemplatePortal } from '@angular/cdk/portal';
|
|
6
|
+
import { filter } from 'rxjs/operators';
|
|
7
|
+
import { buildPopoverPositions } from 'ngx-com/components/popover';
|
|
8
|
+
import { mergeClasses } from 'ngx-com/utils';
|
|
9
|
+
import { FocusKeyManager } from '@angular/cdk/a11y';
|
|
10
|
+
import { cva } from 'class-variance-authority';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Token to access the root menu trigger from anywhere in the menu tree.
|
|
14
|
+
* Used by items to close the entire menu on selection.
|
|
15
|
+
*/
|
|
16
|
+
const ROOT_MENU_TRIGGER = new InjectionToken('ROOT_MENU_TRIGGER');
|
|
17
|
+
/**
|
|
18
|
+
* Token to access the nearest parent menu component.
|
|
19
|
+
* Used by items and submenu triggers to coordinate with their parent.
|
|
20
|
+
*/
|
|
21
|
+
const MENU_REF = new InjectionToken('MENU_REF');
|
|
22
|
+
|
|
23
|
+
// Re-export from shared utilities
|
|
24
|
+
let menuIdCounter = 0;
|
|
25
|
+
/**
|
|
26
|
+
* Generate a unique ID for a menu instance.
|
|
27
|
+
*/
|
|
28
|
+
function generateMenuId() {
|
|
29
|
+
return `menu-${++menuIdCounter}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unified menu trigger directive — opens a menu in a CDK overlay.
|
|
34
|
+
*
|
|
35
|
+
* **Root context** (outside a menu): Opens on click, has backdrop.
|
|
36
|
+
* **Submenu context** (inside a menu): Opens on hover/ArrowRight, no backdrop.
|
|
37
|
+
*
|
|
38
|
+
* @tokens `--color-popover`, `--color-popover-foreground`, `--color-border`, `--shadow-lg`
|
|
39
|
+
*
|
|
40
|
+
* @example Root trigger
|
|
41
|
+
* ```html
|
|
42
|
+
* <button comButton [comMenuTrigger]="menu">Options</button>
|
|
43
|
+
* <ng-template #menu>
|
|
44
|
+
* <com-menu>
|
|
45
|
+
* <button comMenuItem>Edit</button>
|
|
46
|
+
* <button comMenuItem>Delete</button>
|
|
47
|
+
* </com-menu>
|
|
48
|
+
* </ng-template>
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Submenu trigger (inside a menu, combined with comMenuItem)
|
|
52
|
+
* ```html
|
|
53
|
+
* <button comMenuItem [comMenuTrigger]="shareMenu" side="right" align="start">
|
|
54
|
+
* Share
|
|
55
|
+
* <com-menu-sub-indicator />
|
|
56
|
+
* </button>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
class MenuTriggerDirective {
|
|
60
|
+
overlay = inject(Overlay);
|
|
61
|
+
elementRef = inject(ElementRef);
|
|
62
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
63
|
+
injector = inject(Injector);
|
|
64
|
+
destroyRef = inject(DestroyRef);
|
|
65
|
+
renderer = inject(Renderer2);
|
|
66
|
+
// Context detection - if MENU_REF exists, we're inside a menu (submenu trigger)
|
|
67
|
+
parentMenu = inject(MENU_REF, { optional: true });
|
|
68
|
+
parentRootTrigger = inject(ROOT_MENU_TRIGGER, { optional: true });
|
|
69
|
+
overlayRef = null;
|
|
70
|
+
attachedMenu = null;
|
|
71
|
+
menuId = generateMenuId();
|
|
72
|
+
// Submenu hover timers
|
|
73
|
+
openDelayTimer = null;
|
|
74
|
+
closeDelayTimer = null;
|
|
75
|
+
mouseInSubmenu = false;
|
|
76
|
+
// Listener cleanup functions
|
|
77
|
+
submenuMouseEnterCleanup = null;
|
|
78
|
+
submenuMouseLeaveCleanup = null;
|
|
79
|
+
// ─── Context ───
|
|
80
|
+
/** Whether this trigger is inside a menu (submenu context). */
|
|
81
|
+
isSubmenu = computed(() => !!this.parentMenu, ...(ngDevMode ? [{ debugName: "isSubmenu" }] : []));
|
|
82
|
+
// ─── Inputs ───
|
|
83
|
+
/** Template containing `<com-menu>` with items. */
|
|
84
|
+
comMenuTrigger = input.required(...(ngDevMode ? [{ debugName: "comMenuTrigger" }] : []));
|
|
85
|
+
/** Preferred position direction (root trigger only). */
|
|
86
|
+
menuPosition = input('below', ...(ngDevMode ? [{ debugName: "menuPosition" }] : []));
|
|
87
|
+
/** Alignment along cross-axis (root trigger only). */
|
|
88
|
+
menuAlignment = input('start', ...(ngDevMode ? [{ debugName: "menuAlignment" }] : []));
|
|
89
|
+
/** Gap in px between trigger and menu. */
|
|
90
|
+
menuOffset = input(4, ...(ngDevMode ? [{ debugName: "menuOffset" }] : []));
|
|
91
|
+
/** Prevents opening when true. */
|
|
92
|
+
menuDisabled = input(false, { ...(ngDevMode ? { debugName: "menuDisabled" } : {}), transform: booleanAttribute });
|
|
93
|
+
/** Two-way bindable open state. */
|
|
94
|
+
menuOpen = model(false, ...(ngDevMode ? [{ debugName: "menuOpen" }] : []));
|
|
95
|
+
/** Custom CSS class(es) on the overlay panel. */
|
|
96
|
+
menuPanelClass = input('', ...(ngDevMode ? [{ debugName: "menuPanelClass" }] : []));
|
|
97
|
+
/** Close menu when an item is selected. */
|
|
98
|
+
menuCloseOnSelect = input(true, { ...(ngDevMode ? { debugName: "menuCloseOnSelect" } : {}), transform: booleanAttribute });
|
|
99
|
+
/** Side for submenu positioning (submenu context only). */
|
|
100
|
+
side = input('right', ...(ngDevMode ? [{ debugName: "side" }] : []));
|
|
101
|
+
/** Alignment for submenu positioning (submenu context only). */
|
|
102
|
+
align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : []));
|
|
103
|
+
/** Hover delay before submenu opens in ms (submenu context only). */
|
|
104
|
+
subMenuOpenDelay = input(200, ...(ngDevMode ? [{ debugName: "subMenuOpenDelay" }] : []));
|
|
105
|
+
/** Hover delay before submenu closes in ms (submenu context only). */
|
|
106
|
+
subMenuCloseDelay = input(150, ...(ngDevMode ? [{ debugName: "subMenuCloseDelay" }] : []));
|
|
107
|
+
// ─── Outputs ───
|
|
108
|
+
/** Emitted after menu opens. */
|
|
109
|
+
menuOpened = output();
|
|
110
|
+
/** Emitted after menu closes. */
|
|
111
|
+
menuClosed = output();
|
|
112
|
+
// ─── Computed ───
|
|
113
|
+
ariaControls = computed(() => this.menuOpen() ? this.menuId : null, ...(ngDevMode ? [{ debugName: "ariaControls" }] : []));
|
|
114
|
+
panelClassArray = computed(() => {
|
|
115
|
+
const panelClass = this.menuPanelClass();
|
|
116
|
+
if (Array.isArray(panelClass))
|
|
117
|
+
return panelClass;
|
|
118
|
+
return panelClass ? [panelClass] : [];
|
|
119
|
+
}, ...(ngDevMode ? [{ debugName: "panelClassArray" }] : []));
|
|
120
|
+
constructor() {
|
|
121
|
+
this.destroyRef.onDestroy(() => this.disposeOverlay());
|
|
122
|
+
}
|
|
123
|
+
// ─── Public API ───
|
|
124
|
+
/** Programmatically open the menu. */
|
|
125
|
+
open() {
|
|
126
|
+
if (!this.menuDisabled() && !this.overlayRef?.hasAttached()) {
|
|
127
|
+
this.openMenu();
|
|
128
|
+
this.menuOpen.set(true);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Programmatically close the menu. */
|
|
132
|
+
close() {
|
|
133
|
+
if (this.isSubmenu()) {
|
|
134
|
+
// For submenu, close this level
|
|
135
|
+
this.closeSubmenu();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// For root, close entire menu tree
|
|
139
|
+
this.closeMenu();
|
|
140
|
+
}
|
|
141
|
+
this.menuOpen.set(false);
|
|
142
|
+
}
|
|
143
|
+
/** Toggle the menu open/close state. */
|
|
144
|
+
toggle() {
|
|
145
|
+
if (this.menuOpen()) {
|
|
146
|
+
this.close();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.open();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Called by MenuComponent to register itself when attached. */
|
|
153
|
+
registerMenu(menu) {
|
|
154
|
+
this.attachedMenu = menu;
|
|
155
|
+
}
|
|
156
|
+
// ─── Event Handlers ───
|
|
157
|
+
onClick(event) {
|
|
158
|
+
if (this.menuDisabled())
|
|
159
|
+
return;
|
|
160
|
+
if (!this.isSubmenu()) {
|
|
161
|
+
this.toggle();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Submenu: prevent comMenuItem's click handler and open submenu
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
this.cancelOpenTimer();
|
|
168
|
+
this.openSubmenuInternal();
|
|
169
|
+
this.focusFirstItemDeferred();
|
|
170
|
+
}
|
|
171
|
+
onArrowDown(event) {
|
|
172
|
+
if (this.isSubmenu() || this.menuOpen())
|
|
173
|
+
return;
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
this.open();
|
|
176
|
+
this.focusFirstItemDeferred();
|
|
177
|
+
}
|
|
178
|
+
onArrowUp(event) {
|
|
179
|
+
if (this.isSubmenu() || this.menuOpen())
|
|
180
|
+
return;
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
this.open();
|
|
183
|
+
this.focusLastItemDeferred();
|
|
184
|
+
}
|
|
185
|
+
onArrowRight(event) {
|
|
186
|
+
if (!this.isSubmenu() || this.side() !== 'right')
|
|
187
|
+
return;
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
event.stopPropagation();
|
|
190
|
+
this.openSubmenuInternal();
|
|
191
|
+
this.focusFirstItemDeferred();
|
|
192
|
+
}
|
|
193
|
+
onArrowLeft(event) {
|
|
194
|
+
if (!this.isSubmenu())
|
|
195
|
+
return;
|
|
196
|
+
if (this.side() === 'left') {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
this.openSubmenuInternal();
|
|
200
|
+
this.focusFirstItemDeferred();
|
|
201
|
+
}
|
|
202
|
+
else if (this.menuOpen()) {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
event.stopPropagation();
|
|
205
|
+
this.closeSubmenu();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
onEnter(event) {
|
|
209
|
+
if (this.menuDisabled())
|
|
210
|
+
return;
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
event.stopPropagation();
|
|
213
|
+
if (this.isSubmenu()) {
|
|
214
|
+
this.openSubmenuInternal();
|
|
215
|
+
this.focusFirstItemDeferred();
|
|
216
|
+
}
|
|
217
|
+
else if (!this.menuOpen()) {
|
|
218
|
+
this.open();
|
|
219
|
+
this.focusFirstItemDeferred();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
onSpace(event) {
|
|
223
|
+
this.onEnter(event);
|
|
224
|
+
}
|
|
225
|
+
onMouseEnter() {
|
|
226
|
+
if (this.menuDisabled())
|
|
227
|
+
return;
|
|
228
|
+
if (!this.isSubmenu())
|
|
229
|
+
return; // Hover only opens submenus
|
|
230
|
+
this.cancelCloseTimer();
|
|
231
|
+
this.openDelayTimer = setTimeout(() => {
|
|
232
|
+
this.openSubmenuInternal();
|
|
233
|
+
}, this.subMenuOpenDelay());
|
|
234
|
+
}
|
|
235
|
+
onMouseLeave() {
|
|
236
|
+
if (!this.isSubmenu())
|
|
237
|
+
return;
|
|
238
|
+
this.cancelOpenTimer();
|
|
239
|
+
this.closeDelayTimer = setTimeout(() => {
|
|
240
|
+
if (!this.mouseInSubmenu) {
|
|
241
|
+
this.closeSubmenu();
|
|
242
|
+
}
|
|
243
|
+
}, this.subMenuCloseDelay());
|
|
244
|
+
}
|
|
245
|
+
focusFirstItemDeferred() {
|
|
246
|
+
setTimeout(() => this.attachedMenu?.focusFirstItem(), 0);
|
|
247
|
+
}
|
|
248
|
+
focusLastItemDeferred() {
|
|
249
|
+
setTimeout(() => this.attachedMenu?.focusLastItem(), 0);
|
|
250
|
+
}
|
|
251
|
+
// ─── Menu Management ───
|
|
252
|
+
openMenu() {
|
|
253
|
+
if (this.menuDisabled())
|
|
254
|
+
return;
|
|
255
|
+
this.createOverlay();
|
|
256
|
+
this.attachContent();
|
|
257
|
+
this.subscribeToCloseEvents();
|
|
258
|
+
this.menuOpened.emit();
|
|
259
|
+
}
|
|
260
|
+
closeMenu() {
|
|
261
|
+
if (this.overlayRef?.hasAttached()) {
|
|
262
|
+
this.overlayRef.detach();
|
|
263
|
+
}
|
|
264
|
+
this.attachedMenu = null;
|
|
265
|
+
this.menuClosed.emit();
|
|
266
|
+
this.elementRef.nativeElement.focus();
|
|
267
|
+
}
|
|
268
|
+
// ─── Submenu Management ───
|
|
269
|
+
openSubmenuInternal() {
|
|
270
|
+
if (this.menuOpen() || this.menuDisabled())
|
|
271
|
+
return;
|
|
272
|
+
this.createOverlay();
|
|
273
|
+
this.attachContent();
|
|
274
|
+
this.subscribeToSubmenuCloseEvents();
|
|
275
|
+
this.menuOpen.set(true);
|
|
276
|
+
this.menuOpened.emit();
|
|
277
|
+
}
|
|
278
|
+
closeSubmenu() {
|
|
279
|
+
if (!this.menuOpen())
|
|
280
|
+
return;
|
|
281
|
+
this.cleanupSubmenuListeners();
|
|
282
|
+
if (this.overlayRef?.hasAttached()) {
|
|
283
|
+
this.overlayRef.detach();
|
|
284
|
+
}
|
|
285
|
+
this.menuOpen.set(false);
|
|
286
|
+
this.attachedMenu = null;
|
|
287
|
+
this.menuClosed.emit();
|
|
288
|
+
}
|
|
289
|
+
// ─── Overlay Management ───
|
|
290
|
+
createOverlay() {
|
|
291
|
+
if (this.overlayRef)
|
|
292
|
+
return;
|
|
293
|
+
const positions = this.isSubmenu()
|
|
294
|
+
? buildPopoverPositions(this.side(), this.align(), 0)
|
|
295
|
+
: buildPopoverPositions(this.menuPosition(), this.menuAlignment(), this.menuOffset());
|
|
296
|
+
const positionStrategy = this.overlay
|
|
297
|
+
.position()
|
|
298
|
+
.flexibleConnectedTo(this.elementRef)
|
|
299
|
+
.withPositions(positions)
|
|
300
|
+
.withFlexibleDimensions(false)
|
|
301
|
+
.withPush(true)
|
|
302
|
+
.withViewportMargin(8);
|
|
303
|
+
this.overlayRef = this.overlay.create({
|
|
304
|
+
positionStrategy,
|
|
305
|
+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
|
306
|
+
hasBackdrop: !this.isSubmenu(),
|
|
307
|
+
backdropClass: this.isSubmenu() ? '' : 'cdk-overlay-transparent-backdrop',
|
|
308
|
+
panelClass: ['com-menu-panel', ...this.panelClassArray()],
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
attachContent() {
|
|
312
|
+
if (!this.overlayRef)
|
|
313
|
+
return;
|
|
314
|
+
const template = this.comMenuTrigger();
|
|
315
|
+
// For submenu, wrap the root trigger to delegate close to the actual root
|
|
316
|
+
const rootTriggerValue = this.isSubmenu()
|
|
317
|
+
? {
|
|
318
|
+
close: () => this.parentRootTrigger?.close(),
|
|
319
|
+
menuCloseOnSelect: () => this.parentRootTrigger?.menuCloseOnSelect() ?? true,
|
|
320
|
+
registerMenu: (menu) => {
|
|
321
|
+
this.attachedMenu = menu;
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
: this;
|
|
325
|
+
const injector = Injector.create({
|
|
326
|
+
parent: this.injector,
|
|
327
|
+
providers: [{ provide: ROOT_MENU_TRIGGER, useValue: rootTriggerValue }],
|
|
328
|
+
});
|
|
329
|
+
const portal = new TemplatePortal(template, this.viewContainerRef, undefined, injector);
|
|
330
|
+
this.overlayRef.attach(portal);
|
|
331
|
+
// For submenu, track mouse in overlay using Renderer2
|
|
332
|
+
if (this.isSubmenu()) {
|
|
333
|
+
const overlayEl = this.overlayRef.overlayElement;
|
|
334
|
+
this.submenuMouseEnterCleanup = this.renderer.listen(overlayEl, 'mouseenter', () => this.onSubmenuMouseEnter());
|
|
335
|
+
this.submenuMouseLeaveCleanup = this.renderer.listen(overlayEl, 'mouseleave', () => this.onSubmenuMouseLeave());
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
subscribeToCloseEvents() {
|
|
339
|
+
if (!this.overlayRef)
|
|
340
|
+
return;
|
|
341
|
+
this.overlayRef
|
|
342
|
+
.backdropClick()
|
|
343
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
344
|
+
.subscribe(() => this.close());
|
|
345
|
+
this.overlayRef
|
|
346
|
+
.keydownEvents()
|
|
347
|
+
.pipe(filter((event) => event.key === 'Escape'), takeUntilDestroyed(this.destroyRef))
|
|
348
|
+
.subscribe((event) => {
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
this.close();
|
|
351
|
+
});
|
|
352
|
+
this.overlayRef
|
|
353
|
+
.detachments()
|
|
354
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
355
|
+
.subscribe(() => {
|
|
356
|
+
this.menuOpen.set(false);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
subscribeToSubmenuCloseEvents() {
|
|
360
|
+
if (!this.overlayRef)
|
|
361
|
+
return;
|
|
362
|
+
const closeKey = this.side() === 'right' ? 'ArrowLeft' : 'ArrowRight';
|
|
363
|
+
this.overlayRef
|
|
364
|
+
.keydownEvents()
|
|
365
|
+
.pipe(filter((event) => event.key === 'Escape' || event.key === closeKey), takeUntilDestroyed(this.destroyRef))
|
|
366
|
+
.subscribe((event) => {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
event.stopPropagation();
|
|
369
|
+
this.closeSubmenu();
|
|
370
|
+
this.elementRef.nativeElement.focus();
|
|
371
|
+
});
|
|
372
|
+
this.overlayRef
|
|
373
|
+
.detachments()
|
|
374
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
375
|
+
.subscribe(() => {
|
|
376
|
+
this.menuOpen.set(false);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// ─── Submenu Hover Tracking ───
|
|
380
|
+
onSubmenuMouseEnter() {
|
|
381
|
+
this.mouseInSubmenu = true;
|
|
382
|
+
this.cancelCloseTimer();
|
|
383
|
+
}
|
|
384
|
+
onSubmenuMouseLeave() {
|
|
385
|
+
this.mouseInSubmenu = false;
|
|
386
|
+
this.closeDelayTimer = setTimeout(() => {
|
|
387
|
+
if (!this.mouseInSubmenu) {
|
|
388
|
+
this.closeSubmenu();
|
|
389
|
+
}
|
|
390
|
+
}, this.subMenuCloseDelay());
|
|
391
|
+
}
|
|
392
|
+
cancelOpenTimer() {
|
|
393
|
+
if (this.openDelayTimer) {
|
|
394
|
+
clearTimeout(this.openDelayTimer);
|
|
395
|
+
this.openDelayTimer = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
cancelCloseTimer() {
|
|
399
|
+
if (this.closeDelayTimer) {
|
|
400
|
+
clearTimeout(this.closeDelayTimer);
|
|
401
|
+
this.closeDelayTimer = null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// ─── Cleanup ───
|
|
405
|
+
disposeOverlay() {
|
|
406
|
+
this.cancelOpenTimer();
|
|
407
|
+
this.cancelCloseTimer();
|
|
408
|
+
this.cleanupSubmenuListeners();
|
|
409
|
+
if (this.overlayRef) {
|
|
410
|
+
this.overlayRef.dispose();
|
|
411
|
+
this.overlayRef = null;
|
|
412
|
+
}
|
|
413
|
+
this.attachedMenu = null;
|
|
414
|
+
}
|
|
415
|
+
cleanupSubmenuListeners() {
|
|
416
|
+
this.submenuMouseEnterCleanup?.();
|
|
417
|
+
this.submenuMouseEnterCleanup = null;
|
|
418
|
+
this.submenuMouseLeaveCleanup?.();
|
|
419
|
+
this.submenuMouseLeaveCleanup = null;
|
|
420
|
+
}
|
|
421
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
422
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: MenuTriggerDirective, isStandalone: true, selector: "[comMenuTrigger]", inputs: { comMenuTrigger: { classPropertyName: "comMenuTrigger", publicName: "comMenuTrigger", isSignal: true, isRequired: true, transformFunction: null }, menuPosition: { classPropertyName: "menuPosition", publicName: "menuPosition", isSignal: true, isRequired: false, transformFunction: null }, menuAlignment: { classPropertyName: "menuAlignment", publicName: "menuAlignment", isSignal: true, isRequired: false, transformFunction: null }, menuOffset: { classPropertyName: "menuOffset", publicName: "menuOffset", isSignal: true, isRequired: false, transformFunction: null }, menuDisabled: { classPropertyName: "menuDisabled", publicName: "menuDisabled", isSignal: true, isRequired: false, transformFunction: null }, menuOpen: { classPropertyName: "menuOpen", publicName: "menuOpen", isSignal: true, isRequired: false, transformFunction: null }, menuPanelClass: { classPropertyName: "menuPanelClass", publicName: "menuPanelClass", isSignal: true, isRequired: false, transformFunction: null }, menuCloseOnSelect: { classPropertyName: "menuCloseOnSelect", publicName: "menuCloseOnSelect", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, subMenuOpenDelay: { classPropertyName: "subMenuOpenDelay", publicName: "subMenuOpenDelay", isSignal: true, isRequired: false, transformFunction: null }, subMenuCloseDelay: { classPropertyName: "subMenuCloseDelay", publicName: "subMenuCloseDelay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuOpen: "menuOpenChange", menuOpened: "menuOpened", menuClosed: "menuClosed" }, host: { listeners: { "click": "onClick($event)", "keydown.arrowdown": "onArrowDown($event)", "keydown.arrowup": "onArrowUp($event)", "keydown.arrowright": "onArrowRight($event)", "keydown.arrowleft": "onArrowLeft($event)", "keydown.enter": "onEnter($event)", "keydown.space": "onSpace($event)", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" }, properties: { "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "menuOpen()", "attr.aria-controls": "ariaControls()" } }, exportAs: ["comMenuTrigger"], ngImport: i0 });
|
|
423
|
+
}
|
|
424
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuTriggerDirective, decorators: [{
|
|
425
|
+
type: Directive,
|
|
426
|
+
args: [{
|
|
427
|
+
selector: '[comMenuTrigger]',
|
|
428
|
+
exportAs: 'comMenuTrigger',
|
|
429
|
+
host: {
|
|
430
|
+
'[attr.aria-haspopup]': '"menu"',
|
|
431
|
+
'[attr.aria-expanded]': 'menuOpen()',
|
|
432
|
+
'[attr.aria-controls]': 'ariaControls()',
|
|
433
|
+
'(click)': 'onClick($event)',
|
|
434
|
+
'(keydown.arrowdown)': 'onArrowDown($event)',
|
|
435
|
+
'(keydown.arrowup)': 'onArrowUp($event)',
|
|
436
|
+
'(keydown.arrowright)': 'onArrowRight($event)',
|
|
437
|
+
'(keydown.arrowleft)': 'onArrowLeft($event)',
|
|
438
|
+
'(keydown.enter)': 'onEnter($event)',
|
|
439
|
+
'(keydown.space)': 'onSpace($event)',
|
|
440
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
441
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
442
|
+
},
|
|
443
|
+
}]
|
|
444
|
+
}], ctorParameters: () => [], propDecorators: { comMenuTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "comMenuTrigger", required: true }] }], menuPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuPosition", required: false }] }], menuAlignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuAlignment", required: false }] }], menuOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuOffset", required: false }] }], menuDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuDisabled", required: false }] }], menuOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuOpen", required: false }] }, { type: i0.Output, args: ["menuOpenChange"] }], menuPanelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuPanelClass", required: false }] }], menuCloseOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuCloseOnSelect", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], subMenuOpenDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "subMenuOpenDelay", required: false }] }], subMenuCloseDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "subMenuCloseDelay", required: false }] }], menuOpened: [{ type: i0.Output, args: ["menuOpened"] }], menuClosed: [{ type: i0.Output, args: ["menuClosed"] }] } });
|
|
445
|
+
|
|
446
|
+
// ─── Menu Panel Variants ───
|
|
447
|
+
/**
|
|
448
|
+
* Menu panel styling variants.
|
|
449
|
+
*
|
|
450
|
+
* @tokens `--color-popover`, `--color-popover-foreground`, `--color-border`, `--shadow-lg`, `--radius-popover`
|
|
451
|
+
*/
|
|
452
|
+
const menuPanelVariants = cva([
|
|
453
|
+
'bg-popover text-popover-foreground',
|
|
454
|
+
'border border-border',
|
|
455
|
+
'rounded-popover shadow-lg',
|
|
456
|
+
'overflow-hidden',
|
|
457
|
+
'py-1',
|
|
458
|
+
], {
|
|
459
|
+
variants: {
|
|
460
|
+
size: {
|
|
461
|
+
sm: 'min-w-32',
|
|
462
|
+
md: 'min-w-48',
|
|
463
|
+
lg: 'min-w-64',
|
|
464
|
+
},
|
|
465
|
+
variant: {
|
|
466
|
+
default: '',
|
|
467
|
+
compact: 'py-0.5',
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
defaultVariants: {
|
|
471
|
+
size: 'md',
|
|
472
|
+
variant: 'default',
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
/**
|
|
476
|
+
* Menu item styling variants (base for all item types).
|
|
477
|
+
*
|
|
478
|
+
* @tokens `--color-popover-foreground`, `--color-foreground`, `--color-muted`, `--color-warn`, `--color-warn-subtle`
|
|
479
|
+
*/
|
|
480
|
+
const menuItemVariants = cva([
|
|
481
|
+
'relative flex items-center w-full',
|
|
482
|
+
'text-sm text-popover-foreground',
|
|
483
|
+
'outline-none select-none',
|
|
484
|
+
'transition-colors duration-75',
|
|
485
|
+
], {
|
|
486
|
+
variants: {
|
|
487
|
+
size: {
|
|
488
|
+
sm: 'px-2 py-1 gap-2',
|
|
489
|
+
md: 'px-3 py-1.5 gap-2.5',
|
|
490
|
+
lg: 'px-4 py-2 gap-3',
|
|
491
|
+
},
|
|
492
|
+
focused: {
|
|
493
|
+
true: 'bg-muted text-foreground',
|
|
494
|
+
false: '',
|
|
495
|
+
},
|
|
496
|
+
disabled: {
|
|
497
|
+
true: 'bg-disabled text-disabled-foreground pointer-events-none',
|
|
498
|
+
false: 'cursor-pointer',
|
|
499
|
+
},
|
|
500
|
+
destructive: {
|
|
501
|
+
true: '',
|
|
502
|
+
false: '',
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
compoundVariants: [
|
|
506
|
+
{ destructive: true, focused: true, class: 'bg-warn-subtle text-warn' },
|
|
507
|
+
{ destructive: true, focused: false, class: 'text-warn' },
|
|
508
|
+
],
|
|
509
|
+
defaultVariants: {
|
|
510
|
+
size: 'md',
|
|
511
|
+
focused: false,
|
|
512
|
+
disabled: false,
|
|
513
|
+
destructive: false,
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
// ─── Menu Label Variants ───
|
|
517
|
+
/**
|
|
518
|
+
* Menu label (section header) styling variants.
|
|
519
|
+
*
|
|
520
|
+
* @tokens `--color-muted-foreground`
|
|
521
|
+
*/
|
|
522
|
+
const menuLabelVariants = cva('text-muted-foreground font-medium select-none', {
|
|
523
|
+
variants: {
|
|
524
|
+
size: {
|
|
525
|
+
sm: 'px-2 py-1 text-xs',
|
|
526
|
+
md: 'px-3 py-1.5 text-xs',
|
|
527
|
+
lg: 'px-4 py-2 text-sm',
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
defaultVariants: { size: 'md' },
|
|
531
|
+
});
|
|
532
|
+
// ─── Check/Radio Indicator Variants ───
|
|
533
|
+
/**
|
|
534
|
+
* Check/radio indicator sizing variants.
|
|
535
|
+
*/
|
|
536
|
+
const menuCheckIndicatorVariants = cva('inline-flex items-center justify-center shrink-0', {
|
|
537
|
+
variants: {
|
|
538
|
+
size: {
|
|
539
|
+
sm: 'w-3.5 h-3.5',
|
|
540
|
+
md: 'w-4 h-4',
|
|
541
|
+
lg: 'w-5 h-5',
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
defaultVariants: { size: 'md' },
|
|
545
|
+
});
|
|
546
|
+
// ─── Keyboard Shortcut Hint Variants ───
|
|
547
|
+
/**
|
|
548
|
+
* Keyboard shortcut hint styling variants.
|
|
549
|
+
*
|
|
550
|
+
* @tokens `--color-muted-foreground`
|
|
551
|
+
*/
|
|
552
|
+
const menuShortcutVariants = cva('ml-auto text-muted-foreground tracking-widest', {
|
|
553
|
+
variants: {
|
|
554
|
+
size: {
|
|
555
|
+
sm: 'text-[10px]',
|
|
556
|
+
md: 'text-xs',
|
|
557
|
+
lg: 'text-xs',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
defaultVariants: { size: 'md' },
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Base class for focusable menu items.
|
|
565
|
+
* All item directives must implement this interface for FocusKeyManager.
|
|
566
|
+
*/
|
|
567
|
+
class MenuItemBase {
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Menu panel component that renders inside an overlay.
|
|
571
|
+
* Manages keyboard navigation across its items using CDK FocusKeyManager.
|
|
572
|
+
*
|
|
573
|
+
* @tokens `--color-popover`, `--color-popover-foreground`, `--color-border`, `--shadow-lg`
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* ```html
|
|
577
|
+
* <com-menu>
|
|
578
|
+
* <button comMenuItem>Edit</button>
|
|
579
|
+
* <button comMenuItem>Delete</button>
|
|
580
|
+
* </com-menu>
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
class MenuComponent {
|
|
584
|
+
rootTrigger = inject(ROOT_MENU_TRIGGER, { optional: true });
|
|
585
|
+
keyManager = null;
|
|
586
|
+
// ─── Inputs ───
|
|
587
|
+
/** Size variant for the menu panel. */
|
|
588
|
+
menuSize = input('md', ...(ngDevMode ? [{ debugName: "menuSize" }] : []));
|
|
589
|
+
/** Spacing density variant. */
|
|
590
|
+
menuVariant = input('default', ...(ngDevMode ? [{ debugName: "menuVariant" }] : []));
|
|
591
|
+
/** Accessible label for the menu. */
|
|
592
|
+
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
593
|
+
/** ID of element labeling this menu. */
|
|
594
|
+
ariaLabelledBy = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledBy" }] : []));
|
|
595
|
+
// ─── Internal State ───
|
|
596
|
+
menuId = generateMenuId();
|
|
597
|
+
animationState = signal('open', ...(ngDevMode ? [{ debugName: "animationState" }] : []));
|
|
598
|
+
/** Query all focusable items in the menu. */
|
|
599
|
+
items = contentChildren(MenuItemBase, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
600
|
+
// ─── Computed ───
|
|
601
|
+
panelClasses = computed(() => mergeClasses(menuPanelVariants({ size: this.menuSize(), variant: this.menuVariant() })), ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
|
|
602
|
+
constructor() {
|
|
603
|
+
this.rootTrigger?.registerMenu?.(this);
|
|
604
|
+
effect(() => {
|
|
605
|
+
const items = this.items();
|
|
606
|
+
if (items.length > 0 && !this.keyManager) {
|
|
607
|
+
this.keyManager = new FocusKeyManager(items)
|
|
608
|
+
.withVerticalOrientation()
|
|
609
|
+
.withWrap()
|
|
610
|
+
.withHomeAndEnd()
|
|
611
|
+
.withTypeAhead(200);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// ─── Public API ───
|
|
616
|
+
/** Focus the first non-disabled item. */
|
|
617
|
+
focusFirstItem() {
|
|
618
|
+
this.keyManager?.setFirstItemActive();
|
|
619
|
+
}
|
|
620
|
+
/** Focus the last non-disabled item. */
|
|
621
|
+
focusLastItem() {
|
|
622
|
+
this.keyManager?.setLastItemActive();
|
|
623
|
+
}
|
|
624
|
+
/** Close this menu level. */
|
|
625
|
+
close() {
|
|
626
|
+
this.rootTrigger?.close();
|
|
627
|
+
}
|
|
628
|
+
// ─── Event Handlers ───
|
|
629
|
+
onKeydown(event) {
|
|
630
|
+
if (event.key === 'Escape') {
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
event.stopPropagation();
|
|
633
|
+
this.close();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
// ArrowRight/ArrowLeft are handled by submenu triggers
|
|
637
|
+
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') {
|
|
638
|
+
this.keyManager?.onKeydown(event);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
642
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.0", type: MenuComponent, isStandalone: true, selector: "com-menu", inputs: { menuSize: { classPropertyName: "menuSize", publicName: "menuSize", isSignal: true, isRequired: false, transformFunction: null }, menuVariant: { classPropertyName: "menuVariant", publicName: "menuVariant", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "ariaLabelledBy", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: MENU_REF, useExisting: forwardRef(() => MenuComponent) }], queries: [{ propertyName: "items", predicate: MenuItemBase, isSignal: true }], ngImport: i0, template: `
|
|
643
|
+
<div
|
|
644
|
+
[class]="panelClasses()"
|
|
645
|
+
role="menu"
|
|
646
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
647
|
+
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
648
|
+
[id]="menuId"
|
|
649
|
+
[attr.data-state]="animationState()"
|
|
650
|
+
(keydown)="onKeydown($event)"
|
|
651
|
+
>
|
|
652
|
+
<ng-content />
|
|
653
|
+
</div>
|
|
654
|
+
`, isInline: true, styles: [":host{display:contents}[data-state=open]{animation:menu-in .15s ease-out}[data-state=closed]{animation:menu-out .1s ease-in forwards}@keyframes menu-in{0%{opacity:0;transform:scale(.96) translateY(4px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes menu-out{0%{opacity:1}to{opacity:0}}@media(prefers-reduced-motion:reduce){[data-state=open],[data-state=closed]{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
655
|
+
}
|
|
656
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuComponent, decorators: [{
|
|
657
|
+
type: Component,
|
|
658
|
+
args: [{ selector: 'com-menu', template: `
|
|
659
|
+
<div
|
|
660
|
+
[class]="panelClasses()"
|
|
661
|
+
role="menu"
|
|
662
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
663
|
+
[attr.aria-labelledby]="ariaLabelledBy() || null"
|
|
664
|
+
[id]="menuId"
|
|
665
|
+
[attr.data-state]="animationState()"
|
|
666
|
+
(keydown)="onKeydown($event)"
|
|
667
|
+
>
|
|
668
|
+
<ng-content />
|
|
669
|
+
</div>
|
|
670
|
+
`, providers: [{ provide: MENU_REF, useExisting: forwardRef(() => MenuComponent) }], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:contents}[data-state=open]{animation:menu-in .15s ease-out}[data-state=closed]{animation:menu-out .1s ease-in forwards}@keyframes menu-in{0%{opacity:0;transform:scale(.96) translateY(4px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes menu-out{0%{opacity:1}to{opacity:0}}@media(prefers-reduced-motion:reduce){[data-state=open],[data-state=closed]{animation:none}}\n"] }]
|
|
671
|
+
}], ctorParameters: () => [], propDecorators: { menuSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuSize", required: false }] }], menuVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuVariant", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledBy", required: false }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => MenuItemBase), { isSignal: true }] }] } });
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Menu item directive for actionable menu items.
|
|
675
|
+
* Applied to buttons, anchors, or any element that should be selectable.
|
|
676
|
+
*
|
|
677
|
+
* @tokens `--color-popover-foreground`, `--color-foreground`, `--color-muted`, `--color-warn`, `--color-warn-subtle`
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```html
|
|
681
|
+
* <button comMenuItem (menuItemSelect)="onEdit()">Edit</button>
|
|
682
|
+
* <button comMenuItem [menuItemDisabled]="true">Disabled</button>
|
|
683
|
+
* <button comMenuItem destructive>Delete</button>
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
class MenuItemDirective extends MenuItemBase {
|
|
687
|
+
elementRef = inject(ElementRef);
|
|
688
|
+
menu = inject(MENU_REF, { optional: true });
|
|
689
|
+
rootTrigger = inject(ROOT_MENU_TRIGGER, { optional: true });
|
|
690
|
+
// ─── Inputs ───
|
|
691
|
+
/** Disables the item. */
|
|
692
|
+
menuItemDisabled = input(false, { ...(ngDevMode ? { debugName: "menuItemDisabled" } : {}), transform: booleanAttribute });
|
|
693
|
+
/** Marks item as destructive (delete, remove actions). */
|
|
694
|
+
destructive = input(false, { ...(ngDevMode ? { debugName: "destructive" } : {}), transform: booleanAttribute });
|
|
695
|
+
// ─── Outputs ───
|
|
696
|
+
/** Emitted when item is activated. */
|
|
697
|
+
menuItemSelect = output();
|
|
698
|
+
// ─── Internal State ───
|
|
699
|
+
isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : []));
|
|
700
|
+
// ─── Computed ───
|
|
701
|
+
size = computed(() => this.menu?.menuSize() ?? 'md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
702
|
+
itemClasses = computed(() => mergeClasses(menuItemVariants({
|
|
703
|
+
size: this.size(),
|
|
704
|
+
focused: this.isFocused(),
|
|
705
|
+
disabled: this.menuItemDisabled(),
|
|
706
|
+
destructive: this.destructive(),
|
|
707
|
+
})), ...(ngDevMode ? [{ debugName: "itemClasses" }] : []));
|
|
708
|
+
// ─── FocusableOption Implementation ───
|
|
709
|
+
get disabled() {
|
|
710
|
+
return this.menuItemDisabled();
|
|
711
|
+
}
|
|
712
|
+
focus() {
|
|
713
|
+
this.elementRef.nativeElement.focus();
|
|
714
|
+
this.isFocused.set(true);
|
|
715
|
+
}
|
|
716
|
+
getLabel() {
|
|
717
|
+
return this.elementRef.nativeElement.textContent?.trim() ?? '';
|
|
718
|
+
}
|
|
719
|
+
// ─── Event Handlers ───
|
|
720
|
+
onAction(event) {
|
|
721
|
+
if (this.menuItemDisabled())
|
|
722
|
+
return;
|
|
723
|
+
event.preventDefault();
|
|
724
|
+
this.menuItemSelect.emit();
|
|
725
|
+
if (this.rootTrigger?.menuCloseOnSelect()) {
|
|
726
|
+
this.rootTrigger.close();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
onMouseEnter() {
|
|
730
|
+
if (this.menuItemDisabled())
|
|
731
|
+
return;
|
|
732
|
+
this.focus();
|
|
733
|
+
}
|
|
734
|
+
onMouseLeave() {
|
|
735
|
+
this.isFocused.set(false);
|
|
736
|
+
}
|
|
737
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuItemDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
738
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: MenuItemDirective, isStandalone: true, selector: "[comMenuItem]", inputs: { menuItemDisabled: { classPropertyName: "menuItemDisabled", publicName: "menuItemDisabled", isSignal: true, isRequired: false, transformFunction: null }, destructive: { classPropertyName: "destructive", publicName: "destructive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuItemSelect: "menuItemSelect" }, host: { listeners: { "click": "onAction($event)", "keydown.enter": "onAction($event)", "keydown.space": "onAction($event)", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" }, properties: { "class": "itemClasses()", "attr.role": "\"menuitem\"", "attr.tabindex": "-1", "attr.aria-disabled": "menuItemDisabled() || null", "attr.disabled": "menuItemDisabled() || null" } }, providers: [{ provide: MenuItemBase, useExisting: forwardRef(() => MenuItemDirective) }], exportAs: ["comMenuItem"], usesInheritance: true, ngImport: i0 });
|
|
739
|
+
}
|
|
740
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuItemDirective, decorators: [{
|
|
741
|
+
type: Directive,
|
|
742
|
+
args: [{
|
|
743
|
+
selector: '[comMenuItem]',
|
|
744
|
+
exportAs: 'comMenuItem',
|
|
745
|
+
providers: [{ provide: MenuItemBase, useExisting: forwardRef(() => MenuItemDirective) }],
|
|
746
|
+
host: {
|
|
747
|
+
'[class]': 'itemClasses()',
|
|
748
|
+
'[attr.role]': '"menuitem"',
|
|
749
|
+
'[attr.tabindex]': '-1',
|
|
750
|
+
'[attr.aria-disabled]': 'menuItemDisabled() || null',
|
|
751
|
+
'[attr.disabled]': 'menuItemDisabled() || null',
|
|
752
|
+
'(click)': 'onAction($event)',
|
|
753
|
+
'(keydown.enter)': 'onAction($event)',
|
|
754
|
+
'(keydown.space)': 'onAction($event)',
|
|
755
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
756
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
757
|
+
},
|
|
758
|
+
}]
|
|
759
|
+
}], propDecorators: { menuItemDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuItemDisabled", required: false }] }], destructive: [{ type: i0.Input, args: [{ isSignal: true, alias: "destructive", required: false }] }], menuItemSelect: [{ type: i0.Output, args: ["menuItemSelect"] }] } });
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Checkbox menu item component with toggleable checked state.
|
|
763
|
+
*
|
|
764
|
+
* @tokens `--color-popover-foreground`, `--color-foreground`, `--color-muted`
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```html
|
|
768
|
+
* <button comMenuItemCheckbox [(checked)]="showSidebar">Sidebar</button>
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
771
|
+
class MenuItemCheckboxComponent extends MenuItemBase {
|
|
772
|
+
elementRef = inject(ElementRef);
|
|
773
|
+
menu = inject(MENU_REF, { optional: true });
|
|
774
|
+
// ─── Inputs ───
|
|
775
|
+
/** Disables the item. */
|
|
776
|
+
menuItemDisabled = input(false, { ...(ngDevMode ? { debugName: "menuItemDisabled" } : {}), transform: booleanAttribute });
|
|
777
|
+
/** Two-way bindable checked state. */
|
|
778
|
+
checked = model(false, ...(ngDevMode ? [{ debugName: "checked" }] : []));
|
|
779
|
+
// ─── Internal State ───
|
|
780
|
+
isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : []));
|
|
781
|
+
// ─── Computed ───
|
|
782
|
+
size = computed(() => this.menu?.menuSize() ?? 'md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
783
|
+
itemClasses = computed(() => mergeClasses(menuItemVariants({
|
|
784
|
+
size: this.size(),
|
|
785
|
+
focused: this.isFocused(),
|
|
786
|
+
disabled: this.menuItemDisabled(),
|
|
787
|
+
})), ...(ngDevMode ? [{ debugName: "itemClasses" }] : []));
|
|
788
|
+
indicatorClasses = computed(() => mergeClasses(menuCheckIndicatorVariants({ size: this.size() })), ...(ngDevMode ? [{ debugName: "indicatorClasses" }] : []));
|
|
789
|
+
// ─── FocusableOption Implementation ───
|
|
790
|
+
get disabled() {
|
|
791
|
+
return this.menuItemDisabled();
|
|
792
|
+
}
|
|
793
|
+
focus() {
|
|
794
|
+
this.elementRef.nativeElement.focus();
|
|
795
|
+
this.isFocused.set(true);
|
|
796
|
+
}
|
|
797
|
+
getLabel() {
|
|
798
|
+
return this.elementRef.nativeElement.textContent?.trim() ?? '';
|
|
799
|
+
}
|
|
800
|
+
// ─── Event Handlers ───
|
|
801
|
+
toggle(event) {
|
|
802
|
+
if (this.menuItemDisabled())
|
|
803
|
+
return;
|
|
804
|
+
event.preventDefault();
|
|
805
|
+
this.checked.update((c) => !c);
|
|
806
|
+
}
|
|
807
|
+
onMouseEnter() {
|
|
808
|
+
if (this.menuItemDisabled())
|
|
809
|
+
return;
|
|
810
|
+
this.focus();
|
|
811
|
+
}
|
|
812
|
+
onMouseLeave() {
|
|
813
|
+
this.isFocused.set(false);
|
|
814
|
+
}
|
|
815
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuItemCheckboxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
816
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MenuItemCheckboxComponent, isStandalone: true, selector: "[comMenuItemCheckbox]", inputs: { menuItemDisabled: { classPropertyName: "menuItemDisabled", publicName: "menuItemDisabled", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange" }, host: { listeners: { "click": "toggle($event)", "keydown.enter": "toggle($event)", "keydown.space": "toggle($event)", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" }, properties: { "class": "itemClasses()", "attr.role": "\"menuitemcheckbox\"", "attr.tabindex": "-1", "attr.aria-checked": "checked()", "attr.aria-disabled": "menuItemDisabled() || null", "attr.disabled": "menuItemDisabled() || null" } }, providers: [{ provide: MenuItemBase, useExisting: forwardRef(() => MenuItemCheckboxComponent) }], exportAs: ["comMenuItemCheckbox"], usesInheritance: true, ngImport: i0, template: `
|
|
817
|
+
<span [class]="indicatorClasses()">
|
|
818
|
+
@if (checked()) {
|
|
819
|
+
<svg
|
|
820
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
821
|
+
viewBox="0 0 24 24"
|
|
822
|
+
fill="none"
|
|
823
|
+
stroke="currentColor"
|
|
824
|
+
stroke-width="2"
|
|
825
|
+
stroke-linecap="round"
|
|
826
|
+
stroke-linejoin="round"
|
|
827
|
+
class="w-full h-full"
|
|
828
|
+
aria-hidden="true"
|
|
829
|
+
>
|
|
830
|
+
<polyline points="20 6 9 17 4 12" />
|
|
831
|
+
</svg>
|
|
832
|
+
}
|
|
833
|
+
</span>
|
|
834
|
+
<ng-content />
|
|
835
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
836
|
+
}
|
|
837
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuItemCheckboxComponent, decorators: [{
|
|
838
|
+
type: Component,
|
|
839
|
+
args: [{
|
|
840
|
+
selector: '[comMenuItemCheckbox]',
|
|
841
|
+
exportAs: 'comMenuItemCheckbox',
|
|
842
|
+
template: `
|
|
843
|
+
<span [class]="indicatorClasses()">
|
|
844
|
+
@if (checked()) {
|
|
845
|
+
<svg
|
|
846
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
847
|
+
viewBox="0 0 24 24"
|
|
848
|
+
fill="none"
|
|
849
|
+
stroke="currentColor"
|
|
850
|
+
stroke-width="2"
|
|
851
|
+
stroke-linecap="round"
|
|
852
|
+
stroke-linejoin="round"
|
|
853
|
+
class="w-full h-full"
|
|
854
|
+
aria-hidden="true"
|
|
855
|
+
>
|
|
856
|
+
<polyline points="20 6 9 17 4 12" />
|
|
857
|
+
</svg>
|
|
858
|
+
}
|
|
859
|
+
</span>
|
|
860
|
+
<ng-content />
|
|
861
|
+
`,
|
|
862
|
+
providers: [{ provide: MenuItemBase, useExisting: forwardRef(() => MenuItemCheckboxComponent) }],
|
|
863
|
+
host: {
|
|
864
|
+
'[class]': 'itemClasses()',
|
|
865
|
+
'[attr.role]': '"menuitemcheckbox"',
|
|
866
|
+
'[attr.tabindex]': '-1',
|
|
867
|
+
'[attr.aria-checked]': 'checked()',
|
|
868
|
+
'[attr.aria-disabled]': 'menuItemDisabled() || null',
|
|
869
|
+
'[attr.disabled]': 'menuItemDisabled() || null',
|
|
870
|
+
'(click)': 'toggle($event)',
|
|
871
|
+
'(keydown.enter)': 'toggle($event)',
|
|
872
|
+
'(keydown.space)': 'toggle($event)',
|
|
873
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
874
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
875
|
+
},
|
|
876
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
877
|
+
}]
|
|
878
|
+
}], propDecorators: { menuItemDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuItemDisabled", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }] } });
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Token for menu group to allow radio items to coordinate.
|
|
882
|
+
*/
|
|
883
|
+
class MenuGroupRef {
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Menu group directive for organizing related items.
|
|
887
|
+
* For radio items, ensures mutual exclusivity within the group.
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```html
|
|
891
|
+
* <div comMenuGroup [(groupValue)]="sortField">
|
|
892
|
+
* <button comMenuItemRadio value="name">Name</button>
|
|
893
|
+
* <button comMenuItemRadio value="date">Date</button>
|
|
894
|
+
* </div>
|
|
895
|
+
* ```
|
|
896
|
+
*
|
|
897
|
+
* @tokens None - uses only ARIA attributes
|
|
898
|
+
*/
|
|
899
|
+
class MenuGroupDirective extends MenuGroupRef {
|
|
900
|
+
/** Optional accessible label for the group. */
|
|
901
|
+
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
902
|
+
/** Two-way bindable value for radio groups. */
|
|
903
|
+
groupValue = model(undefined, ...(ngDevMode ? [{ debugName: "groupValue" }] : []));
|
|
904
|
+
setGroupValue(value) {
|
|
905
|
+
this.groupValue.set(value);
|
|
906
|
+
}
|
|
907
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuGroupDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
908
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.0", type: MenuGroupDirective, isStandalone: true, selector: "[comMenuGroup]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, groupValue: { classPropertyName: "groupValue", publicName: "groupValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { groupValue: "groupValueChange" }, host: { properties: { "attr.role": "\"group\"", "attr.aria-label": "ariaLabel() || null" } }, providers: [{ provide: MenuGroupRef, useExisting: forwardRef(() => MenuGroupDirective) }], exportAs: ["comMenuGroup"], usesInheritance: true, ngImport: i0 });
|
|
909
|
+
}
|
|
910
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuGroupDirective, decorators: [{
|
|
911
|
+
type: Directive,
|
|
912
|
+
args: [{
|
|
913
|
+
selector: '[comMenuGroup]',
|
|
914
|
+
exportAs: 'comMenuGroup',
|
|
915
|
+
providers: [{ provide: MenuGroupRef, useExisting: forwardRef(() => MenuGroupDirective) }],
|
|
916
|
+
host: {
|
|
917
|
+
'[attr.role]': '"group"',
|
|
918
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
919
|
+
},
|
|
920
|
+
}]
|
|
921
|
+
}], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], groupValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupValue", required: false }] }, { type: i0.Output, args: ["groupValueChange"] }] } });
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Radio menu item component for single-selection within a group.
|
|
925
|
+
*
|
|
926
|
+
* @tokens `--color-popover-foreground`, `--color-foreground`, `--color-muted`
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* ```html
|
|
930
|
+
* <div comMenuGroup [(groupValue)]="sortField">
|
|
931
|
+
* <button comMenuItemRadio value="name">Name</button>
|
|
932
|
+
* <button comMenuItemRadio value="date">Date</button>
|
|
933
|
+
* </div>
|
|
934
|
+
* ```
|
|
935
|
+
*/
|
|
936
|
+
class MenuItemRadioComponent extends MenuItemBase {
|
|
937
|
+
elementRef = inject(ElementRef);
|
|
938
|
+
menu = inject(MENU_REF, { optional: true });
|
|
939
|
+
rootTrigger = inject(ROOT_MENU_TRIGGER, { optional: true });
|
|
940
|
+
group = inject(MenuGroupRef, { optional: true });
|
|
941
|
+
// ─── Inputs ───
|
|
942
|
+
/** Disables the item. */
|
|
943
|
+
menuItemDisabled = input(false, { ...(ngDevMode ? { debugName: "menuItemDisabled" } : {}), transform: booleanAttribute });
|
|
944
|
+
/** The value this radio item represents. */
|
|
945
|
+
value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
946
|
+
// ─── Internal State ───
|
|
947
|
+
isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : []));
|
|
948
|
+
// ─── Computed ───
|
|
949
|
+
size = computed(() => this.menu?.menuSize() ?? 'md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
950
|
+
/** Whether this radio item is currently selected. */
|
|
951
|
+
isChecked = computed(() => this.group?.groupValue() === this.value(), ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
|
|
952
|
+
itemClasses = computed(() => mergeClasses(menuItemVariants({
|
|
953
|
+
size: this.size(),
|
|
954
|
+
focused: this.isFocused(),
|
|
955
|
+
disabled: this.menuItemDisabled(),
|
|
956
|
+
})), ...(ngDevMode ? [{ debugName: "itemClasses" }] : []));
|
|
957
|
+
indicatorClasses = computed(() => mergeClasses(menuCheckIndicatorVariants({ size: this.size() })), ...(ngDevMode ? [{ debugName: "indicatorClasses" }] : []));
|
|
958
|
+
// ─── FocusableOption Implementation ───
|
|
959
|
+
get disabled() {
|
|
960
|
+
return this.menuItemDisabled();
|
|
961
|
+
}
|
|
962
|
+
focus() {
|
|
963
|
+
this.elementRef.nativeElement.focus();
|
|
964
|
+
this.isFocused.set(true);
|
|
965
|
+
}
|
|
966
|
+
getLabel() {
|
|
967
|
+
return this.elementRef.nativeElement.textContent?.trim() ?? '';
|
|
968
|
+
}
|
|
969
|
+
// ─── Event Handlers ───
|
|
970
|
+
select(event) {
|
|
971
|
+
if (this.menuItemDisabled())
|
|
972
|
+
return;
|
|
973
|
+
event.preventDefault();
|
|
974
|
+
this.group?.setGroupValue(this.value());
|
|
975
|
+
if (this.rootTrigger?.menuCloseOnSelect()) {
|
|
976
|
+
this.rootTrigger.close();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
onMouseEnter() {
|
|
980
|
+
if (this.menuItemDisabled())
|
|
981
|
+
return;
|
|
982
|
+
this.focus();
|
|
983
|
+
}
|
|
984
|
+
onMouseLeave() {
|
|
985
|
+
this.isFocused.set(false);
|
|
986
|
+
}
|
|
987
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuItemRadioComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
988
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: MenuItemRadioComponent, isStandalone: true, selector: "[comMenuItemRadio]", inputs: { menuItemDisabled: { classPropertyName: "menuItemDisabled", publicName: "menuItemDisabled", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "click": "select($event)", "keydown.enter": "select($event)", "keydown.space": "select($event)", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" }, properties: { "class": "itemClasses()", "attr.role": "\"menuitemradio\"", "attr.tabindex": "-1", "attr.aria-checked": "isChecked()", "attr.aria-disabled": "menuItemDisabled() || null", "attr.disabled": "menuItemDisabled() || null" } }, providers: [{ provide: MenuItemBase, useExisting: forwardRef(() => MenuItemRadioComponent) }], exportAs: ["comMenuItemRadio"], usesInheritance: true, ngImport: i0, template: `
|
|
989
|
+
<span [class]="indicatorClasses()">
|
|
990
|
+
@if (isChecked()) {
|
|
991
|
+
<svg
|
|
992
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
993
|
+
viewBox="0 0 24 24"
|
|
994
|
+
fill="currentColor"
|
|
995
|
+
class="w-full h-full"
|
|
996
|
+
aria-hidden="true"
|
|
997
|
+
>
|
|
998
|
+
<circle cx="12" cy="12" r="6" />
|
|
999
|
+
</svg>
|
|
1000
|
+
}
|
|
1001
|
+
</span>
|
|
1002
|
+
<ng-content />
|
|
1003
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1004
|
+
}
|
|
1005
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuItemRadioComponent, decorators: [{
|
|
1006
|
+
type: Component,
|
|
1007
|
+
args: [{
|
|
1008
|
+
selector: '[comMenuItemRadio]',
|
|
1009
|
+
exportAs: 'comMenuItemRadio',
|
|
1010
|
+
template: `
|
|
1011
|
+
<span [class]="indicatorClasses()">
|
|
1012
|
+
@if (isChecked()) {
|
|
1013
|
+
<svg
|
|
1014
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1015
|
+
viewBox="0 0 24 24"
|
|
1016
|
+
fill="currentColor"
|
|
1017
|
+
class="w-full h-full"
|
|
1018
|
+
aria-hidden="true"
|
|
1019
|
+
>
|
|
1020
|
+
<circle cx="12" cy="12" r="6" />
|
|
1021
|
+
</svg>
|
|
1022
|
+
}
|
|
1023
|
+
</span>
|
|
1024
|
+
<ng-content />
|
|
1025
|
+
`,
|
|
1026
|
+
providers: [{ provide: MenuItemBase, useExisting: forwardRef(() => MenuItemRadioComponent) }],
|
|
1027
|
+
host: {
|
|
1028
|
+
'[class]': 'itemClasses()',
|
|
1029
|
+
'[attr.role]': '"menuitemradio"',
|
|
1030
|
+
'[attr.tabindex]': '-1',
|
|
1031
|
+
'[attr.aria-checked]': 'isChecked()',
|
|
1032
|
+
'[attr.aria-disabled]': 'menuItemDisabled() || null',
|
|
1033
|
+
'[attr.disabled]': 'menuItemDisabled() || null',
|
|
1034
|
+
'(click)': 'select($event)',
|
|
1035
|
+
'(keydown.enter)': 'select($event)',
|
|
1036
|
+
'(keydown.space)': 'select($event)',
|
|
1037
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
1038
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
1039
|
+
},
|
|
1040
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1041
|
+
}]
|
|
1042
|
+
}], propDecorators: { menuItemDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuItemDisabled", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }] } });
|
|
1043
|
+
|
|
1044
|
+
let labelIdCounter = 0;
|
|
1045
|
+
/**
|
|
1046
|
+
* Non-interactive section label/header inside a menu.
|
|
1047
|
+
*
|
|
1048
|
+
* @tokens `--color-muted-foreground`
|
|
1049
|
+
*
|
|
1050
|
+
* @example
|
|
1051
|
+
* ```html
|
|
1052
|
+
* <span comMenuLabel>Team</span>
|
|
1053
|
+
* <button comMenuItem>View Members</button>
|
|
1054
|
+
* ```
|
|
1055
|
+
*/
|
|
1056
|
+
class MenuLabelDirective {
|
|
1057
|
+
menu = inject(MENU_REF, { optional: true });
|
|
1058
|
+
/** Unique ID that can be referenced by aria-labelledby. */
|
|
1059
|
+
labelId = `menu-label-${++labelIdCounter}`;
|
|
1060
|
+
size = computed(() => this.menu?.menuSize() ?? 'md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1061
|
+
labelClasses = computed(() => mergeClasses(menuLabelVariants({ size: this.size() })), ...(ngDevMode ? [{ debugName: "labelClasses" }] : []));
|
|
1062
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1063
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: MenuLabelDirective, isStandalone: true, selector: "[comMenuLabel]", host: { properties: { "class": "labelClasses()", "attr.role": "\"presentation\"", "id": "labelId" } }, exportAs: ["comMenuLabel"], ngImport: i0 });
|
|
1064
|
+
}
|
|
1065
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuLabelDirective, decorators: [{
|
|
1066
|
+
type: Directive,
|
|
1067
|
+
args: [{
|
|
1068
|
+
selector: '[comMenuLabel]',
|
|
1069
|
+
exportAs: 'comMenuLabel',
|
|
1070
|
+
host: {
|
|
1071
|
+
'[class]': 'labelClasses()',
|
|
1072
|
+
'[attr.role]': '"presentation"',
|
|
1073
|
+
'[id]': 'labelId',
|
|
1074
|
+
},
|
|
1075
|
+
}]
|
|
1076
|
+
}] });
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Visual separator between menu sections.
|
|
1080
|
+
*
|
|
1081
|
+
* @tokens `--color-border`
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* ```html
|
|
1085
|
+
* <button comMenuItem>Edit</button>
|
|
1086
|
+
* <hr comMenuDivider />
|
|
1087
|
+
* <button comMenuItem>Delete</button>
|
|
1088
|
+
* ```
|
|
1089
|
+
*/
|
|
1090
|
+
class MenuDividerDirective {
|
|
1091
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuDividerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1092
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: MenuDividerDirective, isStandalone: true, selector: "[comMenuDivider]", host: { properties: { "attr.role": "\"separator\"" }, classAttribute: "block border-t border-border my-1" }, exportAs: ["comMenuDivider"], ngImport: i0 });
|
|
1093
|
+
}
|
|
1094
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuDividerDirective, decorators: [{
|
|
1095
|
+
type: Directive,
|
|
1096
|
+
args: [{
|
|
1097
|
+
selector: '[comMenuDivider]',
|
|
1098
|
+
exportAs: 'comMenuDivider',
|
|
1099
|
+
host: {
|
|
1100
|
+
class: 'block border-t border-border my-1',
|
|
1101
|
+
'[attr.role]': '"separator"',
|
|
1102
|
+
},
|
|
1103
|
+
}]
|
|
1104
|
+
}] });
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Submenu indicator component — displays a chevron icon indicating a submenu.
|
|
1108
|
+
* Place inside a menu item that has `[comMenuTrigger]` to indicate it opens a submenu.
|
|
1109
|
+
*
|
|
1110
|
+
* @tokens `--color-muted-foreground`
|
|
1111
|
+
*
|
|
1112
|
+
* @example
|
|
1113
|
+
* ```html
|
|
1114
|
+
* <button comMenuItem [comMenuTrigger]="shareMenu">
|
|
1115
|
+
* Share
|
|
1116
|
+
* <com-menu-sub-indicator />
|
|
1117
|
+
* </button>
|
|
1118
|
+
* ```
|
|
1119
|
+
*/
|
|
1120
|
+
class MenuSubIndicatorComponent {
|
|
1121
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuSubIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1122
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: MenuSubIndicatorComponent, isStandalone: true, selector: "com-menu-sub-indicator", host: { properties: { "attr.aria-hidden": "true" }, classAttribute: "ml-auto inline-flex items-center justify-center w-4 h-4 text-muted-foreground" }, ngImport: i0, template: `
|
|
1123
|
+
<svg
|
|
1124
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1125
|
+
viewBox="0 0 24 24"
|
|
1126
|
+
fill="none"
|
|
1127
|
+
stroke="currentColor"
|
|
1128
|
+
stroke-width="2"
|
|
1129
|
+
stroke-linecap="round"
|
|
1130
|
+
stroke-linejoin="round"
|
|
1131
|
+
>
|
|
1132
|
+
<polyline points="9 18 15 12 9 6" />
|
|
1133
|
+
</svg>
|
|
1134
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1135
|
+
}
|
|
1136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuSubIndicatorComponent, decorators: [{
|
|
1137
|
+
type: Component,
|
|
1138
|
+
args: [{
|
|
1139
|
+
selector: 'com-menu-sub-indicator',
|
|
1140
|
+
template: `
|
|
1141
|
+
<svg
|
|
1142
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1143
|
+
viewBox="0 0 24 24"
|
|
1144
|
+
fill="none"
|
|
1145
|
+
stroke="currentColor"
|
|
1146
|
+
stroke-width="2"
|
|
1147
|
+
stroke-linecap="round"
|
|
1148
|
+
stroke-linejoin="round"
|
|
1149
|
+
>
|
|
1150
|
+
<polyline points="9 18 15 12 9 6" />
|
|
1151
|
+
</svg>
|
|
1152
|
+
`,
|
|
1153
|
+
host: {
|
|
1154
|
+
class: 'ml-auto inline-flex items-center justify-center w-4 h-4 text-muted-foreground',
|
|
1155
|
+
'[attr.aria-hidden]': 'true',
|
|
1156
|
+
},
|
|
1157
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1158
|
+
}]
|
|
1159
|
+
}] });
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Keyboard shortcut hint displayed in a menu item.
|
|
1163
|
+
*
|
|
1164
|
+
* @tokens `--color-muted-foreground`
|
|
1165
|
+
*
|
|
1166
|
+
* @example
|
|
1167
|
+
* ```html
|
|
1168
|
+
* <button comMenuItem>
|
|
1169
|
+
* Save
|
|
1170
|
+
* <span comMenuShortcut>⌘S</span>
|
|
1171
|
+
* </button>
|
|
1172
|
+
* ```
|
|
1173
|
+
*/
|
|
1174
|
+
class MenuShortcutDirective {
|
|
1175
|
+
menu = inject(MENU_REF, { optional: true });
|
|
1176
|
+
size = computed(() => this.menu?.menuSize() ?? 'md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1177
|
+
shortcutClasses = computed(() => mergeClasses(menuShortcutVariants({ size: this.size() })), ...(ngDevMode ? [{ debugName: "shortcutClasses" }] : []));
|
|
1178
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuShortcutDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1179
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: MenuShortcutDirective, isStandalone: true, selector: "[comMenuShortcut]", host: { properties: { "class": "shortcutClasses()" } }, exportAs: ["comMenuShortcut"], ngImport: i0 });
|
|
1180
|
+
}
|
|
1181
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: MenuShortcutDirective, decorators: [{
|
|
1182
|
+
type: Directive,
|
|
1183
|
+
args: [{
|
|
1184
|
+
selector: '[comMenuShortcut]',
|
|
1185
|
+
exportAs: 'comMenuShortcut',
|
|
1186
|
+
host: {
|
|
1187
|
+
'[class]': 'shortcutClasses()',
|
|
1188
|
+
},
|
|
1189
|
+
}]
|
|
1190
|
+
}] });
|
|
1191
|
+
|
|
1192
|
+
// Public API for the menu component
|
|
1193
|
+
// Main components/directives
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Generated bundle index. Do not edit.
|
|
1197
|
+
*/
|
|
1198
|
+
|
|
1199
|
+
export { MENU_REF, MenuComponent, MenuDividerDirective, MenuGroupDirective, MenuGroupRef, MenuItemBase, MenuItemCheckboxComponent, MenuItemDirective, MenuItemRadioComponent, MenuLabelDirective, MenuShortcutDirective, MenuSubIndicatorComponent, MenuTriggerDirective, ROOT_MENU_TRIGGER, menuCheckIndicatorVariants, menuItemVariants, menuLabelVariants, menuPanelVariants, menuShortcutVariants };
|
|
1200
|
+
//# sourceMappingURL=ngx-com-components-menu.mjs.map
|