@radix-ng/primitives 0.32.4 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/core/index.d.ts +1 -0
  2. package/core/src/focus-initial.directive.d.ts +9 -0
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +19 -19
  4. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +22 -22
  5. package/fesm2022/radix-ng-primitives-aspect-ratio.mjs +3 -3
  6. package/fesm2022/radix-ng-primitives-avatar.mjs +16 -16
  7. package/fesm2022/radix-ng-primitives-checkbox.mjs +16 -16
  8. package/fesm2022/radix-ng-primitives-collapsible.mjs +9 -9
  9. package/fesm2022/radix-ng-primitives-config.mjs +3 -3
  10. package/fesm2022/radix-ng-primitives-context-menu.mjs +34 -34
  11. package/fesm2022/radix-ng-primitives-core.mjs +26 -7
  12. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-dialog.mjs +25 -25
  14. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs +34 -34
  15. package/fesm2022/radix-ng-primitives-hover-card.mjs +28 -29
  16. package/fesm2022/radix-ng-primitives-hover-card.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-label.mjs +3 -3
  18. package/fesm2022/radix-ng-primitives-menu.mjs +37 -37
  19. package/fesm2022/radix-ng-primitives-menubar.mjs +31 -31
  20. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +1771 -0
  21. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -0
  22. package/fesm2022/radix-ng-primitives-pagination.mjs +28 -28
  23. package/fesm2022/radix-ng-primitives-popover.mjs +50 -32
  24. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  25. package/fesm2022/radix-ng-primitives-progress.mjs +10 -10
  26. package/fesm2022/radix-ng-primitives-radio.mjs +12 -12
  27. package/fesm2022/radix-ng-primitives-roving-focus.mjs +6 -6
  28. package/fesm2022/radix-ng-primitives-select.mjs +34 -34
  29. package/fesm2022/radix-ng-primitives-separator.mjs +3 -3
  30. package/fesm2022/radix-ng-primitives-slider.mjs +31 -31
  31. package/fesm2022/radix-ng-primitives-stepper.mjs +25 -25
  32. package/fesm2022/radix-ng-primitives-switch.mjs +13 -13
  33. package/fesm2022/radix-ng-primitives-tabs.mjs +16 -16
  34. package/fesm2022/radix-ng-primitives-toggle-group.mjs +9 -9
  35. package/fesm2022/radix-ng-primitives-toggle.mjs +6 -6
  36. package/fesm2022/radix-ng-primitives-toolbar.mjs +22 -22
  37. package/fesm2022/radix-ng-primitives-tooltip.mjs +28 -29
  38. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-visually-hidden.mjs +31 -19
  40. package/fesm2022/radix-ng-primitives-visually-hidden.mjs.map +1 -1
  41. package/hover-card/src/hover-card-content.directive.d.ts +2 -2
  42. package/hover-card/src/hover-card-root.directive.d.ts +4 -4
  43. package/navigation-menu/README.md +3 -0
  44. package/navigation-menu/index.d.ts +28 -0
  45. package/navigation-menu/src/navigation-menu-a11y.component.d.ts +15 -0
  46. package/navigation-menu/src/navigation-menu-content.directive.d.ts +31 -0
  47. package/navigation-menu/src/navigation-menu-indicator.directive.d.ts +29 -0
  48. package/navigation-menu/src/navigation-menu-item.directive.d.ts +44 -0
  49. package/navigation-menu/src/navigation-menu-link.directive.d.ts +17 -0
  50. package/navigation-menu/src/navigation-menu-list.directive.d.ts +38 -0
  51. package/navigation-menu/src/navigation-menu-sub.directive.d.ts +19 -0
  52. package/navigation-menu/src/navigation-menu-trigger.directive.d.ts +33 -0
  53. package/navigation-menu/src/navigation-menu-viewport.directive.d.ts +61 -0
  54. package/navigation-menu/src/navigation-menu.directive.d.ts +72 -0
  55. package/navigation-menu/src/navigation-menu.token.d.ts +36 -0
  56. package/navigation-menu/src/navigation-menu.types.d.ts +13 -0
  57. package/navigation-menu/src/utils.d.ts +44 -0
  58. package/package.json +11 -7
  59. package/popover/src/popover-content-attributes.component.d.ts +7 -1
  60. package/popover/src/popover-content.directive.d.ts +2 -2
  61. package/popover/src/popover-root.directive.d.ts +4 -4
  62. package/tooltip/src/tooltip-content.directive.d.ts +2 -2
  63. package/tooltip/src/tooltip-root.directive.d.ts +4 -4
  64. package/visually-hidden/src/visually-hidden-input-bubble.directive.d.ts +4 -2
  65. package/visually-hidden/src/visually-hidden.directive.d.ts +2 -0
