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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/composite/README.md +3 -0
  2. package/fesm2022/radix-ng-primitives-accordion.mjs +12 -36
  3. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  4. package/fesm2022/radix-ng-primitives-checkbox.mjs +33 -18
  5. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  6. package/fesm2022/radix-ng-primitives-composite.mjs +515 -0
  7. package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -0
  8. package/fesm2022/radix-ng-primitives-core.mjs +7 -0
  9. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-dialog.mjs +54 -12
  11. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  12. package/fesm2022/radix-ng-primitives-drawer.mjs +442 -2
  13. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-editable.mjs +12 -7
  15. package/fesm2022/radix-ng-primitives-editable.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs +294 -8
  17. package/fesm2022/radix-ng-primitives-floating-focus-manager.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-focus-scope.mjs +9 -0
  19. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-menu.mjs +71 -20
  21. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  22. package/fesm2022/radix-ng-primitives-menubar.mjs +68 -36
  23. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  24. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +281 -88
  25. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  26. package/fesm2022/radix-ng-primitives-number-field.mjs +7 -2
  27. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  28. package/fesm2022/radix-ng-primitives-popover.mjs +117 -35
  29. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  30. package/fesm2022/radix-ng-primitives-popper.mjs +73 -65
  31. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  32. package/fesm2022/radix-ng-primitives-radio.mjs +77 -36
  33. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  34. package/fesm2022/radix-ng-primitives-roving-focus.mjs +40 -8
  35. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  36. package/fesm2022/radix-ng-primitives-scroll-area.mjs +56 -25
  37. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
  38. package/fesm2022/radix-ng-primitives-select.mjs +62 -37
  39. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  40. package/fesm2022/radix-ng-primitives-slider.mjs +259 -28
  41. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  42. package/fesm2022/radix-ng-primitives-stepper.mjs +11 -7
  43. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  44. package/fesm2022/radix-ng-primitives-switch.mjs +10 -5
  45. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  46. package/fesm2022/radix-ng-primitives-tabs.mjs +64 -30
  47. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  48. package/fesm2022/radix-ng-primitives-toggle-group.mjs +69 -19
  49. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  50. package/fesm2022/radix-ng-primitives-toggle.mjs +37 -13
  51. package/fesm2022/radix-ng-primitives-toggle.mjs.map +1 -1
  52. package/fesm2022/radix-ng-primitives-toolbar.mjs +50 -24
  53. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  54. package/fesm2022/radix-ng-primitives-tooltip.mjs +180 -35
  55. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  56. package/navigation-menu/README.md +5 -2
  57. package/package.json +5 -1
  58. package/types/radix-ng-primitives-accordion.d.ts +9 -13
  59. package/types/radix-ng-primitives-checkbox.d.ts +27 -15
  60. package/types/radix-ng-primitives-composite.d.ts +152 -0
  61. package/types/radix-ng-primitives-core.d.ts +2 -0
  62. package/types/radix-ng-primitives-dialog.d.ts +13 -2
  63. package/types/radix-ng-primitives-drawer.d.ts +40 -2
  64. package/types/radix-ng-primitives-editable.d.ts +11 -5
  65. package/types/radix-ng-primitives-floating-focus-manager.d.ts +113 -16
  66. package/types/radix-ng-primitives-menu.d.ts +13 -5
  67. package/types/radix-ng-primitives-menubar.d.ts +10 -5
  68. package/types/radix-ng-primitives-navigation-menu.d.ts +65 -33
  69. package/types/radix-ng-primitives-number-field.d.ts +8 -3
  70. package/types/radix-ng-primitives-popover.d.ts +26 -10
  71. package/types/radix-ng-primitives-popper.d.ts +1 -0
  72. package/types/radix-ng-primitives-radio.d.ts +22 -13
  73. package/types/radix-ng-primitives-roving-focus.d.ts +15 -1
  74. package/types/radix-ng-primitives-scroll-area.d.ts +4 -1
  75. package/types/radix-ng-primitives-select.d.ts +16 -20
  76. package/types/radix-ng-primitives-slider.d.ts +60 -9
  77. package/types/radix-ng-primitives-stepper.d.ts +11 -4
  78. package/types/radix-ng-primitives-switch.d.ts +10 -4
  79. package/types/radix-ng-primitives-tabs.d.ts +20 -11
  80. package/types/radix-ng-primitives-toggle-group.d.ts +34 -17
  81. package/types/radix-ng-primitives-toggle.d.ts +14 -7
  82. package/types/radix-ng-primitives-toolbar.d.ts +22 -14
  83. package/types/radix-ng-primitives-tooltip.d.ts +38 -14
@@ -1,11 +1,11 @@
1
1
  import * as i0 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';
2
+ import { inject, computed, Directive, ElementRef, input, signal, TemplateRef, booleanAttribute, effect, untracked, output, DestroyRef, isDevMode, model, numberAttribute, ViewContainerRef, Renderer2, NgModule } from '@angular/core';
3
3
  import * as i1 from '@radix-ng/primitives/popper';
4
4
  import { RdxPopperContentWrapper, RdxPopperArrow, RdxPopperContent, provideRdxPopperContentWrapper, provideRdxPopperContentConfig, RdxPopper } from '@radix-ng/primitives/popper';
