@radix-ng/primitives 1.0.0-beta.2 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +76 -6
  3. package/fesm2022/radix-ng-primitives-accordion.mjs +5 -3
  4. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +31 -24
  6. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-autocomplete.mjs +1744 -0
  8. package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -0
  9. package/fesm2022/radix-ng-primitives-calendar.mjs +5 -3
  10. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  11. package/fesm2022/radix-ng-primitives-combobox.mjs +1399 -606
  12. package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
  13. package/fesm2022/radix-ng-primitives-config.mjs +13 -4
  14. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  15. package/fesm2022/radix-ng-primitives-context-menu.mjs +51 -10
  16. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  17. package/fesm2022/radix-ng-primitives-core.mjs +1345 -64
  18. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  19. package/fesm2022/radix-ng-primitives-date-field.mjs +5 -3
  20. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-dialog.mjs +271 -145
  22. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-direction-provider.mjs +70 -0
  24. package/fesm2022/radix-ng-primitives-direction-provider.mjs.map +1 -0
  25. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +519 -184
  26. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-drawer.mjs +154 -64
  28. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-field.mjs +3 -2
  30. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +517 -0
  32. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -0
  33. package/fesm2022/radix-ng-primitives-focus-scope.mjs +296 -70
  34. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  35. package/fesm2022/radix-ng-primitives-menu.mjs +894 -299
  36. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-menubar.mjs +32 -4
  38. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  39. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +176 -207
  40. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  41. package/fesm2022/radix-ng-primitives-popover.mjs +250 -250
  42. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-popper.mjs +94 -45
  44. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  45. package/fesm2022/radix-ng-primitives-portal.mjs +107 -17
  46. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-presence.mjs +262 -79
  48. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-preview-card.mjs +172 -218
  50. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-roving-focus.mjs +4 -2
  52. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  53. package/fesm2022/radix-ng-primitives-scroll-area.mjs +5 -4
  54. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-select.mjs +303 -234
  56. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-slider.mjs +5 -3
  58. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  59. package/fesm2022/radix-ng-primitives-stepper.mjs +5 -3
  60. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  61. package/fesm2022/radix-ng-primitives-time-field.mjs +5 -3
  62. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  63. package/fesm2022/radix-ng-primitives-toast.mjs +15 -36
  64. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -1
  65. package/fesm2022/radix-ng-primitives-toggle-group.mjs +5 -3
  66. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  67. package/fesm2022/radix-ng-primitives-toolbar.mjs +5 -3
  68. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  69. package/fesm2022/radix-ng-primitives-tooltip.mjs +105 -145
  70. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  71. package/package.json +14 -1
  72. package/types/radix-ng-primitives-accordion.d.ts +4 -3
  73. package/types/radix-ng-primitives-alert-dialog.d.ts +17 -11
  74. package/types/radix-ng-primitives-autocomplete.d.ts +661 -0
  75. package/types/radix-ng-primitives-calendar.d.ts +5 -3
  76. package/types/radix-ng-primitives-combobox.d.ts +727 -293
  77. package/types/radix-ng-primitives-config.d.ts +1 -1
  78. package/types/radix-ng-primitives-context-menu.d.ts +15 -5
  79. package/types/radix-ng-primitives-core.d.ts +762 -14
  80. package/types/radix-ng-primitives-date-field.d.ts +3 -2
  81. package/types/radix-ng-primitives-dialog.d.ts +107 -55
  82. package/types/radix-ng-primitives-direction-provider.d.ts +41 -0
  83. package/types/radix-ng-primitives-dismissable-layer.d.ts +147 -99
  84. package/types/radix-ng-primitives-drawer.d.ts +49 -22
  85. package/types/radix-ng-primitives-field.d.ts +1 -0
  86. package/types/radix-ng-primitives-floating-focus-manager.d.ts +175 -0
  87. package/types/radix-ng-primitives-focus-scope.d.ts +132 -1
  88. package/types/radix-ng-primitives-menu.d.ts +204 -112
  89. package/types/radix-ng-primitives-navigation-menu.d.ts +61 -101
  90. package/types/radix-ng-primitives-popover.d.ts +82 -115
  91. package/types/radix-ng-primitives-popper.d.ts +46 -10
  92. package/types/radix-ng-primitives-portal.d.ts +53 -8
  93. package/types/radix-ng-primitives-presence.d.ts +98 -17
  94. package/types/radix-ng-primitives-preview-card.d.ts +63 -95
  95. package/types/radix-ng-primitives-roving-focus.d.ts +7 -6
  96. package/types/radix-ng-primitives-scroll-area.d.ts +2 -2
  97. package/types/radix-ng-primitives-select.d.ts +192 -158
  98. package/types/radix-ng-primitives-slider.d.ts +5 -4
  99. package/types/radix-ng-primitives-stepper.d.ts +4 -3
  100. package/types/radix-ng-primitives-time-field.d.ts +3 -2
  101. package/types/radix-ng-primitives-toast.d.ts +7 -7
  102. package/types/radix-ng-primitives-toggle-group.d.ts +5 -4
  103. package/types/radix-ng-primitives-toolbar.d.ts +3 -2
  104. package/types/radix-ng-primitives-tooltip.d.ts +48 -84