@@ -0,0 +1,1771 @@
1
+ import * as i0 from '@angular/core';
2
+ import { EventEmitter, Output, Input, Component, InjectionToken, inject, ElementRef, input, contentChild, signal, Directive, NgZone, TemplateRef, booleanAttribute, Renderer2, computed, effect, untracked, runInInjectionContext, contentChildren, forwardRef, numberAttribute, output, ViewContainerRef, DestroyRef, NgModule } from '@angular/core';
3
+ import { RdxVisuallyHiddenDirective } from '@radix-ng/primitives/visually-hidden';
4
+ import { injectDocument, ESCAPE, ENTER, SPACE, TAB, injectWindow, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP } from '@radix-ng/primitives/core';
5
+ import * as i1 from '@radix-ng/primitives/roving-focus';
6
+ import { RdxRovingFocusItemDirective, RdxRovingFocusGroupDirective } from '@radix-ng/primitives/roving-focus';
7
+ import { FocusKeyManager } from '@angular/cdk/a11y';
8
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { Subject, map, debounce, timer, tap } from 'rxjs';
10
+ import { usePresence } from '@radix-ng/primitives/presence';
11
+
12
+ class RdxNavigationMenuFocusProxyComponent {
13
+ constructor() {
14
+ this.triggerElement = null;
15
+ this.contentElement = null;
16
+ this.proxyFocus = new EventEmitter();
17
+ }
18
+ onFocus(event) {
19
+ const prevFocusedElement = event.relatedTarget;
20
+ const wasTriggerFocused = prevFocusedElement === this.triggerElement;
21
+ const wasFocusFromContent = this.contentElement ? this.contentElement.contains(prevFocusedElement) : false;
22
+ if (wasTriggerFocused || !wasFocusFromContent) {
23
+ this.proxyFocus.emit(wasTriggerFocused ? 'start' : 'end');
24
+ }
25
+ }
26
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuFocusProxyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
27
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.4", type: RdxNavigationMenuFocusProxyComponent, isStandalone: true, selector: "rdx-navigation-menu-focus-proxy", inputs: { triggerElement: "triggerElement", contentElement: "contentElement" }, outputs: { proxyFocus: "proxyFocus" }, ngImport: i0, template: `
28
+ <span
29
+ [attr.tabindex]="0"
30
+ [attr.aria-hidden]="true"
31
+ (focus)="onFocus($event)"
32
+ rdxVisuallyHidden
33
+ feature="focusable"
34
+ ></span>
35
+ `, isInline: true, dependencies: [{ kind: "directive", type: RdxVisuallyHiddenDirective, selector: "[rdxVisuallyHidden]", inputs: ["feature"] }] }); }
36
+ }
37
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuFocusProxyComponent, decorators: [{
38
+ type: Component,
39
+ args: [{
40
+ selector: 'rdx-navigation-menu-focus-proxy',
41
+ template: `
42
+ <span
43
+ [attr.tabindex]="0"
44
+ [attr.aria-hidden]="true"
45
+ (focus)="onFocus($event)"
46
+ rdxVisuallyHidden
47
+ feature="focusable"
48
+ ></span>
49
+ `,
50
+ imports: [RdxVisuallyHiddenDirective]
51
+ }]
52
+ }], propDecorators: { triggerElement: [{
53
+ type: Input
54
+ }], contentElement: [{
55
+ type: Input
56
+ }], proxyFocus: [{
57
+ type: Output
58
+ }] } });
59
+ class RdxNavigationMenuAriaOwnsComponent {
60
+ constructor() {
61
+ this.contentId = '';
62
+ }
63
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuAriaOwnsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
64
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.4", type: RdxNavigationMenuAriaOwnsComponent, isStandalone: true, selector: "rdx-navigation-menu-aria-owns", inputs: { contentId: "contentId" }, ngImport: i0, template: `
65
+ <span [attr.aria-owns]="contentId" rdxVisuallyHidden feature="fully-hidden"></span>
66
+ `, isInline: true, dependencies: [{ kind: "directive", type: RdxVisuallyHiddenDirective, selector: "[rdxVisuallyHidden]", inputs: ["feature"] }] }); }
67
+ }
68
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuAriaOwnsComponent, decorators: [{
69
+ type: Component,
70
+ args: [{
71
+ selector: 'rdx-navigation-menu-aria-owns',
72
+ template: `
73
+ <span [attr.aria-owns]="contentId" rdxVisuallyHidden feature="fully-hidden"></span>
74
+ `,
75
+ imports: [RdxVisuallyHiddenDirective]
76
+ }]
77
+ }], propDecorators: { contentId: [{
78
+ type: Input
79
+ }] } });
80
+
81
+ const RDX_NAVIGATION_MENU_TOKEN = new InjectionToken('RdxNavigationMenuToken');
82
+ function injectNavigationMenu() {
83
+ return inject(RDX_NAVIGATION_MENU_TOKEN);
84
+ }
85
+ function isRootNavigationMenu(context) {
86
+ return context.isRootMenu;
87
+ }
88
+ function provideNavigationMenuContext(provider) {
89
+ return {
90
+ provide: RDX_NAVIGATION_MENU_TOKEN,
91
+ useExisting: provider
92
+ };
93
+ }
94
+
95
+ var RdxNavigationMenuAnimationStatus;
96
+ (function (RdxNavigationMenuAnimationStatus) {
97
+ RdxNavigationMenuAnimationStatus["OPEN_STARTED"] = "open_started";
98
+ RdxNavigationMenuAnimationStatus["OPEN_ENDED"] = "open_ended";
99
+ RdxNavigationMenuAnimationStatus["CLOSED_STARTED"] = "closed_started";
100
+ RdxNavigationMenuAnimationStatus["CLOSED_ENDED"] = "closed_ended";
101
+ })(RdxNavigationMenuAnimationStatus || (RdxNavigationMenuAnimationStatus = {}));
102
+ /**
103
+ * A stub class solely used to query a single type of focusable element in the navigation menu.
104
+ */
105
+ class RdxNavigationMenuFocusableOption {
106
+ focus() {
107
+ throw new Error('Method not implemented.');
108
+ }
109
+ }
110
+
111
+ const ROOT_CONTENT_DISMISS$1 = 'navigationMenu.rootContentDismiss';
112
+ /**
113
+ * Generate a unique ID
114
+ */
115
+ function generateId() {
116
+ return Math.random().toString(36).substring(2, 11);
117
+ }
118
+ /**
119
+ * Get the open state for data-state attribute
120
+ */
121
+ function getOpenStateLabel(open) {
122
+ return open ? 'open' : 'closed';
123
+ }
124
+ /**
125
+ * Create a trigger ID from base ID and value
126
+ */
127
+ function makeTriggerId(baseId, value) {
128
+ return `${baseId}-trigger-${value}`;
129
+ }
130
+ /**
131
+ * Create a content ID from base ID and value
132
+ */
133
+ function makeContentId(baseId, value) {
134
+ return `${baseId}-content-${value}`;
135
+ }
136
+ /**
137
+ * Get the motion attribute for animations
138
+ */
139
+ function getMotionAttribute(currentValue, previousValue, itemValue, itemValues, dir) {
140
+ // reverse values in RTL
141
+ const values = dir === 'rtl' ? [...itemValues].reverse() : itemValues;
142
+ const currentIndex = currentValue !== null ? values.indexOf(currentValue) : -1;
143
+ const prevIndex = previousValue !== null ? values.indexOf(previousValue) : -1;
144
+ const isSelected = itemValue === currentValue;
145
+ const wasSelected = itemValue === previousValue && previousValue !== null;
146
+ // Preserve motion attribute for items not directly involved in the transition
147
+ // (This matches React's behaviour, using a ref/signal might be needed
148
+ // in the component using this function to fully replicate React's prevMotionAttributeRef)
149
+ // For now, returning null if not involved, as per the original code's intent here.
150
+ if (!isSelected && !wasSelected) {
151
+ return null;
152
+ }
153
+ // handle transitions between items
154
+ if (currentIndex !== -1 && prevIndex !== -1) {
155
+ // if moving to this item (isSelected)
156
+ if (isSelected) {
157
+ return currentIndex > prevIndex ? 'from-end' : 'from-start';
158
+ }
159
+ // if moving away from this item (wasSelected)
160
+ if (wasSelected) {
161
+ return currentIndex > prevIndex ? 'to-start' : 'to-end';
162
+ }
163
+ }
164
+ // handle initial open (prevIndex is -1, currentIndex is valid)
165
+ if (isSelected && prevIndex === -1) {
166
+ return null;
167
+ }
168
+ // handle closing entirely (currentIndex is -1, prevIndex is valid)
169
+ if (wasSelected && currentIndex === -1) {
170
+ return null;
171
+ }
172
+ // fallback if none of the above conditions met (should ideally not happen with clear states)
173
+ return null;
174
+ }
175
+ /**
176
+ * Focus the first element in a list of candidates
177
+ * @param candidates Array of elements that can receive focus
178
+ * @param preventScroll Whether to prevent scrolling when focusing
179
+ * @param activateKeyboardNav Whether to dispatch a dummy keydown event to activate keyboard navigation handlers
180
+ * @returns Whether focus was successfully moved
181
+ */
182
+ function focusFirst(candidates, preventScroll = false, activateKeyboardNav = true) {
183
+ const prevFocusedElement = document.activeElement;
184
+ // sort candidates by tabindex to ensure proper order
185
+ const sortedCandidates = [...candidates].sort((a, b) => {
186
+ const aIndex = a.tabIndex || 0;
187
+ const bIndex = b.tabIndex || 0;
188
+ return aIndex - bIndex;
189
+ });
190
+ const success = sortedCandidates.some((candidate) => {
191
+ // if focus is already where we want it, do nothing
192
+ if (candidate === prevFocusedElement)
193
+ return true;
194
+ try {
195
+ candidate.focus({ preventScroll });
196
+ return document.activeElement !== prevFocusedElement;
197
+ }
198
+ catch (e) {
199
+ console.error('Error focusing element:', e);
200
+ return false;
201
+ }
202
+ });
203
+ // if focus was moved successfully and we want to activate keyboard navigation,
204
+ // dispatch a dummy keypress to ensure keyboard handlers are activated
205
+ if (success && activateKeyboardNav && document.activeElement !== prevFocusedElement) {
206
+ try {
207
+ // dispatch a no-op keydown event to activate any keyboard handlers
208
+ document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', {
209
+ bubbles: true,
210
+ cancelable: true,
211
+ key: 'Tab',
212
+ code: 'Tab'
213
+ }));
214
+ }
215
+ catch (e) {
216
+ console.error('Error dispatching keyboard event:', e);
217
+ }
218
+ }
219
+ return success;
220
+ }
221
+ /**
222
+ * Get all tabbable candidates in a container
223
+ */
224
+ function getTabbableCandidates(container) {
225
+ if (!container || !container.querySelectorAll)
226
+ return [];
227
+ const TABBABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
228
+ 'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), ' +
229
+ '[contenteditable="true"]:not([tabindex="-1"])';
230
+ // use querySelector for better browser support
231
+ const elements = Array.from(container.querySelectorAll(TABBABLE_SELECTOR));
232
+ // filter out elements that are hidden, have display:none, etc.
233
+ return elements.filter((element) => {
234
+ if (element.tabIndex < 0)
235
+ return false;
236
+ if (element.hasAttribute('disabled'))
237
+ return false;
238
+ if (element.hasAttribute('aria-hidden') && element.getAttribute('aria-hidden') === 'true')
239
+ return false;
240
+ // Check if element or any parent is hidden
241
+ let current = element;
242
+ while (current) {
243
+ const style = window.getComputedStyle(current);
244
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
245
+ return false;
246
+ }
247
+ current = current.parentElement;
248
+ }
249
+ return true;
250
+ });
251
+ }
252
+ /**
253
+ * Remove elements from tab order and return a function to restore them
254
+ */
255
+ function removeFromTabOrder(candidates) {
256
+ const originalValues = new Map();
257
+ candidates.forEach((candidate) => {
258
+ // Store original tabindex
259
+ originalValues.set(candidate, candidate.getAttribute('tabindex'));
260
+ // Set to -1 to remove from tab order
261
+ candidate.setAttribute('tabindex', '-1');
262
+ });
263
+ // Return restore function
264
+ return () => {
265
+ candidates.forEach((candidate) => {
266
+ const originalValue = originalValues.get(candidate);
267
+ if (originalValue == null) {
268
+ candidate.removeAttribute('tabindex');
269
+ }
270
+ else {
271
+ candidate.setAttribute('tabindex', originalValue);
272
+ }
273
+ });
274
+ };
275
+ }
276
+ /**
277
+ * Wrap array around itself at given start index
278
+ */
279
+ function wrapArray(array, startIndex) {
280
+ return array.map((_, index) => array[(startIndex + index) % array.length]);
281
+ }
282
+
283
+ class RdxNavigationMenuItemDirective {
284
+ constructor() {
285
+ this.elementRef = inject(ElementRef);
286
+ this.context = injectNavigationMenu();
287
+ this.value = input('');
288
+ /**
289
+ * @ignore
290
+ */
291
+ this.triggerOrLink = contentChild(RdxNavigationMenuFocusableOption);
292
+ this.triggerRef = signal(null);
293
+ this.contentRef = signal(null);
294
+ this.focusProxyRef = signal(null);
295
+ this.wasEscapeCloseRef = signal(false);
296
+ this._restoreContentTabOrderRef = signal(null);
297
+ }
298
+ get restoreContentTabOrderRef() {
299
+ return this._restoreContentTabOrderRef;
300
+ }
301
+ /**
302
+ * Handle keyboard entry into content from trigger
303
+ */
304
+ onEntryKeyDown() {
305
+ // Check if we're using a viewport in a root menu
306
+ if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
307
+ const viewport = this.context.viewport();
308
+ if (viewport) {
309
+ // find tabbable elements in the viewport
310
+ const candidates = getTabbableCandidates(viewport);
311
+ if (candidates.length) {
312
+ this.ensureTabOrder();
313
+ // focus the first element
314
+ focusFirst(candidates);
315
+ return;
316
+ }
317
+ }
318
+ }
319
+ // fallback to content if no viewport or no tabbable elements in viewport
320
+ if (this.contentRef()) {
321
+ // restore tab order if needed
322
+ const restoreFn = this._restoreContentTabOrderRef();
323
+ if (restoreFn)
324
+ restoreFn();
325
+ // find and focus first tabbable element
326
+ const candidates = getTabbableCandidates(this.contentRef());
327
+ if (candidates.length) {
328
+ focusFirst(candidates);
329
+ }
330
+ }
331
+ }
332
+ focus() {
333
+ this.triggerOrLink()?.focus();
334
+ }
335
+ /**
336
+ * Ensure elements are in the tab order by restoring any previously removed tabindex values
337
+ */
338
+ ensureTabOrder() {
339
+ const restoreFn = this._restoreContentTabOrderRef();
340
+ if (restoreFn) {
341
+ restoreFn();
342
+ this._restoreContentTabOrderRef.set(null);
343
+ }
344
+ }
345
+ /**
346
+ * Handle focus coming from the focus proxy element
347
+ * @param side Which side the focus is coming from (start = from trigger, end = from after content)
348
+ */
349
+ onFocusProxyEnter(side = 'start') {
350
+ // check for viewport first
351
+ if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
352
+ const viewport = this.context.viewport();
353
+ if (viewport) {
354
+ const candidates = getTabbableCandidates(viewport);
355
+ if (candidates.length) {
356
+ this.ensureTabOrder();
357
+ // focus first or last element depending on direction
358
+ focusFirst(side === 'start' ? candidates : [...candidates].reverse());
359
+ return;
360
+ }
361
+ }
362
+ }
363
+ // fallback to content
364
+ if (this.contentRef()) {
365
+ // restore tab order if needed
366
+ const restoreFn = this._restoreContentTabOrderRef();
367
+ if (restoreFn)
368
+ restoreFn();
369
+ // find and focus appropriate element based on direction
370
+ const candidates = getTabbableCandidates(this.contentRef());
371
+ if (candidates.length) {
372
+ // Focus first or last element depending on which direction we're coming from
373
+ focusFirst(side === 'start' ? candidates : [...candidates].reverse());
374
+ }
375
+ }
376
+ }
377
+ /**
378
+ * Handle focus moving outside of the content
379
+ * Remove elements from tab order when not focused
380
+ */
381
+ onContentFocusOutside() {
382
+ // get all tabbable elements from both viewport and content
383
+ let allCandidates = [];
384
+ // check viewport first
385
+ if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
386
+ const viewport = this.context.viewport();
387
+ if (viewport) {
388
+ allCandidates = getTabbableCandidates(viewport);
389
+ }
390
+ }
391
+ // ... also check direct content
392
+ if (this.contentRef()) {
393
+ const contentCandidates = getTabbableCandidates(this.contentRef());
394
+ allCandidates = [...allCandidates, ...contentCandidates];
395
+ }
396
+ // remove from tab order and store restore function
397
+ if (allCandidates.length) {
398
+ this._restoreContentTabOrderRef.set(removeFromTabOrder(allCandidates));
399
+ }
400
+ }
401
+ /**
402
+ * Handle content being closed from root menu
403
+ */
404
+ onRootContentClose() {
405
+ this.onContentFocusOutside();
406
+ }
407
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
408
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "19.2.4", type: RdxNavigationMenuItemDirective, isStandalone: true, selector: "[rdxNavigationMenuItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.value": "value()" } }, queries: [{ propertyName: "triggerOrLink", first: true, predicate: RdxNavigationMenuFocusableOption, descendants: true, isSignal: true }], exportAs: ["rdxNavigationMenuItem"], ngImport: i0 }); }
409
+ }
410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuItemDirective, decorators: [{
411
+ type: Directive,
412
+ args: [{
413
+ selector: '[rdxNavigationMenuItem]',
414
+ host: {
415
+ '[attr.value]': 'value()'
416
+ },
417
+ exportAs: 'rdxNavigationMenuItem'
418
+ }]
419
+ }] });
420
+
421
+ class RdxNavigationMenuContentDirective {
422
+ constructor() {
423
+ this.elementRef = inject(ElementRef);
424
+ this.ngZone = inject(NgZone);
425
+ this.template = inject(TemplateRef);
426
+ this.document = injectDocument();
427
+ this.item = inject(RdxNavigationMenuItemDirective);
428
+ this.context = injectNavigationMenu();
429
+ /**
430
+ * Used to keep the content rendered and available in the DOM, even when closed.
431
+ * Useful for animations or SEO.
432
+ * @default false
433
+ */
434
+ this.forceMount = input(false, { transform: booleanAttribute });
435
+ /** @ignore */
436
+ this.contentId = makeContentId(this.context.baseId, this.item.value());
437
+ /** @ignore */
438
+ this.triggerId = makeTriggerId(this.context.baseId, this.item.value());
439
+ this.escapeHandler = null;
440
+ }
441
+ set rdxNavigationMenuContent(value) {
442
+ // structural directive requires this input even if unused
443
+ }
444
+ /** @ignore */
445
+ ngOnInit() {
446
+ this.item.contentRef.set(this.elementRef.nativeElement);
447
+ // register template with viewport in root menu via context
448
+ if (isRootNavigationMenu(this.context) && this.context.onViewportContentChange) {
449
+ this.context.onViewportContentChange(this.item.value(), {
450
+ ref: this.elementRef,
451
+ templateRef: this.template,
452
+ forceMount: this.forceMount(),
453
+ value: this.item.value(),
454
+ getMotionAttribute: this.getMotionAttribute.bind(this),
455
+ additionalAttrs: {
456
+ id: this.contentId,
457
+ 'aria-labelledby': this.triggerId,
458
+ role: 'menu'
459
+ }
460
+ });
461
+ }
462
+ // add Escape key handler
463
+ this.escapeHandler = (event) => {
464
+ if (event.key === ESCAPE && this.context.value() === this.item.value()) {
465
+ // mark that this close was triggered by Escape
466
+ this.item.wasEscapeCloseRef.set(true);
467
+ // close the content
468
+ if (this.context.onItemDismiss) {
469
+ this.context.onItemDismiss();
470
+ }
471
+ // refocus the trigger
472
+ setTimeout(() => {
473
+ const trigger = this.item.triggerRef();
474
+ if (trigger)
475
+ trigger.focus();
476
+ }, 0);
477
+ event.preventDefault();
478
+ event.stopPropagation();
479
+ }
480
+ };
481
+ this.ngZone.runOutsideAngular(() => {
482
+ if (this.escapeHandler) {
483
+ this.document.addEventListener('keydown', this.escapeHandler);
484
+ }
485
+ });
486
+ }
487
+ /** @ignore */
488
+ ngOnDestroy() {
489
+ // unregister from viewport
490
+ if (isRootNavigationMenu(this.context) && this.context.onViewportContentRemove) {
491
+ this.context.onViewportContentRemove(this.item.value());
492
+ }
493
+ // remove escape key handler
494
+ if (this.escapeHandler) {
495
+ this.document.removeEventListener('keydown', this.escapeHandler);
496
+ this.escapeHandler = null;
497
+ }
498
+ }
499
+ /** @ignore - Compute motion attribute for animations */
500
+ getMotionAttribute() {
501
+ if (!isRootNavigationMenu(this.context))
502
+ return null;
503
+ const itemValues = Array.from(this.context.viewportContent?.() ?? new Map()).map(([value]) => value);
504
+ return getMotionAttribute(this.context.value(), this.context.previousValue(), this.item.value(), itemValues, this.context.dir);
505
+ }
506
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
507
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuContentDirective, isStandalone: true, selector: "[rdxNavigationMenuContent]", inputs: { rdxNavigationMenuContent: { classPropertyName: "rdxNavigationMenuContent", publicName: "rdxNavigationMenuContent", isSignal: false, isRequired: false, transformFunction: booleanAttribute }, forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
508
+ }
509
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuContentDirective, decorators: [{
510
+ type: Directive,
511
+ args: [{
512
+ selector: '[rdxNavigationMenuContent]'
513
+ }]
514
+ }], propDecorators: { rdxNavigationMenuContent: [{
515
+ type: Input,
516
+ args: [{ transform: booleanAttribute }]
517
+ }] } });
518
+
519
+ class RdxNavigationMenuIndicatorDirective {
520
+ constructor() {
521
+ this.context = injectNavigationMenu();
522
+ this.elementRef = inject(ElementRef);
523
+ this.renderer = inject(Renderer2);
524
+ /**
525
+ * Used to keep the indicator rendered and available in the DOM, even when hidden.
526
+ * Useful for animations.
527
+ * @default false
528
+ */
529
+ this.forceMount = input(false, { transform: booleanAttribute });
530
+ /** @ignore */
531
+ this._position = signal(null);
532
+ /** @ignore */
533
+ this._activeTrigger = signal(null);
534
+ /** @ignore */
535
+ this._resizeObserver = new ResizeObserver(() => this.updatePosition());
536
+ this.isVisible = computed(() => Boolean(this.context.value() || this.forceMount()));
537
+ // set up effect for tracking active trigger and position
538
+ effect(() => {
539
+ // this effect runs when the current value changes
540
+ const value = this.context.value();
541
+ untracked(() => {
542
+ if (value && isRootNavigationMenu(this.context)) {
543
+ this.findAndSetActiveTrigger();
544
+ }
545
+ });
546
+ });
547
+ // initialize observers for position tracking
548
+ runInInjectionContext(this.context, () => {
549
+ if (isRootNavigationMenu(this.context) && this.context.indicatorTrack) {
550
+ const track = this.context.indicatorTrack();
551
+ if (track) {
552
+ // observe size changes on the track
553
+ this._resizeObserver.observe(track);
554
+ }
555
+ // initial position update if menu is open
556
+ if (this.context.value()) {
557
+ setTimeout(() => this.findAndSetActiveTrigger(), 0);
558
+ }
559
+ }
560
+ });
561
+ }
562
+ /** @ignore */
563
+ ngOnDestroy() {
564
+ this._resizeObserver.disconnect();
565
+ }
566
+ /** @ignore */
567
+ findAndSetActiveTrigger() {
568
+ if (!isRootNavigationMenu(this.context) || !this.context.indicatorTrack)
569
+ return;
570
+ const track = this.context.indicatorTrack();
571
+ if (!track)
572
+ return;
573
+ // find all triggers within the track
574
+ const triggers = Array.from(track.querySelectorAll('[rdxNavigationMenuTrigger]'));
575
+ // find the active trigger based on the current menu value
576
+ const activeTrigger = triggers.find((trigger) => {
577
+ const item = trigger.closest('[rdxNavigationMenuItem]');
578
+ if (!item)
579
+ return false;
580
+ const value = item.getAttribute('value');
581
+ return value === this.context.value();
582
+ });
583
+ if (activeTrigger && activeTrigger !== this._activeTrigger()) {
584
+ this._activeTrigger.set(activeTrigger);
585
+ this.updatePosition();
586
+ }
587
+ }
588
+ /** @ignore */
589
+ updatePosition() {
590
+ const trigger = this._activeTrigger();
591
+ if (!trigger)
592
+ return;
593
+ const isHorizontal = this.context.orientation === 'horizontal';
594
+ // calculate new position
595
+ const newPosition = {
596
+ size: isHorizontal ? trigger.offsetWidth : trigger.offsetHeight,
597
+ offset: isHorizontal ? trigger.offsetLeft : trigger.offsetTop
598
+ };
599
+ // only update if position has changed
600
+ if (JSON.stringify(newPosition) !== JSON.stringify(this._position())) {
601
+ this._position.set(newPosition);
602
+ // apply position styles
603
+ const styles = isHorizontal
604
+ ? {
605
+ position: 'absolute',
606
+ left: '0',
607
+ width: `${newPosition.size}px`,
608
+ transform: `translateX(${newPosition.offset}px)`
609
+ }
610
+ : {
611
+ position: 'absolute',
612
+ top: '0',
613
+ height: `${newPosition.size}px`,
614
+ transform: `translateY(${newPosition.offset}px)`
615
+ };
616
+ Object.entries(styles).forEach(([key, value]) => {
617
+ this.renderer.setStyle(this.elementRef.nativeElement, key, value);
618
+ });
619
+ }
620
+ }
621
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuIndicatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
622
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuIndicatorDirective, isStandalone: true, selector: "[rdxNavigationMenuIndicator]", inputs: { forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, properties: { "attr.data-state": "isVisible() ? \"visible\" : \"hidden\"", "attr.data-orientation": "context.orientation", "style.display": "isVisible() ? null : \"none\"" } }, ngImport: i0 }); }
623
+ }
624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuIndicatorDirective, decorators: [{
625
+ type: Directive,
626
+ args: [{
627
+ selector: '[rdxNavigationMenuIndicator]',
628
+ host: {
629
+ '[attr.data-state]': 'isVisible() ? "visible" : "hidden"',
630
+ '[attr.data-orientation]': 'context.orientation',
631
+ '[style.display]': 'isVisible() ? null : "none"',
632
+ 'aria-hidden': 'true'
633
+ }
634
+ }]
635
+ }], ctorParameters: () => [] });
636
+
637
+ const LINK_SELECT = 'navigationMenu.linkSelect';
638
+ const ROOT_CONTENT_DISMISS = 'navigationMenu.rootContentDismiss';
639
+ class RdxNavigationMenuLinkDirective extends RdxNavigationMenuFocusableOption {
640
+ constructor() {
641
+ super(...arguments);
642
+ this.rovingFocusItem = inject(RdxRovingFocusItemDirective, { self: true });
643
+ this.uniqueId = generateId();
644
+ this.active = input(false, { transform: booleanAttribute });
645
+ this.onSelect = input();
646
+ this.elementRef = inject(ElementRef);
647
+ }
648
+ ngOnInit() {
649
+ this.rovingFocusItem.tabStopId = this.elementRef.nativeElement.id || `link-${this.uniqueId}`;
650
+ }
651
+ focus() {
652
+ this.elementRef.nativeElement.focus();
653
+ }
654
+ onClick(event) {
655
+ const target = event.target;
656
+ // dispatch link select event
657
+ const linkSelectEvent = new CustomEvent(LINK_SELECT, {
658
+ bubbles: true,
659
+ cancelable: true
660
+ });
661
+ // add one-time listener for onSelect handler
662
+ const onSelect = this.onSelect();
663
+ if (onSelect) {
664
+ target.addEventListener(LINK_SELECT, onSelect, { once: true });
665
+ }
666
+ // dispatch event
667
+ target.dispatchEvent(linkSelectEvent);
668
+ // if not prevented and not meta key, dismiss content
669
+ if (!linkSelectEvent.defaultPrevented && !event.metaKey) {
670
+ const dismissEvent = new CustomEvent(ROOT_CONTENT_DISMISS, {
671
+ bubbles: true,
672
+ cancelable: true
673
+ });
674
+ target.dispatchEvent(dismissEvent);
675
+ }
676
+ }
677
+ onKeydown(event) {
678
+ // activate link on Enter or Space
679
+ if (event.key === ENTER || event.key === SPACE) {
680
+ // prevent default behavior like scrolling (Space) or form submission (Enter) BEFORE simulating the click.
681
+ event.preventDefault();
682
+ // simulate a click event on the link element itself
683
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
684
+ this.elementRef.nativeElement.dispatchEvent(clickEvent);
685
+ return;
686
+ }
687
+ }
688
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuLinkDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
689
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuLinkDirective, isStandalone: true, selector: "[rdxNavigationMenuLink]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, onSelect: { classPropertyName: "onSelect", publicName: "onSelect", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.data-active": "active() ? \"\" : undefined", "attr.aria-current": "active() ? \"page\" : undefined" } }, providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuLinkDirective }], usesInheritance: true, hostDirectives: [{ directive: i1.RdxRovingFocusItemDirective, inputs: ["focusable", "focusable"] }], ngImport: i0 }); }
690
+ }
691
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuLinkDirective, decorators: [{
692
+ type: Directive,
693
+ args: [{
694
+ selector: '[rdxNavigationMenuLink]',
695
+ hostDirectives: [{ directive: RdxRovingFocusItemDirective, inputs: ['focusable'] }],
696
+ host: {
697
+ '[attr.data-active]': 'active() ? "" : undefined',
698
+ '[attr.aria-current]': 'active() ? "page" : undefined',
699
+ '(click)': 'onClick($event)',
700
+ '(keydown)': 'onKeydown($event)'
701
+ },
702
+ providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuLinkDirective }]
703
+ }]
704
+ }] });
705
+
706
+ class RdxNavigationMenuListDirective {
707
+ constructor() {
708
+ this.context = injectNavigationMenu();
709
+ this.elementRef = inject((ElementRef));
710
+ this.renderer = inject(Renderer2);
711
+ this.rovingFocusGroup = inject(RdxRovingFocusGroupDirective, { self: true });
712
+ /**
713
+ * @private
714
+ * @ignore
715
+ */
716
+ this.items = contentChildren(forwardRef(() => RdxNavigationMenuItemDirective), { descendants: true });
717
+ }
718
+ /**
719
+ * @ignore
720
+ */
721
+ ngAfterContentInit() {
722
+ const items = this.items();
723
+ this.keyManager = new FocusKeyManager(items);
724
+ if (this.context.orientation === 'horizontal') {
725
+ this.keyManager.withHorizontalOrientation(this.context.dir || 'ltr');
726
+ }
727
+ else {
728
+ this.keyManager.withVerticalOrientation();
729
+ }
730
+ }
731
+ /**
732
+ * @ignore
733
+ */
734
+ ngAfterViewInit() {
735
+ this.rovingFocusGroup.orientation = this.context.orientation;
736
+ this.rovingFocusGroup.dir = this.context.dir;
737
+ // looping typically only applies to the root menu bar
738
+ if (isRootNavigationMenu(this.context)) {
739
+ this.rovingFocusGroup.loop = this.context.loop ?? false;
740
+ }
741
+ else {
742
+ this.rovingFocusGroup.loop = false;
743
+ }
744
+ if (isRootNavigationMenu(this.context) && this.context.onIndicatorTrackChange) {
745
+ const listElement = this.elementRef.nativeElement;
746
+ const parent = listElement.parentNode;
747
+ // ensure parent exists and list hasn't already been wrapped
748
+ if (parent && !listElement.parentElement?.hasAttribute('data-radix-navigation-menu-list-wrapper')) {
749
+ // create a wrapper div with relative positioning
750
+ const wrapper = this.renderer.createElement('div');
751
+ this.renderer.setAttribute(wrapper, 'data-radix-navigation-menu-list-wrapper', ''); // Add marker
752
+ this.renderer.setStyle(wrapper, 'position', 'relative');
753
+ // insert the wrapper before the list element in the parent
754
+ this.renderer.insertBefore(parent, wrapper, listElement);
755
+ // move the list element inside the new wrapper
756
+ this.renderer.appendChild(wrapper, listElement);
757
+ // register the wrapper element as the track for the indicator positioning
758
+ this.context.onIndicatorTrackChange(wrapper);
759
+ }
760
+ else if (listElement.parentElement?.hasAttribute('data-radix-navigation-menu-list-wrapper')) {
761
+ // if wrapper somehow already exists, ensure context has the correct reference
762
+ this.context.onIndicatorTrackChange(listElement.parentElement);
763
+ }
764
+ }
765
+ }
766
+ /**
767
+ * @ignore
768
+ */
769
+ onKeydown(event) {
770
+ if (!this.keyManager.activeItem) {
771
+ this.keyManager.setFirstItemActive();
772
+ }
773
+ if (event.key === TAB && event.shiftKey) {
774
+ if (this.keyManager.activeItemIndex === 0)
775
+ return;
776
+ this.keyManager.setPreviousItemActive();
777
+ event.preventDefault();
778
+ }
779
+ else if (event.key === TAB) {
780
+ const items = this.items();
781
+ if (this.keyManager.activeItemIndex === items.length - 1) {
782
+ return;
783
+ }
784
+ this.keyManager.setNextItemActive();
785
+ event.preventDefault();
786
+ }
787
+ else {
788
+ this.keyManager.onKeydown(event);
789
+ }
790
+ }
791
+ /**
792
+ * @ignore
793
+ */
794
+ setActiveItem(item) {
795
+ this.keyManager.setActiveItem(item);
796
+ }
797
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
798
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "19.2.4", type: RdxNavigationMenuListDirective, isStandalone: true, selector: "[rdxNavigationMenuList]", host: { attributes: { "role": "menubar" }, listeners: { "keydown": "onKeydown($event)" } }, queries: [{ propertyName: "items", predicate: i0.forwardRef(() => RdxNavigationMenuItemDirective), descendants: true, isSignal: true }], hostDirectives: [{ directive: i1.RdxRovingFocusGroupDirective }], ngImport: i0 }); }
799
+ }
800
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuListDirective, decorators: [{
801
+ type: Directive,
802
+ args: [{
803
+ selector: '[rdxNavigationMenuList]',
804
+ hostDirectives: [RdxRovingFocusGroupDirective],
805
+ host: {
806
+ role: 'menubar',
807
+ '(keydown)': 'onKeydown($event)'
808
+ }
809
+ }]
810
+ }] });
811
+
812
+ // define action types for clearer intent
813
+ var RdxNavigationMenuAction;
814
+ (function (RdxNavigationMenuAction) {
815
+ RdxNavigationMenuAction["OPEN"] = "open";
816
+ RdxNavigationMenuAction["CLOSE"] = "close";
817
+ })(RdxNavigationMenuAction || (RdxNavigationMenuAction = {}));
818
+ class RdxNavigationMenuDirective {
819
+ // State
820
+ #value;
821
+ #previousValue;
822
+ #indicatorTrack;
823
+ #viewport;
824
+ #viewportContent;
825
+ #rootNavigationMenu;
826
+ #userDismissedByClick;
827
+ #isOpenDelayed;
828
+ // pointer tracking
829
+ #isPointerOverContent;
830
+ #isPointerOverTrigger;
831
+ constructor() {
832
+ this.elementRef = inject(ElementRef);
833
+ this.document = injectDocument();
834
+ this.window = injectWindow();
835
+ // State
836
+ this.#value = signal('');
837
+ this.#previousValue = signal('');
838
+ this.baseId = `rdx-nav-menu-${generateId()}`;
839
+ this.#indicatorTrack = signal(null);
840
+ this.#viewport = signal(null);
841
+ this.#viewportContent = signal(new Map());
842
+ this.#rootNavigationMenu = signal(this.elementRef.nativeElement);
843
+ this.#userDismissedByClick = signal(false);
844
+ this.userDismissedByClick = () => this.#userDismissedByClick();
845
+ this.resetUserDismissed = () => this.#userDismissedByClick.set(false);
846
+ // delay timers
847
+ this.openTimerRef = 0;
848
+ this.closeTimerRef = 0;
849
+ this.skipDelayTimerRef = 0;
850
+ this.#isOpenDelayed = signal(true);
851
+ // pointer tracking
852
+ this.#isPointerOverContent = signal(false);
853
+ this.#isPointerOverTrigger = signal(false);
854
+ this.documentMouseLeaveHandler = null;
855
+ this.actionSubject$ = new Subject();
856
+ this.orientation = 'horizontal';
857
+ this.dir = 'ltr';
858
+ this.delayDuration = 200;
859
+ this.skipDelayDuration = 300;
860
+ this.loop = false;
861
+ this.cssAnimation = false;
862
+ this.cssOpeningAnimation = false;
863
+ this.cssClosingAnimation = false;
864
+ this.isRootMenu = true;
865
+ this.cssAnimationStatus = signal(null);
866
+ // exposed state as functions for the token
867
+ this.value = () => this.#value();
868
+ this.previousValue = () => this.#previousValue();
869
+ this.rootNavigationMenu = () => this.#rootNavigationMenu();
870
+ this.indicatorTrack = () => this.#indicatorTrack();
871
+ this.viewport = () => this.#viewport();
872
+ this.viewportContent = () => this.#viewportContent();
873
+ // exposed pointer state
874
+ this.setTriggerPointerState = (isOver) => this.#isPointerOverTrigger.set(isOver);
875
+ this.setContentPointerState = (isOver) => this.#isPointerOverContent.set(isOver);
876
+ this.isPointerInSystem = () => this.#isPointerOverContent() || this.#isPointerOverTrigger();
877
+ // exposed animation state
878
+ this.getCssAnimation = () => this.cssAnimation;
879
+ this.getCssOpeningAnimation = () => this.cssOpeningAnimation;
880
+ this.getCssClosingAnimation = () => this.cssClosingAnimation;
881
+ effect(() => {
882
+ const value = this.#value();
883
+ if (value) {
884
+ this.window.clearTimeout(this.skipDelayTimerRef);
885
+ if (this.skipDelayDuration > 0) {
886
+ this.#isOpenDelayed.set(false);
887
+ }
888
+ }
889
+ else {
890
+ // menu is closed, start skip delay timer
891
+ this.window.clearTimeout(this.skipDelayTimerRef);
892
+ this.skipDelayTimerRef = this.window.setTimeout(() => {
893
+ this.#isOpenDelayed.set(true);
894
+ }, this.skipDelayDuration);
895
+ }
896
+ });
897
+ this.actionSubject$
898
+ .pipe(map((config) => {
899
+ // different delays for open vs close (better ux)
900
+ const duration = config.action === RdxNavigationMenuAction.OPEN ? this.delayDuration : 150;
901
+ return { ...config, duration };
902
+ }), debounce((config) => timer(config.duration)), tap((config) => {
903
+ switch (config.action) {
904
+ case RdxNavigationMenuAction.OPEN:
905
+ if (config.itemValue) {
906
+ this.setValue(config.itemValue);
907
+ }
908
+ break;
909
+ case RdxNavigationMenuAction.CLOSE:
910
+ // only close if not hovering over any part of the system
911
+ if (!this.isPointerInSystem()) {
912
+ this.setValue('');
913
+ }
914
+ break;
915
+ }
916
+ }), takeUntilDestroyed())
917
+ .subscribe();
918
+ // set up document mouseleave handler to close menu when mouse leaves window
919
+ this.documentMouseLeaveHandler = () => this.handleClose();
920
+ this.document.addEventListener('mouseleave', this.documentMouseLeaveHandler);
921
+ }
922
+ ngOnDestroy() {
923
+ this.window.clearTimeout(this.openTimerRef);
924
+ this.window.clearTimeout(this.closeTimerRef);
925
+ this.window.clearTimeout(this.skipDelayTimerRef);
926
+ // clean up document event listener
927
+ if (this.documentMouseLeaveHandler) {
928
+ document.removeEventListener('mouseleave', this.documentMouseLeaveHandler);
929
+ }
930
+ }
931
+ onIndicatorTrackChange(track) {
932
+ this.#indicatorTrack.set(track);
933
+ }
934
+ onViewportChange(viewport) {
935
+ this.#viewport.set(viewport);
936
+ }
937
+ onTriggerEnter(itemValue) {
938
+ // skip opening if user explicitly dismissed this menu
939
+ if (this.#userDismissedByClick() && itemValue === this.#previousValue()) {
940
+ return;
941
+ }
942
+ this.window.clearTimeout(this.openTimerRef);
943
+ this.window.clearTimeout(this.closeTimerRef);
944
+ if (this.#isOpenDelayed()) {
945
+ this.handleDelayedOpen(itemValue);
946
+ }
947
+ else {
948
+ this.handleOpen(itemValue);
949
+ }
950
+ }
951
+ onTriggerLeave() {
952
+ this.window.clearTimeout(this.openTimerRef);
953
+ this.startCloseTimer();
954
+ }
955
+ onContentEnter() {
956
+ this.window.clearTimeout(this.closeTimerRef);
957
+ }
958
+ onContentLeave() {
959
+ this.startCloseTimer();
960
+ }
961
+ handleClose() {
962
+ this.actionSubject$.next({ action: RdxNavigationMenuAction.CLOSE });
963
+ }
964
+ onItemSelect(itemValue) {
965
+ const wasOpen = this.#value() === itemValue;
966
+ const newValue = wasOpen ? '' : itemValue;
967
+ // if user is closing an open menu, mark as user-dismissed
968
+ if (wasOpen) {
969
+ this.#userDismissedByClick.set(true);
970
+ }
971
+ else {
972
+ this.#userDismissedByClick.set(false);
973
+ }
974
+ this.setValue(newValue);
975
+ }
976
+ onItemDismiss() {
977
+ this.setValue('');
978
+ }
979
+ onViewportContentChange(contentValue, contentData) {
980
+ const newMap = new Map(this.#viewportContent());
981
+ newMap.set(contentValue, contentData);
982
+ this.#viewportContent.set(newMap);
983
+ }
984
+ onViewportContentRemove(contentValue) {
985
+ const newMap = new Map(this.#viewportContent());
986
+ if (newMap.has(contentValue)) {
987
+ newMap.delete(contentValue);
988
+ this.#viewportContent.set(newMap);
989
+ }
990
+ }
991
+ setValue(value) {
992
+ // Store previous value before changing
993
+ this.#previousValue.set(this.#value());
994
+ this.#value.set(value);
995
+ }
996
+ startCloseTimer() {
997
+ this.window.clearTimeout(this.closeTimerRef);
998
+ this.closeTimerRef = this.window.setTimeout(() => {
999
+ // only close if not hovering over any part of the system
1000
+ if (!this.isPointerInSystem()) {
1001
+ this.setValue('');
1002
+ }
1003
+ }, 150);
1004
+ }
1005
+ handleOpen(itemValue) {
1006
+ this.window.clearTimeout(this.closeTimerRef);
1007
+ this.setValue(itemValue);
1008
+ }
1009
+ handleDelayedOpen(itemValue) {
1010
+ const isOpenItem = this.#value() === itemValue;
1011
+ if (isOpenItem) {
1012
+ // if the item is already open, clear close timer
1013
+ this.window.clearTimeout(this.closeTimerRef);
1014
+ }
1015
+ else {
1016
+ // otherwise, start the open timer
1017
+ this.openTimerRef = this.window.setTimeout(() => {
1018
+ this.window.clearTimeout(this.closeTimerRef);
1019
+ this.setValue(itemValue);
1020
+ }, this.delayDuration);
1021
+ }
1022
+ }
1023
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1024
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.4", type: RdxNavigationMenuDirective, isStandalone: true, selector: "[rdxNavigationMenu]", inputs: { orientation: "orientation", dir: "dir", delayDuration: ["delayDuration", "delayDuration", numberAttribute], skipDelayDuration: ["skipDelayDuration", "skipDelayDuration", numberAttribute], loop: ["loop", "loop", booleanAttribute], cssAnimation: ["cssAnimation", "cssAnimation", booleanAttribute], cssOpeningAnimation: ["cssOpeningAnimation", "cssOpeningAnimation", booleanAttribute], cssClosingAnimation: ["cssClosingAnimation", "cssClosingAnimation", booleanAttribute] }, host: { attributes: { "aria-label": "Main", "role": "navigation" }, properties: { "attr.data-orientation": "orientation", "attr.dir": "dir" } }, providers: [provideNavigationMenuContext(RdxNavigationMenuDirective)], exportAs: ["rdxNavigationMenu"], ngImport: i0 }); }
1025
+ }
1026
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuDirective, decorators: [{
1027
+ type: Directive,
1028
+ args: [{
1029
+ selector: '[rdxNavigationMenu]',
1030
+ providers: [provideNavigationMenuContext(RdxNavigationMenuDirective)],
1031
+ host: {
1032
+ '[attr.data-orientation]': 'orientation',
1033
+ '[attr.dir]': 'dir',
1034
+ 'aria-label': 'Main',
1035
+ role: 'navigation'
1036
+ },
1037
+ exportAs: 'rdxNavigationMenu'
1038
+ }]
1039
+ }], ctorParameters: () => [], propDecorators: { orientation: [{
1040
+ type: Input
1041
+ }], dir: [{
1042
+ type: Input
1043
+ }], delayDuration: [{
1044
+ type: Input,
1045
+ args: [{ transform: numberAttribute }]
1046
+ }], skipDelayDuration: [{
1047
+ type: Input,
1048
+ args: [{ transform: numberAttribute }]
1049
+ }], loop: [{
1050
+ type: Input,
1051
+ args: [{ transform: booleanAttribute }]
1052
+ }], cssAnimation: [{
1053
+ type: Input,
1054
+ args: [{ transform: booleanAttribute }]
1055
+ }], cssOpeningAnimation: [{
1056
+ type: Input,
1057
+ args: [{ transform: booleanAttribute }]
1058
+ }], cssClosingAnimation: [{
1059
+ type: Input,
1060
+ args: [{ transform: booleanAttribute }]
1061
+ }] } });
1062
+
1063
+ class RdxNavigationMenuSubDirective {
1064
+ constructor() {
1065
+ this.orientation = input('horizontal');
1066
+ this.valueChange = output();
1067
+ this.value = signal('');
1068
+ this.previousValue = signal('');
1069
+ this.baseId = `rdx-nav-menu-sub-${generateId()}`;
1070
+ this.isRootMenu = false;
1071
+ this.parent = inject(RdxNavigationMenuDirective, { optional: true });
1072
+ }
1073
+ set defaultValue(val) {
1074
+ if (val)
1075
+ this.value.set(val);
1076
+ }
1077
+ get dir() {
1078
+ if (!this.parent) {
1079
+ return 'ltr';
1080
+ }
1081
+ return this.parent.dir || 'ltr';
1082
+ }
1083
+ get rootNavigationMenu() {
1084
+ return this.parent?.rootNavigationMenu() || null;
1085
+ }
1086
+ onTriggerEnter(itemValue) {
1087
+ this.setValue(itemValue);
1088
+ }
1089
+ onItemSelect(itemValue) {
1090
+ this.setValue(itemValue);
1091
+ }
1092
+ onItemDismiss() {
1093
+ this.setValue('');
1094
+ }
1095
+ setValue(value) {
1096
+ this.previousValue.set(this.value());
1097
+ this.value.set(value);
1098
+ this.valueChange.emit(value);
1099
+ }
1100
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuSubDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1101
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuSubDirective, isStandalone: true, selector: "[rdxNavigationMenuSub]", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "attr.data-orientation": "orientation()" } }, providers: [provideNavigationMenuContext(RdxNavigationMenuSubDirective)], ngImport: i0 }); }
1102
+ }
1103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuSubDirective, decorators: [{
1104
+ type: Directive,
1105
+ args: [{
1106
+ selector: '[rdxNavigationMenuSub]',
1107
+ providers: [provideNavigationMenuContext(RdxNavigationMenuSubDirective)],
1108
+ host: {
1109
+ '[attr.data-orientation]': 'orientation()'
1110
+ }
1111
+ }]
1112
+ }], propDecorators: { defaultValue: [{
1113
+ type: Input
1114
+ }] } });
1115
+
1116
+ class RdxNavigationMenuTriggerDirective extends RdxNavigationMenuFocusableOption {
1117
+ constructor() {
1118
+ super();
1119
+ this.context = injectNavigationMenu();
1120
+ this.item = inject(RdxNavigationMenuItemDirective);
1121
+ this.list = inject(RdxNavigationMenuListDirective);
1122
+ this.rovingFocusItem = inject(RdxRovingFocusItemDirective, { self: true });
1123
+ this.elementRef = inject(ElementRef);
1124
+ this.viewContainerRef = inject(ViewContainerRef);
1125
+ this.disabled = input(false, { transform: booleanAttribute });
1126
+ this.triggerId = makeTriggerId(this.context.baseId, this.item.value());
1127
+ this.contentId = makeContentId(this.context.baseId, this.item.value());
1128
+ this.open = computed(() => {
1129
+ return this.item.value() === this.context.value();
1130
+ });
1131
+ this.focusProxyRef = null;
1132
+ this.ariaOwnsRef = null;
1133
+ this.hasPointerMoveOpened = false;
1134
+ this.wasClickClose = false;
1135
+ effect(() => {
1136
+ this.rovingFocusItem.focusable = !this.disabled();
1137
+ });
1138
+ effect(() => {
1139
+ const isOpen = this.open();
1140
+ untracked(() => {
1141
+ // handle focus proxy and aria-owns when open state changes
1142
+ if (isOpen) {
1143
+ this.createAccessibilityComponents();
1144
+ }
1145
+ else {
1146
+ this.removeAccessibilityComponents();
1147
+ if (!this.item.wasEscapeCloseRef()) {
1148
+ this.item.onRootContentClose();
1149
+ }
1150
+ this.hasPointerMoveOpened = false;
1151
+ }
1152
+ });
1153
+ });
1154
+ }
1155
+ ngOnInit() {
1156
+ this.item.triggerRef.set(this.elementRef.nativeElement);
1157
+ // configure the static part of the roving focus item directive instance
1158
+ this.rovingFocusItem.tabStopId = this.item.value();
1159
+ }
1160
+ ngOnDestroy() {
1161
+ this.removeAccessibilityComponents();
1162
+ }
1163
+ focus() {
1164
+ this.elementRef.nativeElement.focus();
1165
+ }
1166
+ createAccessibilityComponents() {
1167
+ if (this.focusProxyRef || this.ariaOwnsRef) {
1168
+ return;
1169
+ }
1170
+ // create focus proxy component
1171
+ this.focusProxyRef = this.viewContainerRef.createComponent(RdxNavigationMenuFocusProxyComponent);
1172
+ this.focusProxyRef.instance.triggerElement = this.elementRef.nativeElement;
1173
+ this.focusProxyRef.instance.contentElement = this.item.contentRef();
1174
+ this.focusProxyRef.instance.proxyFocus.subscribe((direction) => {
1175
+ this.item.onFocusProxyEnter(direction);
1176
+ });
1177
+ // store reference in item directive
1178
+ this.item.focusProxyRef.set(this.focusProxyRef.location.nativeElement);
1179
+ // only add aria-owns component if using viewport
1180
+ if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
1181
+ this.ariaOwnsRef = this.viewContainerRef.createComponent(RdxNavigationMenuAriaOwnsComponent);
1182
+ this.ariaOwnsRef.instance.contentId = this.contentId;
1183
+ }
1184
+ }
1185
+ removeAccessibilityComponents() {
1186
+ if (this.focusProxyRef) {
1187
+ this.focusProxyRef.destroy();
1188
+ this.focusProxyRef = null;
1189
+ this.item.focusProxyRef.set(null);
1190
+ }
1191
+ if (this.ariaOwnsRef) {
1192
+ this.ariaOwnsRef.destroy();
1193
+ this.ariaOwnsRef = null;
1194
+ }
1195
+ }
1196
+ onPointerEnter() {
1197
+ // ignore if disabled or not the root menu (hover logic primarily for root)
1198
+ if (this.disabled() || !isRootNavigationMenu(this.context))
1199
+ return;
1200
+ this.wasClickClose = false; // Reset click close flag on enter
1201
+ this.item.wasEscapeCloseRef.set(false); // Reset escape flag
1202
+ this.context.setTriggerPointerState?.(true); // Update context state
1203
+ // if the menu isn't already open for this item, trigger the enter logic (handles delays)
1204
+ if (!this.open()) {
1205
+ this.context.onTriggerEnter?.(this.item.value());
1206
+ }
1207
+ }
1208
+ onPointerMove(event) {
1209
+ // ignore if not a mouse event, disabled, closed by click/escape, or already opened by this move
1210
+ if (event.pointerType !== 'mouse' ||
1211
+ this.disabled() ||
1212
+ this.wasClickClose ||
1213
+ this.item.wasEscapeCloseRef() ||
1214
+ this.hasPointerMoveOpened ||
1215
+ !isRootNavigationMenu(this.context)) {
1216
+ return;
1217
+ }
1218
+ // trigger enter logic (handles delays) and mark that this move initiated an open attempt
1219
+ this.context.onTriggerEnter?.(this.item.value());
1220
+ this.hasPointerMoveOpened = true;
1221
+ }
1222
+ onPointerLeave(event) {
1223
+ // ignore if not a mouse event or disabled
1224
+ if (event.pointerType !== 'mouse' || this.disabled() || !isRootNavigationMenu(this.context)) {
1225
+ return;
1226
+ }
1227
+ this.context.setTriggerPointerState?.(false); // Update context state
1228
+ this.context.onTriggerLeave?.(); // Trigger leave logic (handles delays)
1229
+ this.hasPointerMoveOpened = false; // Reset flag
1230
+ // reset user dismissal flag if pointer leaves the whole system (trigger + content)
1231
+ if (this.context.resetUserDismissed) {
1232
+ // relay slightly to allow pointer movement to content area without resetting dismissal state
1233
+ setTimeout(() => {
1234
+ if (!this.context.isPointerInSystem?.()) {
1235
+ this.context.resetUserDismissed?.();
1236
+ }
1237
+ }, 50); // small delay for tolerance
1238
+ }
1239
+ }
1240
+ onClick() {
1241
+ if (this.disabled())
1242
+ return;
1243
+ // manually set the `KeyManager` active item to this trigger
1244
+ this.list.setActiveItem(this.item);
1245
+ if (this.context.onItemSelect) {
1246
+ this.context.onItemSelect(this.item.value());
1247
+ // track if this click action resulted in closing the menu
1248
+ this.wasClickClose = !this.open();
1249
+ // reset escape flag if menu was opened by click
1250
+ if (this.open()) {
1251
+ this.item.wasEscapeCloseRef.set(false);
1252
+ }
1253
+ }
1254
+ }
1255
+ onKeydown(event) {
1256
+ if (this.disabled())
1257
+ return;
1258
+ if (event.key === ENTER || event.key === SPACE) {
1259
+ event.preventDefault(); // prevent default button behavior
1260
+ this.onClick();
1261
+ // if menu was opened by this keypress, move focus into the content
1262
+ if (this.open()) {
1263
+ // defer focus slightly to ensure content is ready
1264
+ setTimeout(() => this.item.onEntryKeyDown(), 0);
1265
+ }
1266
+ return;
1267
+ }
1268
+ const isHorizontal = this.context.orientation === 'horizontal';
1269
+ const isRTL = this.context.dir === 'rtl';
1270
+ // handle `ArrowDown` specifically for viewport navigation
1271
+ if (event.key === ARROW_DOWN || event.key === TAB) {
1272
+ if (event.key === ARROW_DOWN) {
1273
+ event.preventDefault();
1274
+ }
1275
+ // if the menu is open, focus into the content
1276
+ if (this.open()) {
1277
+ if (event.key === TAB) {
1278
+ // needed to ensure that the `keyManager` on the list directive does not activate
1279
+ // any focus updates, shifting focus to the subsequent focusable list item
1280
+ event.stopImmediatePropagation();
1281
+ }
1282
+ // direct focus handling for viewport case
1283
+ if (isRootNavigationMenu(this.context) && this.context.viewport && this.context.viewport()) {
1284
+ // get the viewport element
1285
+ const viewport = this.context.viewport();
1286
+ if (viewport) {
1287
+ // find all tabbable elements in the viewport
1288
+ const tabbables = getTabbableCandidates(viewport);
1289
+ if (tabbables.length > 0) {
1290
+ // focus the first tabbable element directly
1291
+ setTimeout(() => {
1292
+ tabbables[0].focus();
1293
+ }, 0);
1294
+ return;
1295
+ }
1296
+ }
1297
+ }
1298
+ // fallback to the standard entry key down approach
1299
+ setTimeout(() => this.item.onEntryKeyDown(), 0);
1300
+ return;
1301
+ }
1302
+ // if not open but in horizontal orientation, emulate right key navigation
1303
+ if (isHorizontal) {
1304
+ const nextEvent = new KeyboardEvent('keydown', {
1305
+ key: isRTL ? ARROW_LEFT : ARROW_RIGHT,
1306
+ bubbles: true
1307
+ });
1308
+ this.elementRef.nativeElement.dispatchEvent(nextEvent);
1309
+ return;
1310
+ }
1311
+ }
1312
+ // handle ArrowUp in horizontal orientation
1313
+ if (isHorizontal && event.key === ARROW_UP) {
1314
+ event.preventDefault();
1315
+ // emulate a left key press to move to the previous item
1316
+ const nextEvent = new KeyboardEvent('keydown', {
1317
+ key: isRTL ? ARROW_RIGHT : ARROW_LEFT,
1318
+ bubbles: true
1319
+ });
1320
+ this.elementRef.nativeElement.dispatchEvent(nextEvent);
1321
+ return;
1322
+ }
1323
+ // handle vertical navigation and entry into content
1324
+ const verticalEntryKey = isRTL ? ARROW_LEFT : ARROW_RIGHT;
1325
+ const entryKey = isHorizontal ? ARROW_DOWN : verticalEntryKey;
1326
+ if (this.item.contentRef() && event.key === entryKey && event.key !== ARROW_DOWN) {
1327
+ // Skip if it's ArrowDown as we already handled it above
1328
+ event.preventDefault();
1329
+ if (!this.open()) {
1330
+ // if closed, open the menu first
1331
+ this.context.onItemSelect?.(this.item.value());
1332
+ // defer focus movement into content until after state update and render
1333
+ setTimeout(() => this.item.onEntryKeyDown(), 0);
1334
+ }
1335
+ else {
1336
+ // if already open, just move focus into the content
1337
+ this.item.onEntryKeyDown();
1338
+ }
1339
+ return;
1340
+ }
1341
+ }
1342
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1343
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuTriggerDirective, isStandalone: true, selector: "[rdxNavigationMenuTrigger]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, listeners: { "pointerenter": "onPointerEnter()", "pointermove": "onPointerMove($event)", "pointerleave": "onPointerLeave($event)", "click": "onClick()", "keydown": "onKeydown($event)" }, properties: { "id": "triggerId", "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-orientation": "context.orientation", "attr.data-disabled": "disabled() ? \"\" : undefined", "disabled": "disabled() ? true : null", "attr.aria-expanded": "open()", "attr.aria-controls": "contentId", "attr.aria-haspopup": "\"menu\"" } }, providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuTriggerDirective }], usesInheritance: true, hostDirectives: [{ directive: i1.RdxRovingFocusItemDirective }], ngImport: i0 }); }
1344
+ }
1345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuTriggerDirective, decorators: [{
1346
+ type: Directive,
1347
+ args: [{
1348
+ selector: '[rdxNavigationMenuTrigger]',
1349
+ hostDirectives: [RdxRovingFocusItemDirective],
1350
+ host: {
1351
+ '[id]': 'triggerId',
1352
+ '[attr.data-state]': 'open() ? "open" : "closed"',
1353
+ '[attr.data-orientation]': 'context.orientation',
1354
+ '[attr.data-disabled]': 'disabled() ? "" : undefined',
1355
+ '[disabled]': 'disabled() ? true : null',
1356
+ '[attr.aria-expanded]': 'open()',
1357
+ '[attr.aria-controls]': 'contentId',
1358
+ '[attr.aria-haspopup]': '"menu"',
1359
+ '(pointerenter)': 'onPointerEnter()',
1360
+ '(pointermove)': 'onPointerMove($event)',
1361
+ '(pointerleave)': 'onPointerLeave($event)',
1362
+ '(click)': 'onClick()',
1363
+ '(keydown)': 'onKeydown($event)',
1364
+ type: 'button'
1365
+ },
1366
+ providers: [{ provide: RdxNavigationMenuFocusableOption, useExisting: RdxNavigationMenuTriggerDirective }]
1367
+ }]
1368
+ }], ctorParameters: () => [] });
1369
+
1370
+ class RdxNavigationMenuViewportDirective {
1371
+ constructor() {
1372
+ this.context = injectNavigationMenu();
1373
+ this.document = injectDocument();
1374
+ this.window = injectWindow();
1375
+ this.elementRef = inject(ElementRef);
1376
+ this.viewContainerRef = inject(ViewContainerRef);
1377
+ this.renderer = inject(Renderer2);
1378
+ this.zone = inject(NgZone);
1379
+ this.destroyRef = inject(DestroyRef);
1380
+ /**
1381
+ * Used to keep the viewport rendered and available in the DOM, even when closed.
1382
+ * Useful for animations.
1383
+ * @default false
1384
+ */
1385
+ this.forceMount = input(false, { transform: booleanAttribute });
1386
+ this._contentNodes = signal(new Map());
1387
+ this._activeContentNode = signal(null);
1388
+ this._leavingContentNode = signal(null);
1389
+ this._viewportSize = signal(null);
1390
+ this.activeContentValue = computed(() => {
1391
+ if (!isRootNavigationMenu(this.context))
1392
+ return null;
1393
+ return this.context.value() || this.context.previousValue();
1394
+ });
1395
+ this.isOpen = computed(() => {
1396
+ if (!isRootNavigationMenu(this.context))
1397
+ return false;
1398
+ return Boolean(this.context.value() || this.forceMount());
1399
+ });
1400
+ this.dataState = computed(() => getOpenStateLabel(this.isOpen()));
1401
+ this.viewportSize = computed(() => this._viewportSize());
1402
+ this._resizeObserver = new ResizeObserver(() => this.updateSize());
1403
+ this.setupViewportEffect();
1404
+ }
1405
+ ngOnInit() {
1406
+ if (isRootNavigationMenu(this.context) && this.context.onViewportChange) {
1407
+ this.context.onViewportChange(this.elementRef.nativeElement);
1408
+ }
1409
+ }
1410
+ ngOnDestroy() {
1411
+ this._resizeObserver.disconnect();
1412
+ // clean up any remaining nodes/views/subscriptions
1413
+ this._contentNodes().forEach((node) => this.cleanupAfterLeave(node));
1414
+ if (isRootNavigationMenu(this.context) && this.context.onViewportChange) {
1415
+ this.context.onViewportChange(null);
1416
+ }
1417
+ }
1418
+ onKeydown(event) {
1419
+ if (!this.isOpen())
1420
+ return;
1421
+ const tabbableElements = getTabbableCandidates(this.elementRef.nativeElement);
1422
+ if (!tabbableElements.length)
1423
+ return;
1424
+ const activeElement = this.document.activeElement;
1425
+ const currentIndex = tabbableElements.findIndex((el) => el === activeElement);
1426
+ if (event.key === ARROW_DOWN) {
1427
+ event.preventDefault();
1428
+ const nextIndex = currentIndex >= 0 && currentIndex < tabbableElements.length - 1 ? currentIndex + 1 : 0;
1429
+ tabbableElements[nextIndex]?.focus();
1430
+ }
1431
+ else if (event.key === ARROW_UP) {
1432
+ event.preventDefault();
1433
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : tabbableElements.length - 1;
1434
+ tabbableElements[prevIndex]?.focus();
1435
+ }
1436
+ }
1437
+ onPointerEnter() {
1438
+ if (isRootNavigationMenu(this.context) && this.context.onContentEnter) {
1439
+ this.context.onContentEnter();
1440
+ }
1441
+ if (isRootNavigationMenu(this.context) && this.context.setContentPointerState) {
1442
+ this.context.setContentPointerState(true);
1443
+ }
1444
+ }
1445
+ onPointerLeave() {
1446
+ if (isRootNavigationMenu(this.context) && this.context.onContentLeave) {
1447
+ this.context.onContentLeave();
1448
+ }
1449
+ if (isRootNavigationMenu(this.context) && this.context.setContentPointerState) {
1450
+ this.context.setContentPointerState(false);
1451
+ }
1452
+ }
1453
+ setupViewportEffect() {
1454
+ effect(() => {
1455
+ const currentActiveValue = this.context.value();
1456
+ const previousActiveValue = this.context.previousValue();
1457
+ const forceMount = this.forceMount();
1458
+ untracked(() => {
1459
+ // ensure context is root before proceeding
1460
+ if (!isRootNavigationMenu(this.context) || !this.context.viewportContent) {
1461
+ return;
1462
+ }
1463
+ const allContentData = this.context.viewportContent();
1464
+ const currentNodesMap = this._contentNodes();
1465
+ let enteringNode = null;
1466
+ let leavingNode = this._leavingContentNode(); // get potentially already leaving node
1467
+ // 1. Identify Entering Node
1468
+ if (currentActiveValue && allContentData.has(currentActiveValue)) {
1469
+ enteringNode = this.getOrCreateContentNode(currentActiveValue);
1470
+ }
1471
+ // 2. Identify Leaving Node
1472
+ const nodeThatWasActive = previousActiveValue ? currentNodesMap.get(previousActiveValue) : null;
1473
+ // if there was a previously active node, it's different from the entering one,
1474
+ // and it's not already leaving, mark it for removal.
1475
+ if (nodeThatWasActive && nodeThatWasActive !== enteringNode && nodeThatWasActive !== leavingNode) {
1476
+ // if another node was already leaving, force complete its transition
1477
+ if (leavingNode) {
1478
+ this.forceCompleteLeaveTransition(leavingNode);
1479
+ }
1480
+ leavingNode = nodeThatWasActive;
1481
+ this._leavingContentNode.set(leavingNode);
1482
+ }
1483
+ // 3. Handle Entering Node
1484
+ if (enteringNode) {
1485
+ // cancel any pending leave transition for this node if it was leaving
1486
+ if (enteringNode === leavingNode) {
1487
+ this.cancelLeaveTransition(enteringNode);
1488
+ leavingNode = null;
1489
+ this._leavingContentNode.set(null);
1490
+ }
1491
+ // ensure it's in the DOM and set state to open
1492
+ this.addNodeToDOM(enteringNode);
1493
+ this.setNodeState(enteringNode, 'open'); // Triggers enter animation via data-state
1494
+ this._activeContentNode.set(enteringNode);
1495
+ this.updateSize(); // Update size based on the entering node
1496
+ }
1497
+ else {
1498
+ // no node entering, clear active node state
1499
+ this._activeContentNode.set(null);
1500
+ }
1501
+ // 4. Handle Leaving Node
1502
+ if (leavingNode) {
1503
+ if (forceMount) {
1504
+ // if forceMount, just mark as closed, don't trigger removal animation
1505
+ this.setNodeState(leavingNode, 'closed');
1506
+ this._leavingContentNode.set(null); // No longer considered "leaving"
1507
+ }
1508
+ else {
1509
+ // start the leave transition (usePresence handles DOM removal)
1510
+ this.startLeaveTransition(leavingNode);
1511
+ }
1512
+ }
1513
+ });
1514
+ });
1515
+ }
1516
+ // gets or creates the ContentNode (wrapper + view)
1517
+ getOrCreateContentNode(contentValue) {
1518
+ const existingNode = this._contentNodes().get(contentValue);
1519
+ if (existingNode && !existingNode.embeddedView.destroyed) {
1520
+ return existingNode;
1521
+ }
1522
+ // create if doesn't exist or view was destroyed
1523
+ if (!isRootNavigationMenu(this.context) || !this.context.viewportContent)
1524
+ return null;
1525
+ const allContentData = this.context.viewportContent();
1526
+ const contentData = allContentData.get(contentValue);
1527
+ const templateRef = contentData?.templateRef;
1528
+ if (!templateRef) {
1529
+ console.error(`No templateRef found for content value: ${contentValue}`);
1530
+ return null;
1531
+ }
1532
+ try {
1533
+ const embeddedView = this.viewContainerRef.createEmbeddedView(templateRef);
1534
+ const container = this.renderer.createElement('div');
1535
+ this.renderer.setAttribute(container, 'class', 'NavigationMenuContentWrapper');
1536
+ this.renderer.setAttribute(container, 'data-content-value', contentValue);
1537
+ embeddedView.rootNodes.forEach((node) => this.renderer.appendChild(container, node));
1538
+ const newNode = {
1539
+ embeddedView,
1540
+ element: container,
1541
+ contentValue,
1542
+ state: 'closed'
1543
+ };
1544
+ const newMap = new Map(this._contentNodes());
1545
+ newMap.set(contentValue, newNode);
1546
+ this._contentNodes.set(newMap);
1547
+ return newNode;
1548
+ }
1549
+ catch (error) {
1550
+ console.error(`Error creating content node for ${contentValue}:`, error);
1551
+ return null;
1552
+ }
1553
+ }
1554
+ // adds node element to viewport DOM if not already present
1555
+ addNodeToDOM(node) {
1556
+ if (!this.elementRef.nativeElement.contains(node.element)) {
1557
+ this.renderer.appendChild(this.elementRef.nativeElement, node.element);
1558
+ // observe size only when added to DOM
1559
+ this._resizeObserver.observe(node.element);
1560
+ }
1561
+ }
1562
+ // removes node element from viewport DOM
1563
+ removeNodeFromDOM(node) {
1564
+ if (this.elementRef.nativeElement.contains(node.element)) {
1565
+ this._resizeObserver.unobserve(node.element); // stop observing before removal
1566
+ this.renderer.removeChild(this.elementRef.nativeElement, node.element);
1567
+ }
1568
+ }
1569
+ // updates the data-state and motion attributes
1570
+ setNodeState(node, state) {
1571
+ if (node.state === state)
1572
+ return; // avoid redundant updates
1573
+ node.state = state;
1574
+ this.renderer.setAttribute(node.element, 'data-state', state);
1575
+ // apply motion attribute based on context
1576
+ if (isRootNavigationMenu(this.context) && this.context.viewportContent) {
1577
+ const contentData = this.context.viewportContent().get(node.contentValue);
1578
+ if (contentData?.getMotionAttribute) {
1579
+ // get motion based on current state transition
1580
+ const motionAttr = contentData.getMotionAttribute();
1581
+ if (motionAttr) {
1582
+ this.renderer.setAttribute(node.element, 'data-motion', motionAttr);
1583
+ }
1584
+ else {
1585
+ this.renderer.removeAttribute(node.element, 'data-motion');
1586
+ }
1587
+ }
1588
+ else {
1589
+ this.renderer.removeAttribute(node.element, 'data-motion');
1590
+ }
1591
+ }
1592
+ // apply A11y attributes (might only be needed on open?)
1593
+ if (state === 'open') {
1594
+ this.applyA11yAttributes(node);
1595
+ }
1596
+ }
1597
+ // apply A11y attributes to the first child element
1598
+ applyA11yAttributes(node) {
1599
+ if (!isRootNavigationMenu(this.context) || !this.context.viewportContent)
1600
+ return;
1601
+ const contentData = this.context.viewportContent().get(node.contentValue);
1602
+ if (contentData?.additionalAttrs && node.embeddedView.rootNodes.length > 0) {
1603
+ const firstRootNode = node.embeddedView.rootNodes[0];
1604
+ if (firstRootNode) {
1605
+ Object.entries(contentData.additionalAttrs).forEach(([attr, value]) => {
1606
+ this.renderer.setAttribute(firstRootNode, attr, value);
1607
+ });
1608
+ }
1609
+ }
1610
+ }
1611
+ startLeaveTransition(node) {
1612
+ // ensure node exists and isn't already leaving with an active subscription
1613
+ if (!node || node.transitionSubscription) {
1614
+ node.transitionSubscription?.unsubscribe();
1615
+ return;
1616
+ }
1617
+ const startFn = () => {
1618
+ this.setNodeState(node, 'closed');
1619
+ return () => this.cleanupAfterLeave(node);
1620
+ };
1621
+ const options = {
1622
+ animation: true, // assuming CSS animations/transitions handle the exit
1623
+ state: 'continue' // start the leave process
1624
+ };
1625
+ node.transitionSubscription = usePresence(this.zone, node.element, startFn, options)
1626
+ .pipe(takeUntilDestroyed(this.destroyRef))
1627
+ .subscribe({
1628
+ complete: () => {
1629
+ this.cleanupAfterLeave(node);
1630
+ }
1631
+ });
1632
+ }
1633
+ /**
1634
+ * Cleanup function called after leave animation finishes
1635
+ * @param node The node that is leaving
1636
+ */
1637
+ cleanupAfterLeave(node) {
1638
+ // check if this node is still marked as the one leaving
1639
+ if (this._leavingContentNode() === node) {
1640
+ this.removeNodeFromDOM(node);
1641
+ if (!this.forceMount() && node.embeddedView && !node.embeddedView.destroyed) {
1642
+ node.embeddedView.destroy();
1643
+ // Remove from cache if destroyed
1644
+ const newMap = new Map(this._contentNodes());
1645
+ newMap.delete(node.contentValue);
1646
+ this._contentNodes.set(newMap);
1647
+ }
1648
+ node.transitionSubscription = null;
1649
+ this._leavingContentNode.set(null);
1650
+ }
1651
+ else {
1652
+ // if this node is NOT the one currently marked as leaving, it means
1653
+ // a new transition started before this one finished. Just clean up DOM/Sub.
1654
+ this.removeNodeFromDOM(node);
1655
+ node.transitionSubscription?.unsubscribe();
1656
+ node.transitionSubscription = null;
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Cancels an ongoing leave transition (e.g., if user hovers back)
1661
+ * @param node The node that is leaving
1662
+ */
1663
+ cancelLeaveTransition(node) {
1664
+ node.transitionSubscription?.unsubscribe();
1665
+ node.transitionSubscription = null;
1666
+ }
1667
+ /**
1668
+ * Force completes a leave transition (e.g., if another leave starts)
1669
+ * @param node The node that is leaving
1670
+ */
1671
+ forceCompleteLeaveTransition(node) {
1672
+ if (node && node.transitionSubscription) {
1673
+ node.transitionSubscription.unsubscribe();
1674
+ // perform cleanup immediately
1675
+ this.cleanupAfterLeave(node);
1676
+ }
1677
+ }
1678
+ updateSize() {
1679
+ const activeNode = this._activeContentNode()?.element; // measure the currently active node
1680
+ if (!activeNode || !activeNode.isConnected)
1681
+ return;
1682
+ const firstChild = activeNode.firstChild;
1683
+ if (!firstChild)
1684
+ return;
1685
+ this.window.requestAnimationFrame(() => {
1686
+ // keep rAF here for measurement stability
1687
+ activeNode.getBoundingClientRect(); // force layout
1688
+ const width = Math.ceil(firstChild.offsetWidth);
1689
+ const height = Math.ceil(firstChild.offsetHeight);
1690
+ if (width !== 0 || height !== 0) {
1691
+ const currentSize = this._viewportSize();
1692
+ if (!currentSize || currentSize.width !== width || currentSize.height !== height) {
1693
+ this._viewportSize.set({ width, height });
1694
+ }
1695
+ }
1696
+ else if (this._viewportSize() !== null) {
1697
+ this._viewportSize.set(null);
1698
+ }
1699
+ });
1700
+ }
1701
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuViewportDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1702
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.4", type: RdxNavigationMenuViewportDirective, isStandalone: true, selector: "[rdxNavigationMenuViewport]", inputs: { forceMount: { classPropertyName: "forceMount", publicName: "forceMount", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onKeydown($event)", "pointerenter": "onPointerEnter()", "pointerleave": "onPointerLeave()" }, properties: { "attr.data-state": "dataState()", "attr.data-orientation": "context.orientation", "style.--radix-navigation-menu-viewport-width.px": "viewportSize()?.width", "style.--radix-navigation-menu-viewport-height.px": "viewportSize()?.height" } }, ngImport: i0 }); }
1703
+ }
1704
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuViewportDirective, decorators: [{
1705
+ type: Directive,
1706
+ args: [{
1707
+ selector: '[rdxNavigationMenuViewport]',
1708
+ host: {
1709
+ '[attr.data-state]': 'dataState()',
1710
+ '[attr.data-orientation]': 'context.orientation',
1711
+ '[style.--radix-navigation-menu-viewport-width.px]': 'viewportSize()?.width',
1712
+ '[style.--radix-navigation-menu-viewport-height.px]': 'viewportSize()?.height',
1713
+ '(keydown)': 'onKeydown($event)',
1714
+ '(pointerenter)': 'onPointerEnter()',
1715
+ '(pointerleave)': 'onPointerLeave()'
1716
+ }
1717
+ }]
1718
+ }], ctorParameters: () => [] });
1719
+
1720
+ const _imports = [
1721
+ RdxNavigationMenuDirective,
1722
+ RdxNavigationMenuSubDirective,
1723
+ RdxNavigationMenuListDirective,
1724
+ RdxNavigationMenuItemDirective,
1725
+ RdxNavigationMenuTriggerDirective,
1726
+ RdxNavigationMenuLinkDirective,
1727
+ RdxNavigationMenuIndicatorDirective,
1728
+ RdxNavigationMenuContentDirective,
1729
+ RdxNavigationMenuViewportDirective,
1730
+ RdxNavigationMenuFocusProxyComponent,
1731
+ RdxNavigationMenuAriaOwnsComponent
1732
+ ];
1733
+ class RdxNavigationMenuModule {
1734
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1735
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule, imports: [RdxNavigationMenuDirective,
1736
+ RdxNavigationMenuSubDirective,
1737
+ RdxNavigationMenuListDirective,
1738
+ RdxNavigationMenuItemDirective,
1739
+ RdxNavigationMenuTriggerDirective,
1740
+ RdxNavigationMenuLinkDirective,
1741
+ RdxNavigationMenuIndicatorDirective,
1742
+ RdxNavigationMenuContentDirective,
1743
+ RdxNavigationMenuViewportDirective,
1744
+ RdxNavigationMenuFocusProxyComponent,
1745
+ RdxNavigationMenuAriaOwnsComponent], exports: [RdxNavigationMenuDirective,
1746
+ RdxNavigationMenuSubDirective,
1747
+ RdxNavigationMenuListDirective,
1748
+ RdxNavigationMenuItemDirective,
1749
+ RdxNavigationMenuTriggerDirective,
1750
+ RdxNavigationMenuLinkDirective,
1751
+ RdxNavigationMenuIndicatorDirective,
1752
+ RdxNavigationMenuContentDirective,
1753
+ RdxNavigationMenuViewportDirective,
1754
+ RdxNavigationMenuFocusProxyComponent,
1755
+ RdxNavigationMenuAriaOwnsComponent] }); }
1756
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule }); }
1757
+ }
1758
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: RdxNavigationMenuModule, decorators: [{
1759
+ type: NgModule,
1760
+ args: [{
1761
+ imports: [..._imports],
1762
+ exports: [..._imports]
1763
+ }]
1764
+ }] });
1765
+
1766
+ /**
1767
+ * Generated bundle index. Do not edit.
1768
+ */
1769
+
1770
+ export { RDX_NAVIGATION_MENU_TOKEN, RdxNavigationMenuAction, RdxNavigationMenuAnimationStatus, RdxNavigationMenuAriaOwnsComponent, RdxNavigationMenuContentDirective, RdxNavigationMenuDirective, RdxNavigationMenuFocusProxyComponent, RdxNavigationMenuFocusableOption, RdxNavigationMenuIndicatorDirective, RdxNavigationMenuItemDirective, RdxNavigationMenuLinkDirective, RdxNavigationMenuListDirective, RdxNavigationMenuModule, RdxNavigationMenuSubDirective, RdxNavigationMenuTriggerDirective, RdxNavigationMenuViewportDirective, injectNavigationMenu, isRootNavigationMenu, provideNavigationMenuContext };
1771
+ //# sourceMappingURL=radix-ng-primitives-navigation-menu.mjs.map