5
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';
7
- import * as i1$1 from '@radix-ng/primitives/roving-focus';
8
- import { RdxRovingFocusGroupDirective, RdxRovingFocusItemDirective } from '@radix-ng/primitives/roving-focus';
6
+ import { createContext, ENTER, SPACE, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ARROW_DOWN, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_REGISTRATION, TAB, HOME, END, rdxDevError, useGraceArea, createFloatingRootContext, useTransitionStatus, provideFloatingTree, provideFloatingRootContext, RdxFloatingNodeRegistration, createCancelableChangeEventDetails, injectDocument, getMaxTransitionDuration } from '@radix-ng/primitives/core';
7
+ import * as i1$1 from '@radix-ng/primitives/composite';
8
+ import { RdxCompositeItem, RdxCompositeRoot } from '@radix-ng/primitives/composite';
9
9
  import { RdxDismiss } from '@radix-ng/primitives/dismissable-layer';
10
10
  import * as i1$2 from '@radix-ng/primitives/portal';
11
11
  import { RdxPortalPresence } from '@radix-ng/primitives/portal';
@@ -49,24 +49,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
49
49
  class RdxNavigationMenuBackdrop {
50
50
  constructor() {
51
51
  this.rootContext = injectNavigationMenuRootContext();
52
+ const host = inject(ElementRef).nativeElement;
53
+ host.style.setProperty('-webkit-user-select', 'none');
54
+ host.style.webkitUserSelect = 'none';
52
55
  }
53
56
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
54
- 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 }); }
57
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNavigationMenuBackdrop, isStandalone: true, selector: "[rdxNavigationMenuBackdrop]", host: { attributes: { "role": "presentation" }, properties: { "attr.hidden": "rootContext.present() ? undefined : \"\"", "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\"", "style.user-select": "\"none\"" } }, ngImport: i0 }); }
55
58
  }
56
59
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuBackdrop, decorators: [{
57
60
  type: Directive,
58
61
  args: [{
59
62
  selector: '[rdxNavigationMenuBackdrop]',
60
63
  host: {
64
+ role: 'presentation',
65
+ '[attr.hidden]': 'rootContext.present() ? undefined : ""',
61
66
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
62
67
  '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
63
68
  '[attr.data-starting-style]': 'rootContext.transitionStatus() === "starting" ? "" : undefined',
64
69
  '[attr.data-ending-style]': 'rootContext.transitionStatus() === "ending" ? "" : undefined',
65
70
  '[attr.data-instant]': 'rootContext.instant() ? "" : undefined',
66
- '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"'
71
+ '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"',
72
+ '[style.user-select]': '"none"'
67
73
  }
68
74
  }]
69
- }] });
75
+ }], ctorParameters: () => [] });
70
76
 
71
77
  /**
72
78
  * Generate a short unique id segment.
@@ -250,10 +256,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
250
256
  }] });
251
257
 
252
258
  /**
253
- * A navigation link. Closes the menu on selection unless prevented.
259
+ * A navigation link. Can close the menu on selection when `closeOnClick` is enabled.
254
260
  *
255
- * Used both as a top-level menubar item and inside content. It is a plain tabbable anchor (not part
256
- * of the menubar's arrow-key roving), matching Base UI.
261
+ * Used both as a top-level navigation item and inside content. Top-level links join the list's
262
+ * composite collection, matching Base UI's CompositeItem-backed NavigationMenu.Link.
257
263
  */