@@ -1,17 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, computed, Directive, input, signal, TemplateRef, booleanAttribute, effect, untracked, ElementRef, output, DestroyRef, numberAttribute, model, ViewContainerRef, Renderer2, NgModule } from '@angular/core';
2
+ import { inject, computed, Directive, input, signal, TemplateRef, booleanAttribute, effect, untracked, ElementRef, output, DestroyRef, isDevMode, model, numberAttribute, ViewContainerRef, Renderer2, NgModule } from '@angular/core';
3
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';
4
+ import { RdxPopperContentWrapper, RdxPopperArrow, RdxPopperContent, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, RdxPopper } from '@radix-ng/primitives/popper';
5
+ import * as i2 from '@radix-ng/primitives/core';
6
+ import { createContext, ENTER, SPACE, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, ARROW_DOWN, ARROW_UP, HOME, END, rdxDevError, useGraceArea, createFloatingRootContext, useTransitionStatus, provideFloatingTree, provideFloatingRootContext, RdxFloatingNodeRegistration, createCancelableChangeEventDetails, injectDocument, ARROW_LEFT, ARROW_RIGHT, getMaxTransitionDuration } from '@radix-ng/primitives/core';
6
7
  import * as i1$1 from '@radix-ng/primitives/roving-focus';
7
8
  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';
9
+ import { RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
11
10
  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
+ import { RdxPortalPresence } from '@radix-ng/primitives/portal';
12
+ import { provideRdxPresenceContext } from '@radix-ng/primitives/presence';
13
+ import { injectDirection } from '@radix-ng/primitives/direction-provider';
15
14
 
16
15
  const [injectNavigationMenuRootContext, provideNavigationMenuRootContext] = createContext('RdxNavigationMenuRootContext', 'components/navigation-menu');
17
16
 
@@ -318,7 +317,7 @@ class RdxNavigationMenuList {
318
317
  if (event.pointerType === 'touch') {
319
318
  return;
320
319
  }
321
- this.rootContext.closeOnHover();
320
+ this.rootContext.closeOnHover(event);
322
321
  }
323
322
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
324
323
  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 }); }
