@radix-ng/primitives 0.50.0 → 1.0.0-beta.0

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