258
264
  class RdxNavigationMenuLink {
259
265
  constructor() {
@@ -266,7 +272,7 @@ class RdxNavigationMenuLink {
266
272
  /**
267
273
  * Whether selecting the link should close the menu.
268
274
  */
269
- this.closeOnClick = input(true, { ...(ngDevMode ? { debugName: "closeOnClick" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
275
+ this.closeOnClick = input(false, { ...(ngDevMode ? { debugName: "closeOnClick" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
270
276
  /**
271
277
  * Emits when the link is selected. Call `preventDefault()` to keep the menu open.
272
278
  */
@@ -285,12 +291,13 @@ class RdxNavigationMenuLink {
285
291
  }
286
292
  }
287
293
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuLink, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
288
- 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 }); }
294
+ 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" } }, hostDirectives: [{ directive: i1$1.RdxCompositeItem }], ngImport: i0 }); }
289
295
  }
290
296
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuLink, decorators: [{
291
297
  type: Directive,
292
298
  args: [{
293
299
  selector: '[rdxNavigationMenuLink]',
300
+ hostDirectives: [RdxCompositeItem],
294
301
  host: {
295
302
  '[attr.data-active]': 'active() ? "" : undefined',
296
303
  '[attr.aria-current]': 'active() ? "page" : undefined',
@@ -301,18 +308,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
301
308
  }], 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"] }] } });
302
309
 
303
310
  /**
304
- * Contains the navigation menu items. Renders as a menubar with roving keyboard focus.
311
+ * Contains the navigation menu items and coordinates composite keyboard focus between triggers.
305
312
  */
306
313
  class RdxNavigationMenuList {
307
314
  constructor() {
308
315
  this.rootContext = injectNavigationMenuRootContext();
309
- this.rovingFocusGroup = inject(RdxRovingFocusGroupDirective, { self: true });
316
+ this.compositeRoot = inject(RdxCompositeRoot, { self: true });
317
+ const unregisterList = this.rootContext.registerList(inject(ElementRef).nativeElement);
318
+ inject(DestroyRef).onDestroy(unregisterList);
310
319
  effect(() => {
311
- this.rovingFocusGroup.setOrientation(this.rootContext.orientation());
312
- this.rovingFocusGroup.setDir(this.rootContext.dir());
313
- this.rovingFocusGroup.setLoop(this.rootContext.loop());
320
+ this.compositeRoot.setOrientation(this.rootContext.orientation());
321
+ this.compositeRoot.setDir(this.rootContext.dir());
322
+ this.compositeRoot.setLoopFocus(false);
314
323
  });
315
324
  }
325
+ onKeydown(event) {
326
+ if (this.rootContext.nested) {
327
+ return;
328
+ }
329
+ const horizontal = this.rootContext.orientation() === 'horizontal';
330
+ const shouldStop = horizontal
331
+ ? event.key === ARROW_LEFT || event.key === ARROW_RIGHT
332
+ : event.key === ARROW_UP || event.key === ARROW_DOWN;
333
+ if (shouldStop) {
334
+ event.stopPropagation();
335
+ }
336
+ }
316
337
  onPointerLeave(event) {
317
338
  if (event.pointerType === 'touch') {
318
339
  return;
@@ -320,16 +341,16 @@ class RdxNavigationMenuList {
320
341
  this.rootContext.closeOnHover(event);
321
342
  }
322
343
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuList, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
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 }); }
344
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxNavigationMenuList, isStandalone: true, selector: "[rdxNavigationMenuList]", host: { listeners: { "keydown": "onKeydown($event)", "pointerleave": "onPointerLeave($event)" }, properties: { "attr.data-orientation": "rootContext.orientation()" } }, hostDirectives: [{ directive: i1$1.RdxCompositeRoot }], ngImport: i0 }); }
324
345
  }
325
346
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuList, decorators: [{
326
347
  type: Directive,
327
348
  args: [{
328
349
  selector: '[rdxNavigationMenuList]',
329
- hostDirectives: [RdxRovingFocusGroupDirective],
350
+ hostDirectives: [RdxCompositeRoot],
330
351
  host: {
331
- role: 'menubar',
332
352
  '[attr.data-orientation]': 'rootContext.orientation()',
353
+ '(keydown)': 'onKeydown($event)',
333
354
  '(pointerleave)': 'onPointerLeave($event)'
334
355
  }
335
356
  }]
@@ -347,11 +368,10 @@ class RdxNavigationMenuPopup {
347
368
  this.elementRef = inject(ElementRef);
348
369
  this.side = computed(() => this.wrapper?.placedSide(), ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
349
370
  this.align = computed(() => this.wrapper?.placedAlign(), ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
350
- /** Names the menu after the active trigger so the `role="menu"` element has an accessible name. */
351
- this.labelledBy = computed(() => {
371
+ this.id = computed(() => {
352
372
  const value = this.rootContext.value() ?? this.rootContext.previousValue();
353
- return value ? this.rootContext.triggerId(value) : undefined;
354
- }, ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
373
+ return value ? `${this.rootContext.baseId}-popup-${value}` : `${this.rootContext.baseId}-popup`;
374
+ }, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
355
375
  /**
356
376
  * Event handler called when the escape key is down. Can be prevented.
357
377
  */
@@ -366,7 +386,9 @@ class RdxNavigationMenuPopup {
366
386
  this.focusOutside = output();
367
387
  const destroyRef = inject(DestroyRef);
368
388
  const unregisterTransitionElement = this.rootContext.registerTransitionElement(this.elementRef.nativeElement);
389
+ const unregisterPopup = this.rootContext.registerPopup(this.elementRef.nativeElement);
369
390
  destroyRef.onDestroy(unregisterTransitionElement);
391
+ destroyRef.onDestroy(unregisterPopup);
370
392
  // The popup is this layer's floating element (the inside surface for containment checks). The
371
393
  // triggers are registered as "inside" on the shared root context (in `registerTrigger`), so a
372
394
  // press / focus on a sibling trigger to switch items — or back on the active trigger — never
@@ -402,16 +424,23 @@ class RdxNavigationMenuPopup {
402
424
  this.rootContext.closeOnHover(event);
403
425
  }
404
426
  /**
405
- * Keyboard navigation inside the open panel: Down/Up move between the panel's focusable items in
406
- * DOM order, Home/End jump to the first/last, and Up from the first item returns focus to the
407
- * trigger. (Tab keeps working natively; Escape is handled by the dismissal capability.)
427
+ * Keyboard navigation inside the open panel mirrors Base UI's CompositeRoot-backed content:
428
+ * arrow keys move between panel focusables in DOM order, Home/End jump to the first/last, Up from
429
+ * the first item returns focus to the trigger, and Tab exits the portalled panel through the
430
+ * logical top-level navigation order. Escape is handled by the dismissal capability.
408
431
  */
409
432
  onKeydown(event) {
410
- if (event.key !== ARROW_DOWN && event.key !== ARROW_UP && event.key !== HOME && event.key !== END) {
433
+ if (event.key !== ARROW_DOWN &&
434
+ event.key !== ARROW_UP &&
435
+ event.key !== ARROW_LEFT &&
436
+ event.key !== ARROW_RIGHT &&
437
+ event.key !== TAB &&
438
+ event.key !== HOME &&
439
+ event.key !== END) {
411
440
  return;
412
441
  }
413
442
  // If the key originates from a nested navigation menu rendered inside this popup, let that
414
- // menu's own roving group / popup handle it — otherwise both react and focus jumps/skips.
443
+ // menu's own composite list / popup handle it — otherwise both react and focus jumps/skips.
415
444
  const nestedRoot = event.target.closest('[rdxNavigationMenuRoot]');
416
445
  if (nestedRoot && this.elementRef.nativeElement.contains(nestedRoot)) {
417
446
  return;
@@ -420,8 +449,12 @@ class RdxNavigationMenuPopup {
420
449
  if (candidates.length === 0) {
421
450
  return;
422
451
  }
423
- event.preventDefault();
424
452
  const currentIndex = candidates.indexOf(document.activeElement);
453
+ if (event.key === TAB) {
454
+ this.handleTabKey(event, candidates, currentIndex);
455
+ return;
456
+ }
457
+ event.preventDefault();
425
458
  if (event.key === HOME) {
426
459
  focusFirst([candidates[0]]);
427
460
  return;
@@ -435,6 +468,18 @@ class RdxNavigationMenuPopup {
435
468
  focusFirst([candidates[next]]);
436
469
  return;
437
470
  }
471
+ if (event.key === ARROW_RIGHT || event.key === ARROW_LEFT) {
472
+ const moveNext = this.rootContext.dir() === 'rtl' ? event.key === ARROW_LEFT : event.key === ARROW_RIGHT;
473
+ const next = moveNext
474
+ ? currentIndex < candidates.length - 1
475
+ ? currentIndex + 1
476
+ : 0
477
+ : currentIndex > 0
478
+ ? currentIndex - 1
479
+ : candidates.length - 1;
480
+ focusFirst([candidates[next]]);
481
+ return;
482
+ }
438
483
  // ArrowUp: from the first item, return focus to the trigger; otherwise move to the previous.
439
484
  if (currentIndex <= 0) {
440
485
  this.rootContext.trigger()?.focus();
@@ -443,8 +488,50 @@ class RdxNavigationMenuPopup {
443
488
  focusFirst([candidates[currentIndex - 1]]);
444
489
  }
445
490
  }
491
+ handleTabKey(event, candidates, currentIndex) {
492
+ if (event.altKey || event.ctrlKey || event.metaKey) {
493
+ return;
494
+ }
495
+ const isFirst = currentIndex <= 0;
496
+ const isLast = currentIndex === candidates.length - 1;
497
+ if (event.shiftKey) {
498
+ if (!isFirst) {
499
+ return;
500
+ }
501
+ event.preventDefault();
502
+ this.rootContext.trigger()?.focus();
503
+ return;
504
+ }
505
+ if (!isLast) {
506
+ return;
507
+ }
508
+ const nextItem = this.getNextTopLevelItem();
509
+ if (!nextItem) {
510
+ return;
511
+ }
512
+ event.preventDefault();
513
+ nextItem.focus();
514
+ }
515
+ getNextTopLevelItem() {
516
+ const activeTrigger = this.rootContext.trigger();
517
+ if (!activeTrigger) {
518
+ return undefined;
519
+ }
520
+ const items = this.getTopLevelItems();
521
+ const currentIndex = items.indexOf(activeTrigger);
522
+ return currentIndex === -1 ? undefined : items[currentIndex + 1];
523
+ }
524
+ getTopLevelItems() {
525
+ const list = this.rootContext.list();
526
+ if (!list) {
527
+ return this.rootContext.triggers();
528
+ }
529
+ return Array.from(list.querySelectorAll('[rdxNavigationMenuTrigger], [rdxNavigationMenuLink]'))
530
+ .filter((item) => !item.hasAttribute('disabled'))
531
+ .filter((item) => item.getAttribute('aria-hidden') !== 'true');
532
+ }
446
533
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPopup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
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 }); }
534
+ 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: { "tabindex": "-1" }, listeners: { "pointerenter": "rootContext.cancelHoverClose()", "pointerleave": "onPointerLeave($event)", "keydown": "onKeydown($event)" }, properties: { "id": "id()", "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()", "style.--popup-width.px": "rootContext.size()?.width", "style.--popup-height.px": "rootContext.size()?.height" } }, hostDirectives: [{ directive: i1.RdxPopperContent }], ngImport: i0 }); }
448
535
  }
449
536
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPopup, decorators: [{
450
537
  type: Directive,
@@ -452,9 +539,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
452
539
  selector: '[rdxNavigationMenuPopup]',
453
540
  hostDirectives: [RdxPopperContent],
454
541
  host: {
455
- role: 'menu',
456
542
  tabindex: '-1',
457
- '[attr.aria-labelledby]': 'labelledBy()',
543
+ '[id]': 'id()',
458
544
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
459
545
  '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
460
546
  '[attr.data-starting-style]': 'rootContext.transitionStatus() === "starting" ? "" : undefined',
@@ -463,6 +549,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
463
549
  '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"',
464
550
  '[attr.data-side]': 'side()',
465
551
  '[attr.data-align]': 'align()',
552
+ '[style.--popup-width.px]': 'rootContext.size()?.width',
553
+ '[style.--popup-height.px]': 'rootContext.size()?.height',
466
554
  '(pointerenter)': 'rootContext.cancelHoverClose()',
467
555
  '(pointerleave)': 'onPointerLeave($event)',
468
556
  '(keydown)': 'onKeydown($event)'
@@ -473,7 +561,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
473
561
  /**
474
562
  * Structural directive that teleports the navigation menu popup into a container (default
475
563
  * `document.body`) while the menu is open, and keeps it mounted until any CSS exit `@keyframes`
476
- * finishes.
564
+ * finishes. Set `[keepMounted]="true"` to keep the portal mounted while closed.
477
565
  *
478
566
  * Apply it with the `*` microsyntax on the positioner —
479
567
  * `<div *rdxNavigationMenuPortal rdxNavigationMenuPositioner>` — or as an explicit
@@ -481,8 +569,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
481
569
  * `[container]`.
482
570
  */
483
571
  class RdxNavigationMenuPortal {
572
+ constructor() {
573
+ this.rootContext = injectNavigationMenuRootContext();
574
+ /**
575
+ * Keep the portal mounted while the menu is closed.
576
+ */
577
+ this.keepMounted = input(false, { ...(ngDevMode ? { debugName: "keepMounted" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
578
+ this.present = computed(() => this.rootContext.present() || this.keepMounted(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
579
+ }
484
580
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPortal, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
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 }); }
581
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxNavigationMenuPortal, isStandalone: true, selector: "ng-template[rdxNavigationMenuPortal]", inputs: { keepMounted: { classPropertyName: "keepMounted", publicName: "keepMounted", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideRdxPresenceContext(() => ({ present: inject(RdxNavigationMenuPortal).present }))], exportAs: ["rdxNavigationMenuPortal"], hostDirectives: [{ directive: i1$2.RdxPortalPresence, inputs: ["container", "container"] }], ngImport: i0 }); }
486
582
  }
487
583
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPortal, decorators: [{
488
584
  type: Directive,
@@ -490,9 +586,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
490
586
  selector: 'ng-template[rdxNavigationMenuPortal]',
491
587
  exportAs: 'rdxNavigationMenuPortal',
492
588
  hostDirectives: [{ directive: RdxPortalPresence, inputs: ['container'] }],
493
- providers: [provideRdxPresenceContext(() => ({ present: injectNavigationMenuRootContext().present }))]
589
+ providers: [provideRdxPresenceContext(() => ({ present: inject(RdxNavigationMenuPortal).present }))]
494
590
  }]
495
- }] });
591
+ }], propDecorators: { keepMounted: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepMounted", required: false }] }] } });
496
592
  /**
497
593
  * Dev-mode guard: `rdxNavigationMenuPortal` used to be an attribute directive on a `<div>`. It is now
498
594
  * structural, so the old `<div rdxNavigationMenuPortal>` markup would silently stop portaling — fail
@@ -532,13 +628,28 @@ class RdxNavigationMenuPositioner extends RdxPopperContentWrapper {
532
628
  this.triggerEl = signal(null, ...(ngDevMode ? [{ debugName: "triggerEl" }] : /* istanbul ignore next */ []));
533
629
  this.containerEl = signal(this.containerRef.nativeElement, ...(ngDevMode ? [{ debugName: "containerEl" }] : /* istanbul ignore next */ []));
534
630
  this.graceArea = useGraceArea(this.triggerEl, this.containerEl);
631
+ const destroyRef = inject(DestroyRef);
535
632
  effect(() => this.triggerEl.set(this.rootContext.trigger() ?? null));
633
+ effect((onCleanup) => {
634
+ const list = this.rootContext.list();
635
+ const inTransit = this.graceArea.isPointerInTransit();
636
+ if (!list || !inTransit) {
637
+ return;
638
+ }
639
+ const previous = list.style.pointerEvents;
640
+ list.style.pointerEvents = 'none';
641
+ onCleanup(() => {
642
+ if (!destroyRef.destroyed) {
643
+ list.style.pointerEvents = previous;
644
+ }
645
+ });
646
+ });
536
647
  // Keep the menu open while the pointer travels from the trigger to the popup; close once it
537
648
  // leaves the grace area between them.
538
649
  this.graceArea.onPointerExit(() => this.rootContext.closeOnHover());
539
650
  }
540
651
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuPositioner, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
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: [
652
+ 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", "style.--positioner-width.px": "rootContext.size()?.width", "style.--positioner-height.px": "rootContext.size()?.height" } }, providers: [
542
653
  ...provideRdxPopperContentWrapper(RdxNavigationMenuPositioner),
543
654
  provideRdxPopperContentConfig({ arrowPadding: 5, collisionPadding: 5, updatePositionStrategy: 'always' })
544
655
  ], usesInheritance: true, ngImport: i0 }); }
@@ -554,7 +665,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
554
665
  host: {
555
666
  '[attr.data-open]': 'rootContext.isOpen() ? "" : undefined',
556
667
  '[attr.data-closed]': 'rootContext.isOpen() ? undefined : ""',
557
- '[attr.data-instant]': 'rootContext.instant() ? "" : undefined'
668
+ '[attr.data-instant]': 'rootContext.instant() ? "" : undefined',
669
+ '[style.--positioner-width.px]': 'rootContext.size()?.width',
670
+ '[style.--positioner-height.px]': 'rootContext.size()?.height'
558
671
  }
559
672
  }]
560
673
  }], ctorParameters: () => [] });