@@ -342,9 +341,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
342
341
  class RdxNavigationMenuPopup {
343
342
  constructor() {
344
343
  this.rootContext = injectNavigationMenuRootContext();
345
- this.dismissableLayer = inject(RdxDismissableLayer);
344
+ this.floatingContext = inject(RDX_FLOATING_ROOT_CONTEXT);
345
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
346
346
  this.wrapper = inject(RdxPopperContentWrapper, { optional: true });
347
- this.layersContext = inject(RdxDismissableLayersContextToken);
348
347
  this.elementRef = inject(ElementRef);
349
348
  this.side = computed(() => this.wrapper?.placedSide(), ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
350
349
  this.align = computed(() => this.wrapper?.placedAlign(), ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
@@ -353,59 +352,46 @@ class RdxNavigationMenuPopup {
353
352
  const value = this.rootContext.value() ?? this.rootContext.previousValue();
354
353
  return value ? this.rootContext.triggerId(value) : undefined;
355
354
  }, ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
356
- this.dismissReason = 'none';
357
- this.dismissEvent = new Event('navigation-menu.dismiss');
358
355
  /**
359
356
  * Event handler called when the escape key is down. Can be prevented.
360
357
  */
361
- this.escapeKeyDown = outputFromObservable(outputToObservable(this.dismissableLayer.escapeKeyDown));
358
+ this.escapeKeyDown = output();
362
359
  /**
363
360
  * Event handler called when a pointerdown event happens outside the popup. Can be prevented.
364
361
  */
365
- this.pointerDownOutside = outputFromObservable(outputToObservable(this.dismissableLayer.pointerDownOutside));
362
+ this.pointerDownOutside = output();
366
363
  /**
367
364
  * Event handler called when focus moves outside the popup. Can be prevented.
368
365
  */
369
- this.focusOutside = outputFromObservable(outputToObservable(this.dismissableLayer.focusOutside));
366
+ this.focusOutside = output();
370
367
  const destroyRef = inject(DestroyRef);
371
368
  const unregisterTransitionElement = this.rootContext.registerTransitionElement(this.elementRef.nativeElement);
372
369
  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.
376
- effect(() => {
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
- }));
383
- });
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();
370
+ // The popup is this layer's floating element (the inside surface for containment checks). The
371
+ // triggers are registered as "inside" on the shared root context (in `registerTrigger`), so a
372
+ // press / focus on a sibling trigger to switch items — or back on the active trigger never
373
+ // counts as an outside dismissal. This replaces the legacy dismissable-layer `branches` registry.
374
+ this.floatingContext.setFloatingElement(this.elementRef.nativeElement);
375
+ // Dismissal (ADR 0015): Escape, an outside press, or focus moving outside closes the menu.
376
+ // Navigation Menu registers its FloatingNode on the root, matching Base UI's
377
+ // NavigationMenuRoot → FloatingNode wiring. This lets nested roots participate in parent/child
378
+ // ownership even though each root has one shared popup.
379
+ // It does not trap focus, so it closes on focus-out like a non-modal menu.
380
+ new RdxDismiss(this.floatingContext, () => this.registration?.node() ?? null, {
381
+ escapeKey: () => true,
382
+ outsidePress: () => true,
383
+ outsidePressEvent: () => 'intentional',
384
+ focusOutside: () => true,
385
+ onEscapeKeyDown: (event) => this.escapeKeyDown.emit(event),
386
+ onPointerDownOutside: (event) => this.pointerDownOutside.emit(event),
387
+ onFocusOutside: (event) => this.focusOutside.emit(event),
388
+ onDismiss: (reason, event) => {
389
+ const navReason = reason === 'escape-key' ? 'escape-key' : reason === 'focus-outside' ? 'focus-out' : 'outside-press';
390
+ this.rootContext.close(navReason, event);
391
+ // Return focus to the trigger after an Escape dismissal.
392
+ if (reason === 'escape-key') {
393
+ this.rootContext.trigger()?.focus();
394
+ }
409
395
  }
410
396
  });
411
397
  }
@@ -413,12 +399,12 @@ class RdxNavigationMenuPopup {
413
399
  if (event.pointerType === 'touch') {
414
400
  return;
415
401
  }
416
- this.rootContext.closeOnHover();
402
+ this.rootContext.closeOnHover(event);
417
403
  }
418
404
  /**
419
405
  * Keyboard navigation inside the open panel: Down/Up move between the panel's focusable items in
420
406
  * 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.)
407
+ * trigger. (Tab keeps working natively; Escape is handled by the dismissal capability.)
422
408
  */
423
409
  onKeydown(event) {
424
410
  if (event.key !== ARROW_DOWN && event.key !== ARROW_UP && event.key !== HOME && event.key !== END) {
@@ -458,13 +444,13 @@ class RdxNavigationMenuPopup {
458
444
  }
459
445
  }
460
446
  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 }); }
447
+ 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 }], ngImport: i0 }); }
462
448
  }
463
449
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPopup, decorators: [{
464
450
  type: Directive,
465
451
  args: [{
466
452
  selector: '[rdxNavigationMenuPopup]',
467
- hostDirectives: [RdxPopperContent, RdxDismissableLayer],
453
+ hostDirectives: [RdxPopperContent],
468
454
  host: {
469
455
  role: 'menu',
470
456
  tabindex: '-1',
@@ -485,186 +471,93 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
485
471
  }], ctorParameters: () => [], propDecorators: { escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }] } });
486
472
 
