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