@@ -598,10 +711,6 @@ class RdxNavigationMenuRoot {
598
711
  */
599
712
  this.dirInput = input(undefined, { ...(ngDevMode ? { debugName: "dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
600
713
  this.dir = injectDirection(this.dirInput);
601
- /**
602
- * Whether keyboard navigation loops from the last item back to the first and vice versa.
603
- */
604
- this.loop = input(false, { ...(ngDevMode ? { debugName: "loop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
605
714
  /**
606
715
  * How long to wait before opening the menu on hover, in milliseconds.
607
716
  */
@@ -635,7 +744,10 @@ class RdxNavigationMenuRoot {
635
744
  this.present = computed(() => this.isOpen() || this.preventUnmountOnClose(), ...(ngDevMode ? [{ debugName: "present" }] : /* istanbul ignore next */ []));
636
745
  this.trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
637
746
  this.triggers = signal([], ...(ngDevMode ? [{ debugName: "triggers" }] : /* istanbul ignore next */ []));
747
+ this.list = signal(undefined, ...(ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []));
638
748
  this.contents = signal(new Map(), ...(ngDevMode ? [{ debugName: "contents" }] : /* istanbul ignore next */ []));
749
+ this.popup = signal(undefined, ...(ngDevMode ? [{ debugName: "popup" }] : /* istanbul ignore next */ []));
750
+ this.size = signal(null, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
639
751
  this.activeContent = computed(() => {
640
752
  const value = this.value() ?? this.previousValue();
641
753
  return value ? this.contents().get(value) : undefined;
@@ -688,7 +800,7 @@ class RdxNavigationMenuRoot {
688
800
  if (change.eventDetails.isCanceled()) {
689
801
  return;
690
802
  }
691
- this.instant.set(changedTriggerWhileOpen || reason === 'trigger-focus');
803
+ this.instant.set(changedTriggerWhileOpen);
692
804
  if (changedTriggerWhileOpen) {
693
805
  this.scheduleInstantReset();
694
806
  if (previousTrigger && nextTrigger) {
@@ -702,6 +814,9 @@ class RdxNavigationMenuRoot {
702
814
  this.preventUnmountOnClose.set(value === null ? change.shouldPreventUnmountOnClose() : false);
703
815
  this.value.set(value);
704
816
  this.onValueChange.emit(value);
817
+ if (this.nested && value === null && reason === 'link-select') {
818
+ this.parentRoot?.close(reason, event);
819
+ }
705
820
  }
706
821
  open(value, trigger, reason = 'none', event) {
707
822
  this.clearHoverTimers();
@@ -786,6 +901,14 @@ class RdxNavigationMenuRoot {
786
901
  });
787
902
  };
788
903
  }
904
+ registerList(list) {
905
+ this.list.set(list);
906
+ return () => {
907
+ if (this.list() === list) {
908
+ this.list.set(undefined);
909
+ }
910
+ };
911
+ }
789
912
  registerContent(entry) {
790
913
  this.contents.update((contents) => new Map(contents).set(entry.value, entry));
791
914
  return () => {
@@ -799,6 +922,17 @@ class RdxNavigationMenuRoot {
799
922
  });
800
923
  };
801
924
  }
925
+ registerPopup(element) {
926
+ this.popup.set(element);
927
+ return () => {
928
+ if (this.popup() === element) {
929
+ this.popup.set(undefined);
930
+ }
931
+ };
932
+ }
933
+ setSize(size) {
934
+ this.size.set(size);
935
+ }
802
936
  registerTransitionElement(element) {
803
937
  return this.transition.registerElement(element);
804
938
  }
@@ -851,7 +985,7 @@ class RdxNavigationMenuRoot {
851
985
  });
852
986
  }
853
987
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuRoot, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
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: [
988
+ 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 }, 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: { properties: { "attr.data-orientation": "orientation()", "attr.data-nested": "nested ? \"\" : undefined", "attr.dir": "dir()" } }, providers: [
855
989
  provideNavigationMenuRootContext(context),
856
990
  // Base UI wraps every NavigationMenu.Root in a FloatingNode and only creates FloatingTree at the
857
991
  // top boundary. `provideFloatingTree()` is inherit-or-create, so nested navigation menus join the
@@ -879,20 +1013,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
879
1013
  ],
880
1014
  hostDirectives: [RdxPopper, RdxFloatingNodeRegistration],
881
1015
  host: {
882
- role: 'navigation',
883
- 'aria-label': 'Main',
884
1016
  '[attr.data-orientation]': 'orientation()',
1017
+ '[attr.data-nested]': 'nested ? "" : undefined',
885
1018
  '[attr.dir]': 'dir()'
886
1019
  }
887
1020
  }]
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"] }] } });
1021
+ }], 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 }] }], 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"] }] } });
889
1022
  function contextFor(root) {
890
1023
  return {
891
1024
  nested: root.nested,
892
1025
  baseId: root.baseId,
893
1026
  orientation: root.orientation,
894
1027
  dir: root.dir,
895
- loop: root.loop,
896
1028
  value: root.value,
897
1029
  previousValue: root.previousValue.asReadonly(),
898
1030
  isOpen: root.isOpen,
@@ -901,7 +1033,11 @@ function contextFor(root) {
901
1033
  transitionStatus: root.transitionStatus,
902
1034
  trigger: root.trigger.asReadonly(),
903
1035
  triggers: root.triggers.asReadonly(),
1036
+ list: root.list.asReadonly(),
1037
+ contents: root.contents.asReadonly(),
904
1038
  activeContent: root.activeContent,
1039
+ popup: root.popup.asReadonly(),
1040
+ size: root.size.asReadonly(),
905
1041
  contentId: (value) => root.contentId(value),
906
1042
  triggerId: (value) => root.triggerId(value),
907
1043
  setValue: (value, reason, event) => root.setValue(value, reason, event),
@@ -912,8 +1048,11 @@ function contextFor(root) {
912
1048
  closeOnHover: (event) => root.closeOnHover(event),
913
1049
  cancelHoverOpen: () => root.cancelHoverOpen(),
914
1050
  cancelHoverClose: () => root.cancelHoverClose(),
1051
+ setSize: (size) => root.setSize(size),
915
1052
  registerTrigger: (value, trigger) => root.registerTrigger(value, trigger),
1053
+ registerList: (list) => root.registerList(list),
916
1054
  registerContent: (entry) => root.registerContent(entry),
1055
+ registerPopup: (element) => root.registerPopup(element),
917
1056
  registerTransitionElement: (element) => root.registerTransitionElement(element),
918
1057
  registerViewport: (onTriggerChange) => root.registerViewport(onTriggerChange)
919
1058
  };
@@ -941,7 +1080,6 @@ class RdxNavigationMenuTrigger {
941
1080
  constructor() {
942
1081
  this.item = inject(RdxNavigationMenuItem);
943
1082
  this.rootContext = injectNavigationMenuRootContext();
944
- this.rovingFocusItem = inject(RdxRovingFocusItemDirective, { self: true });
945
1083
  this.elementRef = inject(ElementRef);
946
1084
  this.document = injectDocument();
947
1085
  /**
@@ -955,9 +1093,6 @@ class RdxNavigationMenuTrigger {
955
1093
  this.open = computed(() => this.item.isOpen(), ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
956
1094
  // Host element is available in the constructor; the trigger ref does not depend on inputs.
957
1095
  this.item.triggerRef.set(this.elementRef.nativeElement);
958
- effect(() => this.rovingFocusItem.setFocusable(!this.disabled()));
959
- // `value` is an input on the item, so read it in an effect (kept in sync if it ever changes).
960
- effect(() => this.rovingFocusItem.setTabStopId(this.item.value()));
961
1096
  effect((onCleanup) => {
962
1097
  const value = this.item.value();
963
1098
  const element = this.elementRef.nativeElement;
@@ -985,17 +1120,6 @@ class RdxNavigationMenuTrigger {
985
1120
  }
986
1121
  this.rootContext.cancelHoverOpen();
987
1122
  }
988
- /**
989
- * Open-follows-focus: while the menu is already open, moving keyboard focus (arrow keys via
990
- * roving) to another trigger switches the shared popup to that item — matching Base UI, so the
991
- * open menu visibly responds to arrow-key navigation. Focus never *opens* a closed menu.
992
- */
993
- onFocus() {
994
- if (this.disabled() || !this.rootContext.isOpen() || this.open()) {
995
- return;
996
- }
997
- this.rootContext.open(this.item.value(), this.elementRef.nativeElement, 'trigger-focus');
998
- }
999
1123
  onKeydown(event) {
1000
1124
  if (this.disabled()) {
1001
1125
  return;
@@ -1009,10 +1133,13 @@ class RdxNavigationMenuTrigger {
1009
1133
  }
1010
1134
  return;
1011
1135
  }
1136
+ if (this.rootContext.nested) {
1137
+ return;
1138
+ }
1012
1139
  if (event.key === entryKey) {
1013
1140
  event.preventDefault();
1014
1141
  if (!this.open()) {
1015
- this.rootContext.open(this.item.value(), this.elementRef.nativeElement, 'trigger-press', event);
1142
+ this.rootContext.open(this.item.value(), this.elementRef.nativeElement, 'list-navigation', event);
1016
1143
  }
1017
1144
  this.focusContent();
1018
1145
  }
@@ -1048,19 +1175,18 @@ class RdxNavigationMenuTrigger {
1048
1175
  requestAnimationFrame(tryFocus);
1049
1176
  }
1050
1177
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1051
- 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 }); }
1178
+ 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)" }, properties: { "id": "item.triggerId()", "attr.aria-controls": "open() ? rootContext.popup()?.id : undefined", "attr.aria-expanded": "open()", "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.RdxCompositeItem }], ngImport: i0 }); }
1052
1179
  }