487
473
  /**
488
- * Moves the navigation menu popup to a different part of the DOM (by default `document.body`).
474
+ * Structural directive that teleports the navigation menu popup into a container (default
475
+ * `document.body`) while the menu is open, and keeps it mounted until any CSS exit `@keyframes`
476
+ * finishes.
477
+ *
478
+ * Apply it with the `*` microsyntax on the positioner —
479
+ * `<div *rdxNavigationMenuPortal rdxNavigationMenuPositioner>` — or as an explicit
480
+ * `<ng-template rdxNavigationMenuPortal>`. For a custom container use the explicit form with
481
+ * `[container]`.
489
482
  */
490
483
  class RdxNavigationMenuPortal {
491
- constructor() {
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 */ []));
497
- }
498
484
  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 }); }
485
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNavigationMenuPortal, isStandalone: true, selector: "ng-template[rdxNavigationMenuPortal]", providers: [provideRdxPresenceContext(() => ({ present: injectNavigationMenuRootContext().present }))], exportAs: ["rdxNavigationMenuPortal"], hostDirectives: [{ directive: i1$2.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
500
486
  }
501
487
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPortal, decorators: [{
502
488
  type: Directive,
503
489
  args: [{
504
- selector: '[rdxNavigationMenuPortal]',
505
- hostDirectives: [
506
- {
507
- directive: RdxPortal,
508
- inputs: ['container']
509
- }
510
- ],
511
- host: {
512
- '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
513
- '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
514
- '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"'
515
- }
490
+ selector: 'ng-template[rdxNavigationMenuPortal]',
491
+ exportAs: 'rdxNavigationMenuPortal',
492
+ hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
493
+ providers: [provideRdxPresenceContext(() => ({ present: injectNavigationMenuRootContext().present }))]
516
494
  }]
517
- }], propDecorators: { container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }] } });
518
-
495
+ }] });
519
496
  /**
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
- * ```
497
+ * Dev-mode guard: `rdxNavigationMenuPortal` used to be an attribute directive on a `<div>`. It is now
498
+ * structural, so the old `<div rdxNavigationMenuPortal>` markup would silently stop portaling — fail
499
+ * loudly instead.
525
500
  */
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 }); }
501
+ class RdxNavigationMenuPortalMisuseGuard {
502
+ constructor() {
503
+ if (isDevMode()) {
504
+ rdxDevError('navigation-menu/portal-on-element', '`rdxNavigationMenuPortal` is now a structural directive. ' +
505
+ 'Use `*rdxNavigationMenuPortal` on the positioner element or ' +
506
+ '`<ng-template rdxNavigationMenuPortal>`. rdxNavigationMenuPortalPresence has been removed.', 'components/navigation-menu');
507
+ }
508
+ }
509
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPortalMisuseGuard, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
510
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNavigationMenuPortalMisuseGuard, isStandalone: true, selector: "[rdxNavigationMenuPortal]:not(ng-template)", ngImport: i0 }); }
534
511
  }
535
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPortalPresence, decorators: [{
512
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPortalMisuseGuard, decorators: [{
536
513
  type: Directive,
537
514
  args: [{
538
- selector: 'ng-template[rdxNavigationMenuPortalPresence]',
539
- hostDirectives: [RdxPresenceDirective],
540
- providers: [
541
- provideRdxPresenceContext(() => {
542
- const context = injectNavigationMenuRootContext();
543
- return { present: context.isOpen };
544
- })
545
- ]
515
+ selector: '[rdxNavigationMenuPortal]:not(ng-template)'
546
516
  }]
547
- }] });
517
+ }], ctorParameters: () => [] });
548
518
 
549
519
  /**
550
520
  * Positions the shared popup against the active trigger.
521
+ *
522
+ * A "thin" positioner (ADR 0012): it inherits the popper positioning surface (inputs, `placed`
523
+ * output, unified vars + placement attrs) from {@link RdxPopperContentWrapper} and adds the
524
+ * navigation-menu defaults, the open/closed/instant state attributes, and the grace-area hover
525
+ * bridge. It exposes no legacy `--radix-*` aliases.
551
526
  */
