@radix-ng/primitives 0.32.3 → 0.33.0

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