1053
1180
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuTrigger, decorators: [{
1054
1181
  type: Directive,
1055
1182
  args: [{
1056
1183
  selector: 'button[rdxNavigationMenuTrigger]',
1057
- hostDirectives: [RdxRovingFocusItemDirective],
1184
+ hostDirectives: [RdxCompositeItem],
1058
1185
  host: {
1059
1186
  type: 'button',
1060
1187
  '[id]': 'item.triggerId()',
1061
- '[attr.aria-controls]': 'item.contentId()',
1188
+ '[attr.aria-controls]': 'open() ? rootContext.popup()?.id : undefined',
1062
1189
  '[attr.aria-expanded]': 'open()',
1063
- '[attr.aria-haspopup]': '"menu"',
1064
1190
  '[attr.data-state]': 'open() ? "open" : "closed"',
1065
1191
  '[attr.data-popup-open]': 'open() ? "" : undefined',
1066
1192
  '[attr.data-disabled]': 'disabled() ? "" : undefined',
@@ -1068,8 +1194,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1068
1194
  '(click)': 'onClick($event)',
1069
1195
  '(pointerenter)': 'onPointerEnter($event)',
1070
1196
  '(pointerleave)': 'onPointerLeave($event)',
1071
- '(keydown)': 'onKeydown($event)',
1072
- '(focus)': 'onFocus()'
1197
+ '(keydown)': 'onKeydown($event)'
1073
1198
  }
1074
1199
  }]
1075
1200
  }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], openOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "openOnHover", required: false }] }] } });