552
- class RdxNavigationMenuPositioner {
527
+ class RdxNavigationMenuPositioner extends RdxPopperContentWrapper {
553
528
  constructor() {
529
+ super();
554
530
  this.rootContext = injectNavigationMenuRootContext();
555
- this.wrapper = inject(RdxPopperContentWrapper);
556
- this.elementRef = inject(ElementRef);
531
+ this.containerRef = inject(ElementRef);
557
532
  this.triggerEl = signal(null, ...(ngDevMode ? [{ debugName: "triggerEl" }] : /* istanbul ignore next */ []));
558
- this.containerEl = signal(this.elementRef.nativeElement, ...(ngDevMode ? [{ debugName: "containerEl" }] : /* istanbul ignore next */ []));
533
+ this.containerEl = signal(this.containerRef.nativeElement, ...(ngDevMode ? [{ debugName: "containerEl" }] : /* istanbul ignore next */ []));
559
534
  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
535
  effect(() => this.triggerEl.set(this.rootContext.trigger() ?? null));
613
536
  // Keep the menu open while the pointer travels from the trigger to the popup; close once it
614
537
  // leaves the grace area between them.
615
538
  this.graceArea.onPointerExit(() => this.rootContext.closeOnHover());
616
539
  }
617
540
  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: [
541
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNavigationMenuPositioner, isStandalone: true, selector: "[rdxNavigationMenuPositioner]", host: { properties: { "attr.data-open": "rootContext.isOpen() ? \"\" : undefined", "attr.data-closed": "rootContext.isOpen() ? undefined : \"\"", "attr.data-instant": "rootContext.instant() ? \"\" : undefined" } }, providers: [
542
+ ...provideRdxPopperContentWrapper(RdxNavigationMenuPositioner),
619
543
  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 }); }
544
+ ], usesInheritance: true, ngImport: i0 }); }
621
545
  }
622
546
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPositioner, decorators: [{
623
547
  type: Directive,
624
548
  args: [{
625
549
  selector: '[rdxNavigationMenuPositioner]',
626
550
  providers: [
551
+ ...provideRdxPopperContentWrapper(RdxNavigationMenuPositioner),
627
552
  provideRdxPopperContentConfig({ arrowPadding: 5, collisionPadding: 5, updatePositionStrategy: 'always' })
628
553
  ],
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
554
  host: {
650
555
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
651
556
  '[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
- }`
557
+ '[attr.data-instant]': 'rootContext.instant() ? "" : undefined'
665
558
  }
666
559
  }]
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 }] }] } });
560
+ }], ctorParameters: () => [] });
668
561
 
669
562
  const context = () => contextFor(inject(RdxNavigationMenuRoot));
670
563
  /**
@@ -679,6 +572,12 @@ class RdxNavigationMenuRoot {
679
572
  this.popper = inject(RdxPopper);
680
573
  this.destroyRef = inject(DestroyRef);
681
574
  this.parentRoot = inject(RdxNavigationMenuRoot, { optional: true, skipSelf: true });
575
+ this.registration = inject(RDX_FLOATING_REGISTRATION, { optional: true });
576
+ /** Per-popup floating root context (ADR 0015) — `open` / `triggers` / reference for the dismissal engine. */
577
+ this.floatingContext = createFloatingRootContext({
578
+ ownerDocument: inject(ElementRef).nativeElement.ownerDocument,
579
+ open: () => this.isOpen()
580
+ });
682
581
  /** Whether this root is nested inside another navigation menu's content. */
683
582
  this.nested = !!this.parentRoot;
684
583
  this.baseId = `rdx-nav-menu-${generateId()}`;
@@ -697,7 +596,8 @@ class RdxNavigationMenuRoot {
697
596
  /**
698
597
  * The reading direction of the navigation menu.
699
598
  */
700
- this.dir = input('ltr', ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
599
+ this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
600
+ this.dir = injectDirection(this.dirInput);
701
601
  /**
702
602
  * Whether keyboard navigation loops from the last item back to the first and vice versa.
703
603
  */
@@ -731,6 +631,8 @@ class RdxNavigationMenuRoot {
731
631
  this.transitionStatus = this.transition.status;
732
632
  this.previousValue = signal(null, ...(ngDevMode ? [{ debugName: "previousValue" }] : /* istanbul ignore next */ []));
733
633
  this.isOpen = computed(() => this.value() !== null, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
634
+ this.preventUnmountOnClose = signal(false, ...(ngDevMode ? [{ debugName: "preventUnmountOnClose" }] : /* istanbul ignore next */ []));
635
+ this.present = computed(() => this.isOpen() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
734
636
  this.trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
735
637
  this.triggers = signal([], ...(ngDevMode ? [{ debugName: "triggers" }] : /* istanbul ignore next */ []));
736
638
  this.contents = signal(new Map(), ...(ngDevMode ? [{ debugName: "contents" }] : /* istanbul ignore next */ []));
@@ -757,6 +659,9 @@ class RdxNavigationMenuRoot {
757
659
  });
758
660
  // Anchor the shared popper to the active trigger.
759
661
  effect(() => this.popper.anchorOverride.set(this.trigger()));
662
+ // Keep the dismissal reference in sync with the active trigger (the anchor) so a press / focus on
663
+ // it counts as "inside" (ADR 0015). The full trigger registry is maintained in `registerTrigger`.
664
+ effect(() => this.floatingContext.setReferenceElement(this.trigger() ?? null));
760
665
  this.destroyRef.onDestroy(() => {
761
666
  this.clearHoverTimers();
762
667
  if (this.instantFrame !== undefined) {
@@ -778,6 +683,11 @@ class RdxNavigationMenuRoot {
778
683
  const previousTrigger = this.trigger();
779
684
  const nextTrigger = value ? this.registeredTriggers.get(value) : undefined;
780
685
  const changedTriggerWhileOpen = previous !== null && value !== null && previousTrigger !== nextTrigger;
686
+ const change = this.createOpenChangeEvent(value, reason, event, nextTrigger ?? previousTrigger);
687
+ this.onOpenChange.emit(change.payload);
688
+ if (change.eventDetails.isCanceled()) {
689
+ return;
690
+ }
781
691
  this.instant.set(changedTriggerWhileOpen || reason === 'trigger-focus');
782
692
  if (changedTriggerWhileOpen) {
783
693
  this.scheduleInstantReset();
@@ -789,9 +699,9 @@ class RdxNavigationMenuRoot {
789
699
  this.trigger.set(nextTrigger);
790
700
  }
791
701
  this.previousValue.set(previous);
702
+ this.preventUnmountOnClose.set(value === null ? change.shouldPreventUnmountOnClose() : false);
792
703
  this.value.set(value);
793
704
  this.onValueChange.emit(value);
794
- this.onOpenChange.emit({ value, open: value !== null, reason, event });
795
705
  }
796
706
  open(value, trigger, reason = 'none', event) {
797
707
  this.clearHoverTimers();
@@ -828,7 +738,11 @@ class RdxNavigationMenuRoot {
828
738
  }
829
739
  this.openTimer = setTimeout(() => this.open(value, trigger, 'trigger-hover', event), this.delay());
830
740
  }
831
- closeOnHover() {
741
+ closeOnHover(event) {
742
+ if (event && this.isInsideOpenChild(event.relatedTarget)) {
743
+ this.cancelHoverClose();
744
+ return;
745
+ }
832
746
  this.clearOpenTimer();
833
747
  this.clearCloseTimer();
834
748
  this.closeTimer = setTimeout(() => this.close('list-leave', new Event('navigation-menu.hover-close')), this.closeDelay());
@@ -842,6 +756,9 @@ class RdxNavigationMenuRoot {
842
756
  registerTrigger(value, trigger) {
843
757
  this.registeredTriggers.set(value, trigger);
844
758
  this.triggers.update((triggers) => (triggers.includes(trigger) ? triggers : [...triggers, trigger]));
759
+ // Mark every trigger as "inside" the floating layer (ADR 0015): a press / focus on a sibling
760
+ // trigger (to switch items) or back on the active trigger must not count as an outside dismissal.
761
+ this.floatingContext.triggers.add(trigger);
845
762
  if (this.value() === value) {
846
763
  this.trigger.set(trigger);
847
764
  }
@@ -850,6 +767,7 @@ class RdxNavigationMenuRoot {
850
767
  this.registeredTriggers.delete(value);
851
768
  }
852
769
  this.triggers.update((triggers) => triggers.filter((candidate) => candidate !== trigger));
770
+ this.floatingContext.triggers.delete(trigger);
853
771
  if (this.destroyRef.destroyed || this.value() !== value) {
854
772
  return;
855
773
  }
@@ -888,6 +806,9 @@ class RdxNavigationMenuRoot {
888
806
  this.viewportTriggerChange.add(onTriggerChange);
889
807
  return () => this.viewportTriggerChange.delete(onTriggerChange);
890
808
  }
809
+ createOpenChangeEvent(value, reason, event, trigger) {
810
+ return createNavigationMenuOpenChangeEvent(value, reason, event, trigger);
811
+ }
891
812
  scheduleInstantReset() {
892
813
  if (this.instantFrame !== undefined) {
893
814
  cancelAnimationFrame(this.instantFrame);
@@ -915,16 +836,48 @@ class RdxNavigationMenuRoot {
915
836
  this.closeTimer = undefined;
916
837
  }
917
838
  }
839
+ isInsideOpenChild(target) {
840
+ if (!(target instanceof Node)) {
841
+ return false;
842
+ }
843
+ const node = this.registration?.node();
844
+ if (!node) {
845
+ return false;
846
+ }
847
+ return node.tree.children(node, { onlyOpen: true }).some((child) => {
848
+ const context = child.context;
849
+ const floating = context?.floatingElement;
850
+ return !!floating && floating.contains(target);
851
+ });
852
+ }
918
853
  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 }); }
854
+ 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 }, dirInput: { classPropertyName: "dirInput", 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: [
855
+ provideNavigationMenuRootContext(context),
856
+ // Base UI wraps every NavigationMenu.Root in a FloatingNode and only creates FloatingTree at the
857
+ // top boundary. `provideFloatingTree()` is inherit-or-create, so nested navigation menus join the
858
+ // parent's tree while the top-level menu starts the coordination store.
859
+ provideFloatingTree(),
860
+ // New floating foundation (ADR 0015/0017) — dismissal reads this shared root context, while the
861
+ // root-level node above gives nested navigation menus real parent/child ownership.
862
+ provideFloatingRootContext(() => inject(RdxNavigationMenuRoot).floatingContext)
863
+ ], exportAs: ["rdxNavigationMenuRoot"], hostDirectives: [{ directive: i1.RdxPopper }, { directive: i2.RdxFloatingNodeRegistration }], ngImport: i0 }); }
920
864
  }
921
865
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuRoot, decorators: [{
922
866
  type: Directive,
923
867
  args: [{
924
868
  selector: '[rdxNavigationMenuRoot]',
925
869
  exportAs: 'rdxNavigationMenuRoot',
926
- providers: [provideNavigationMenuRootContext(context)],
927
- hostDirectives: [RdxPopper],
870
+ providers: [
871
+ provideNavigationMenuRootContext(context),
872
+ // Base UI wraps every NavigationMenu.Root in a FloatingNode and only creates FloatingTree at the
873
+ // top boundary. `provideFloatingTree()` is inherit-or-create, so nested navigation menus join the
874
+ // parent's tree while the top-level menu starts the coordination store.
875
+ provideFloatingTree(),
876
+ // New floating foundation (ADR 0015/0017) — dismissal reads this shared root context, while the
877
+ // root-level node above gives nested navigation menus real parent/child ownership.
878
+ provideFloatingRootContext(() => inject(RdxNavigationMenuRoot).floatingContext)
879
+ ],
880
+ hostDirectives: [RdxPopper, RdxFloatingNodeRegistration],
928
881
  host: {
929
882
  role: 'navigation',
930
883
  'aria-label': 'Main',
@@ -932,7 +885,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
932
885
  '[attr.dir]': 'dir()'
933
886
  }
934
887
  }]
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"] }] } });
888
+ }], 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 }] }], dirInput: [{ 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
889
  function contextFor(root) {
937
890
  return {
938
891
  nested: root.nested,
@@ -943,6 +896,7 @@ function contextFor(root) {
943
896
  value: root.value,
944
897
  previousValue: root.previousValue.asReadonly(),
945
898
  isOpen: root.isOpen,
899
+ present: root.present,
946
900
  instant: root.instant.asReadonly(),
947
901
  transitionStatus: root.transitionStatus,
948
902
  trigger: root.trigger.asReadonly(),
@@ -955,7 +909,7 @@ function contextFor(root) {
955
909
  close: (reason, event) => root.close(reason, event),
956
910
  toggle: (value, trigger, event) => root.toggle(value, trigger, event),
957
911
  openOnHover: (value, trigger, event) => root.openOnHover(value, trigger, event),
958
- closeOnHover: () => root.closeOnHover(),
912
+ closeOnHover: (event) => root.closeOnHover(event),
959
913
  cancelHoverOpen: () => root.cancelHoverOpen(),
960
914
  cancelHoverClose: () => root.cancelHoverClose(),
961
915
  registerTrigger: (value, trigger) => root.registerTrigger(value, trigger),
@@ -964,6 +918,21 @@ function contextFor(root) {
964
918
  registerViewport: (onTriggerChange) => root.registerViewport(onTriggerChange)
965
919
  };
966
920
  }
921
+ function createNavigationMenuOpenChangeEvent(value, reason, event, trigger) {
922
+ const change = createCancelableChangeEventDetails(reason, event, trigger);
923
+ return {
924
+ payload: {
925
+ value,
926
+ open: value !== null,
927
+ reason,
928
+ event: change.eventDetails.event,
929
+ trigger,
930
+ eventDetails: change.eventDetails
931
+ },
932
+ eventDetails: change.eventDetails,
933
+ shouldPreventUnmountOnClose: change.shouldPreventUnmountOnClose
934
+ };
935
+ }
967
936
 
968
937
  /**
969
938
  * A button that opens its item's content in the shared popup.
@@ -1248,7 +1217,7 @@ const navigationMenuImports = [
1248
1217
  RdxNavigationMenuContent,
1249
1218
  RdxNavigationMenuLink,
1250
1219
  RdxNavigationMenuPortal,
1251
- RdxNavigationMenuPortalPresence,
1220
+ RdxNavigationMenuPortalMisuseGuard,
1252
1221
  RdxNavigationMenuBackdrop,
1253
1222
  RdxNavigationMenuPositioner,
1254
1223
  RdxNavigationMenuPopup,
@@ -1265,7 +1234,7 @@ class RdxNavigationMenuModule {
1265
1234
  RdxNavigationMenuContent,
1266
1235
  RdxNavigationMenuLink,
1267
1236
  RdxNavigationMenuPortal,
1268
- RdxNavigationMenuPortalPresence,
1237
+ RdxNavigationMenuPortalMisuseGuard,
1269
1238
  RdxNavigationMenuBackdrop,
1270
1239
  RdxNavigationMenuPositioner,
1271
1240
  RdxNavigationMenuPopup,
@@ -1278,7 +1247,7 @@ class RdxNavigationMenuModule {
1278
1247
  RdxNavigationMenuContent,
1279
1248
  RdxNavigationMenuLink,
1280
1249
  RdxNavigationMenuPortal,
1281
- RdxNavigationMenuPortalPresence,
1250
+ RdxNavigationMenuPortalMisuseGuard,
1282
1251
  RdxNavigationMenuBackdrop,
1283
1252
  RdxNavigationMenuPositioner,
1284
1253
  RdxNavigationMenuPopup,
@@ -1298,5 +1267,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1298
1267
  * Generated bundle index. Do not edit.
1299
1268
  */
1300
1269
 
1301
- export { RdxNavigationMenuArrow, RdxNavigationMenuBackdrop, RdxNavigationMenuContent, RdxNavigationMenuIcon, RdxNavigationMenuItem, RdxNavigationMenuLink, RdxNavigationMenuList, RdxNavigationMenuModule, RdxNavigationMenuPopup, RdxNavigationMenuPortal, RdxNavigationMenuPortalPresence, RdxNavigationMenuPositioner, RdxNavigationMenuRoot, RdxNavigationMenuTrigger, RdxNavigationMenuViewport, injectNavigationMenuRootContext, navigationMenuImports, provideNavigationMenuRootContext };
1270
+ export { RdxNavigationMenuArrow, RdxNavigationMenuBackdrop, RdxNavigationMenuContent, RdxNavigationMenuIcon, RdxNavigationMenuItem, RdxNavigationMenuLink, RdxNavigationMenuList, RdxNavigationMenuModule, RdxNavigationMenuPopup, RdxNavigationMenuPortal, RdxNavigationMenuPortalMisuseGuard, RdxNavigationMenuPositioner, RdxNavigationMenuRoot, RdxNavigationMenuTrigger, RdxNavigationMenuViewport, injectNavigationMenuRootContext, navigationMenuImports, provideNavigationMenuRootContext };
1302
1271
  //# sourceMappingURL=radix-ng-primitives-navigation-menu.mjs.map