@@ -1093,37 +1218,66 @@ class RdxNavigationMenuViewport {
1093
1218
  this.transitioning = signal(false, ...(ngDevMode ? [{ debugName: "transitioning" }] : /* istanbul ignore next */ []));
1094
1219
  this.size = signal(null, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1095
1220
  this.current = null;
1221
+ this.rendered = new Map();
1096
1222
  this.previousElement = null;
1097
1223
  this.resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => this.measure()) : null;
1098
- this.activeContent = computed(() => this.rootContext.activeContent(), ...(ngDevMode ? [{ debugName: "activeContent" }] : /* istanbul ignore next */ []));
1224
+ this.activeContent = computed(() => this.rootContext.isOpen() || this.rootContext.transitionStatus() === 'ending'
1225
+ ? this.rootContext.activeContent()
1226
+ : undefined, ...(ngDevMode ? [{ debugName: "activeContent" }] : /* istanbul ignore next */ []));
1227
+ this.contents = computed(() => this.rootContext.contents(), ...(ngDevMode ? [{ debugName: "contents" }] : /* istanbul ignore next */ []));
1099
1228
  const unregister = this.rootContext.registerViewport((previous, next) => {
1100
1229
  this.pendingDirection = getActivationDirection(previous, next);
1101
1230
  });
1102
1231
  effect(() => {
1103
1232
  const entry = this.activeContent();
1104
- untracked(() => this.render(entry));
1233
+ const contents = this.contents();
1234
+ untracked(() => this.sync(contents, entry));
1105
1235
  });
1106
1236
  inject(DestroyRef).onDestroy(() => {
1107
1237
  unregister();
1108
1238
  this.resizeObserver?.disconnect();
1109
1239
  this.clearCleanupTimer();
1110
1240
  this.removePrevious();
1111
- this.current?.view.destroy();
1241
+ this.rendered.forEach((content) => content.view.destroy());
1242
+ this.rendered.clear();
1243
+ this.current = null;
1112
1244
  });
1113
1245
  }
1246
+ sync(contents, activeEntry) {
1247
+ for (const content of [...this.rendered.values()]) {
1248
+ if (content.value === activeEntry?.value) {
1249
+ continue;
1250
+ }
1251
+ const latestEntry = contents.get(content.value);
1252
+ if (!latestEntry || !latestEntry.forceMount()) {
1253
+ if (content !== this.current) {
1254
+ this.destroyRendered(content);
1255
+ }
1256
+ }
1257
+ }
1258
+ for (const entry of contents.values()) {
1259
+ if (entry.value !== activeEntry?.value && entry.forceMount() && !this.rendered.has(entry.value)) {
1260
+ this.markInactive(this.createRendered(entry));
1261
+ }
1262
+ }
1263
+ this.render(activeEntry);
1264
+ }
1114
1265
  render(entry) {
1115
1266
  if (!entry) {
1267
+ this.deactivateCurrent();
1116
1268
  return;
1117
1269
  }
1118
1270
  if (this.current?.value === entry.value) {
1119
1271
  return;
1120
1272
  }
1121
- // Snapshot the outgoing content so it can animate out as `data-previous`.
1122
- if (this.current) {
1123
- this.resizeObserver?.unobserve(this.current.measureTarget);
1124
- this.startLeave(this.current.element);
1125
- this.current.view.destroy();
1126
- }
1273
+ this.deactivateCurrent();
1274
+ const next = this.rendered.get(entry.value) ?? this.createRendered(entry);
1275
+ this.markCurrent(next);
1276
+ this.current = next;
1277
+ this.resizeObserver?.observe(next.measureTarget);
1278
+ this.measure();
1279
+ }
1280
+ createRendered(entry) {
1127
1281
  const view = this.viewContainerRef.createEmbeddedView(entry.templateRef);
1128
1282
  view.detectChanges();
1129
1283
  const element = this.renderer.createElement('div');
@@ -1138,9 +1292,48 @@ class RdxNavigationMenuViewport {
1138
1292
  // and stretches to the viewport, so measuring it would feed the viewport its own width back
1139
1293
  // and the popup would balloon to fill the page. The content root sizes to its content.
1140
1294
  const measureTarget = element.firstElementChild ?? element;
1141
- this.current = { value: entry.value, view, element, measureTarget };
1142
- this.resizeObserver?.observe(measureTarget);
1143
- this.measure();
1295
+ const rendered = { value: entry.value, entry, view, element, measureTarget };
1296
+ this.rendered.set(entry.value, rendered);
1297
+ return rendered;
1298
+ }
1299
+ deactivateCurrent() {
1300
+ const current = this.current;
1301
+ if (!current) {
1302
+ return;
1303
+ }
1304
+ this.resizeObserver?.unobserve(current.measureTarget);
1305
+ if (current.entry.forceMount()) {
1306
+ this.markInactive(current);
1307
+ }
1308
+ else {
1309
+ this.startLeave(current.element);
1310
+ current.view.destroy();
1311
+ this.rendered.delete(current.value);
1312
+ }
1313
+ this.current = null;
1314
+ }
1315
+ markCurrent(content) {
1316
+ content.element.hidden = false;
1317
+ content.element.removeAttribute('aria-hidden');
1318
+ content.element.removeAttribute('inert');
1319
+ content.element.setAttribute('data-current', '');
1320
+ content.element.removeAttribute('data-previous');
1321
+ }
1322
+ markInactive(content) {
1323
+ this.resizeObserver?.unobserve(content.measureTarget);
1324
+ content.element.hidden = true;
1325
+ content.element.setAttribute('aria-hidden', 'true');
1326
+ content.element.setAttribute('inert', '');
1327
+ content.element.removeAttribute('data-current');
1328
+ content.element.removeAttribute('data-previous');
1329
+ }
1330
+ destroyRendered(content) {
1331
+ this.resizeObserver?.unobserve(content.measureTarget);
1332
+ content.view.destroy();
1333
+ this.rendered.delete(content.value);
1334
+ if (this.current === content) {
1335
+ this.current = null;
1336
+ }
1144
1337
  }
1145
1338
  startLeave(element) {
1146
1339
  this.removePrevious();
@@ -1185,11 +1378,13 @@ class RdxNavigationMenuViewport {
1185
1378
  }
1186
1379
  const size = this.size();
1187
1380
  if (!size || size.width !== width || size.height !== height) {
1188
- this.size.set({ width, height });
1381
+ const nextSize = { width, height };
1382
+ this.size.set(nextSize);
1383
+ this.rootContext.setSize(nextSize);
1189
1384
  }
1190
1385
  }
1191
1386
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuViewport, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1192
- 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 }); }
1387
+ 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" } }, ngImport: i0 }); }
1193
1388
  }
1194
1389
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxNavigationMenuViewport, decorators: [{
1195
1390
  type: Directive,
@@ -1201,9 +1396,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
1201
1396
  '[attr.data-state]': 'rootContext.isOpen() ? "open" : "closed"',
1202
1397
  '[attr.data-orientation]': 'rootContext.orientation()',
1203
1398
  '[attr.data-activation-direction]': 'activationDirection()',
1204
- '[attr.data-transitioning]': 'transitioning() ? "" : undefined',
1205
- '[style.--popup-width.px]': 'size()?.width',
1206
- '[style.--popup-height.px]': 'size()?.height'
1399
+ '[attr.data-transitioning]': 'transitioning() ? "" : undefined'
1207
1400
  }
1208
1401
  }]
1209
1402
  }], ctorParameters: () => [], propDecorators: { forceMount: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceMount", required: false }] }] } });