@seniorsistemas/angular-components 19.8.0 → 19.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/bignumber-input/lib/bignumber-input/bignumber-input.directive.d.ts +2 -1
  2. package/code-editor/lib/code-editor/core/facades/core-facade.d.ts +3 -1
  3. package/code-editor/lib/code-editor/infra/cores/codemirror-6/codemirror-6-core.d.ts +12 -2
  4. package/drawer/README.md +119 -0
  5. package/drawer/index.d.ts +5 -0
  6. package/{sidebar/lib/sidebar/sidebar.component.d.ts → drawer/lib/drawer/drawer.component.d.ts} +18 -15
  7. package/drawer/lib/drawer/drawer.module.d.ts +8 -0
  8. package/drawer/public-api.d.ts +2 -0
  9. package/esm2022/bignumber-input/lib/bignumber-input/bignumber-input.directive.mjs +6 -3
  10. package/esm2022/code-editor/lib/code-editor/core/facades/core-facade.mjs +7 -1
  11. package/esm2022/code-editor/lib/code-editor/infra/cores/codemirror-6/codemirror-6-core.mjs +47 -22
  12. package/esm2022/drawer/lib/drawer/drawer.component.mjs +192 -0
  13. package/esm2022/drawer/lib/drawer/drawer.module.mjs +17 -0
  14. package/esm2022/drawer/public-api.mjs +3 -0
  15. package/esm2022/drawer/seniorsistemas-angular-components-drawer.mjs +5 -0
  16. package/esm2022/dynamic-form/dynamic-form/form-field/fields/bignumber/bignumber-field.component.mjs +4 -5
  17. package/esm2022/lib/locale/fallback.mjs +3 -2
  18. package/esm2022/select/lib/select/select.component.mjs +11 -14
  19. package/esm2022/side-sheet/lib/side-sheet/models/side-sheet-config.mjs +2 -0
  20. package/esm2022/side-sheet/lib/side-sheet/models/side-sheet-size.mjs +2 -0
  21. package/esm2022/side-sheet/lib/side-sheet/services/side-sheet.service.mjs +82 -0
  22. package/esm2022/side-sheet/lib/side-sheet/side-sheet.component.mjs +171 -0
  23. package/esm2022/side-sheet/public-api.mjs +3 -0
  24. package/esm2022/side-sheet/seniorsistemas-angular-components-side-sheet.mjs +5 -0
  25. package/esm2022/sidebar/public-api.mjs +12 -3
  26. package/esm2022/switch/lib/switch/switch.component.mjs +5 -3
  27. package/esm2022/switch/lib/switch/switch.module.mjs +2 -2
  28. package/esm2022/tabs/lib/tab-item/tab-item.component.mjs +4 -3
  29. package/esm2022/tabs/lib/tabs/tabs.component.mjs +14 -10
  30. package/esm2022/tiered-menu/lib/tiered-menu/components/tiered-menu/tiered-menu.component.mjs +82 -75
  31. package/esm2022/tiered-menu/lib/tiered-menu/components/tiered-menu-item/tiered-menu-item.component.mjs +3 -3
  32. package/esm2022/tiered-menu/lib/tiered-menu/components/tiered-menu-nested/tiered-menu-nested.component.mjs +35 -18
  33. package/esm2022/tiered-menu/lib/tiered-menu/services/tiered-menu.event.service.mjs +2 -1
  34. package/esm2022/tiered-menu/lib/tiered-menu/tiered-menu.directive.mjs +89 -77
  35. package/esm2022/toast/lib/toast/toast.service.mjs +2 -2
  36. package/fesm2022/seniorsistemas-angular-components-bignumber-input.mjs +5 -2
  37. package/fesm2022/seniorsistemas-angular-components-bignumber-input.mjs.map +1 -1
  38. package/fesm2022/seniorsistemas-angular-components-code-editor.mjs +52 -21
  39. package/fesm2022/seniorsistemas-angular-components-code-editor.mjs.map +1 -1
  40. package/fesm2022/seniorsistemas-angular-components-drawer.mjs +211 -0
  41. package/fesm2022/seniorsistemas-angular-components-drawer.mjs.map +1 -0
  42. package/fesm2022/seniorsistemas-angular-components-dynamic-form.mjs +3 -4
  43. package/fesm2022/seniorsistemas-angular-components-dynamic-form.mjs.map +1 -1
  44. package/fesm2022/seniorsistemas-angular-components-select.mjs +10 -13
  45. package/fesm2022/seniorsistemas-angular-components-select.mjs.map +1 -1
  46. package/fesm2022/seniorsistemas-angular-components-side-sheet.mjs +257 -0
  47. package/fesm2022/seniorsistemas-angular-components-side-sheet.mjs.map +1 -0
  48. package/fesm2022/seniorsistemas-angular-components-sidebar.mjs +10 -180
  49. package/fesm2022/seniorsistemas-angular-components-sidebar.mjs.map +1 -1
  50. package/fesm2022/seniorsistemas-angular-components-switch.mjs +5 -3
  51. package/fesm2022/seniorsistemas-angular-components-switch.mjs.map +1 -1
  52. package/fesm2022/seniorsistemas-angular-components-tabs.mjs +16 -11
  53. package/fesm2022/seniorsistemas-angular-components-tabs.mjs.map +1 -1
  54. package/fesm2022/seniorsistemas-angular-components-tiered-menu.mjs +201 -164
  55. package/fesm2022/seniorsistemas-angular-components-tiered-menu.mjs.map +1 -1
  56. package/fesm2022/seniorsistemas-angular-components-toast.mjs +1 -1
  57. package/fesm2022/seniorsistemas-angular-components-toast.mjs.map +1 -1
  58. package/fesm2022/seniorsistemas-angular-components.mjs +2 -1
  59. package/fesm2022/seniorsistemas-angular-components.mjs.map +1 -1
  60. package/package.json +19 -7
  61. package/select/lib/select/select.component.d.ts +2 -1
  62. package/side-sheet/README.md +184 -0
  63. package/side-sheet/index.d.ts +5 -0
  64. package/side-sheet/lib/side-sheet/models/side-sheet-config.d.ts +20 -0
  65. package/side-sheet/lib/side-sheet/models/side-sheet-size.d.ts +1 -0
  66. package/side-sheet/lib/side-sheet/services/side-sheet.service.d.ts +49 -0
  67. package/side-sheet/lib/side-sheet/side-sheet.component.d.ts +61 -0
  68. package/side-sheet/public-api.d.ts +4 -0
  69. package/sidebar/README.md +20 -93
  70. package/sidebar/public-api.d.ts +11 -2
  71. package/tabs/lib/tab-item/tab-item.component.d.ts +1 -0
  72. package/tabs/lib/tabs/tabs.component.d.ts +2 -1
  73. package/tiered-menu/lib/tiered-menu/components/tiered-menu/tiered-menu.component.d.ts +19 -7
  74. package/tiered-menu/lib/tiered-menu/components/tiered-menu-nested/tiered-menu-nested.component.d.ts +4 -3
  75. package/tiered-menu/lib/tiered-menu/services/tiered-menu.event.service.d.ts +1 -0
  76. package/tiered-menu/lib/tiered-menu/tiered-menu.directive.d.ts +6 -5
  77. package/esm2022/sidebar/lib/sidebar/sidebar.component.mjs +0 -169
  78. package/esm2022/sidebar/lib/sidebar/sidebar.module.mjs +0 -17
  79. package/sidebar/lib/sidebar/sidebar.module.d.ts +0 -8
@@ -1,9 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, EventEmitter, inject, Component, Input, HostListener, ApplicationRef, ComponentFactoryResolver, Injector, ChangeDetectorRef, Output, ElementRef, Directive, NgModule } from '@angular/core';
2
+ import { Injectable, EventEmitter, inject, Component, Input, HostListener, ElementRef, Injector, ChangeDetectorRef, Output, Directive, NgModule } from '@angular/core';
3
+ import { Overlay } from '@angular/cdk/overlay';
4
+ import { ComponentPortal } from '@angular/cdk/portal';
3
5
  import { Subject } from 'rxjs';
4
6
  import { takeUntil } from 'rxjs/operators';
5
- import { disableScroll, enableScroll } from '@seniorsistemas/angular-components/utils';
6
- import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
7
+ import { FocusTrapFactory } from '@angular/cdk/a11y';
8
+ import { NgClass, NgTemplateOutlet } from '@angular/common';
7
9
 
8
10
  class TieredMenuService {
9
11
  currentItem = null;
@@ -70,6 +72,7 @@ class TieredMenuEventService {
70
72
  incrementCurrentItemEvent = new EventEmitter();
71
73
  decrementCurrentItemEvent = new EventEmitter();
72
74
  closeAllMenusEvent = new EventEmitter();
75
+ restoreFocusEvent = new EventEmitter();
73
76
  selectItemEvent = new EventEmitter();
74
77
  openItemMenuEvent = new EventEmitter();
75
78
  closeItemMenuEvent = new EventEmitter();
@@ -117,11 +120,11 @@ class TieredMenuItemComponent {
117
120
  window.clearTimeout(this._showTimeout);
118
121
  }
119
122
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
120
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TieredMenuItemComponent, isStandalone: true, selector: "s-tiered-menu-item", inputs: { item: "item", focused: "focused", highlight: "highlight", triggerEvent: "triggerEvent", closeOnClick: "closeOnClick" }, host: { listeners: { "click": "onClick()", "touchend": "onClick()", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0, template: "<div\n [id]=\"item.id\"\n class=\"tiered-menu-item\"\n role=\"menuitem\"\n [tabindex]=\"focused && !item.disabled ? 0 : -1\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.aria-haspopup]=\"item.submenu ? 'menu' : null\"\n [attr.aria-expanded]=\"item.submenu ? item.isOpen : null\"\n [attr.data-key]=\"item.dataKey\"\n [ngClass]=\"{\n 'tiered-menu-item--open': item.isOpen,\n 'tiered-menu-item--focused': focused,\n 'tiered-menu-item--disabled': item.disabled,\n }\"\n>\n <div class=\"tiered-menu-item-content\">\n <span\n class=\"icon\"\n [ngClass]=\"item.iconClass\"\n ></span>\n <span class=\"label\">{{ item.label }}</span>\n </div>\n @if (item.submenu) {\n <span\n class=\"submenu-icon fas\"\n [class.fa-chevron-left]=\"item.isOpen\"\n [class.fa-chevron-right]=\"!item.isOpen\"\n >\n </span>\n }\n</div>\n", styles: [".tiered-menu-item{align-items:center;display:flex;justify-content:space-between;padding:8px 16px;-webkit-user-select:none;user-select:none;cursor:pointer}.tiered-menu-item .tiered-menu-item-content .label{margin:0 12px}.tiered-menu-item:hover{background-color:#e9e6e6}.tiered-menu-item--focused{background-color:#ccc!important}.tiered-menu-item--open{background-color:#e4e2e2}.tiered-menu-item--disabled{opacity:50%;cursor:default}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TieredMenuItemComponent, isStandalone: true, selector: "s-tiered-menu-item", inputs: { item: "item", focused: "focused", highlight: "highlight", triggerEvent: "triggerEvent", closeOnClick: "closeOnClick" }, host: { listeners: { "click": "onClick()", "touchend": "onClick()", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0, template: "<div\n [id]=\"item.id\"\n class=\"tiered-menu-item\"\n role=\"menuitem\"\n [tabindex]=\"focused && !item.disabled ? 0 : -1\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.aria-haspopup]=\"item.submenu ? 'menu' : null\"\n [attr.aria-expanded]=\"item.submenu ? item.isOpen : null\"\n [attr.data-key]=\"item.dataKey\"\n [ngClass]=\"{\n 'tiered-menu-item--open': item.isOpen,\n 'tiered-menu-item--focused': focused,\n 'tiered-menu-item--disabled': item.disabled,\n }\"\n>\n <div class=\"tiered-menu-item-content\">\n <span\n class=\"icon\"\n [ngClass]=\"item.iconClass\"\n ></span>\n <span class=\"label\">{{ item.label }}</span>\n </div>\n @if (item.submenu) {\n <span\n class=\"submenu-icon fas\"\n [class.fa-chevron-left]=\"item.isOpen\"\n [class.fa-chevron-right]=\"!item.isOpen\"\n >\n </span>\n }\n</div>\n", styles: [".tiered-menu-item{align-items:center;display:flex;justify-content:space-between;padding:8px 16px;-webkit-user-select:none;user-select:none;cursor:pointer}.tiered-menu-item .tiered-menu-item-content .label{margin:0 12px;white-space:nowrap}.tiered-menu-item:hover{background-color:#e9e6e6}.tiered-menu-item--focused{background-color:#ccc!important}.tiered-menu-item--open{background-color:#e4e2e2}.tiered-menu-item--disabled{opacity:50%;cursor:default}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
121
124
  }
122
125
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuItemComponent, decorators: [{
123
126
  type: Component,
124
- args: [{ selector: 's-tiered-menu-item', standalone: true, imports: [NgClass], template: "<div\n [id]=\"item.id\"\n class=\"tiered-menu-item\"\n role=\"menuitem\"\n [tabindex]=\"focused && !item.disabled ? 0 : -1\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.aria-haspopup]=\"item.submenu ? 'menu' : null\"\n [attr.aria-expanded]=\"item.submenu ? item.isOpen : null\"\n [attr.data-key]=\"item.dataKey\"\n [ngClass]=\"{\n 'tiered-menu-item--open': item.isOpen,\n 'tiered-menu-item--focused': focused,\n 'tiered-menu-item--disabled': item.disabled,\n }\"\n>\n <div class=\"tiered-menu-item-content\">\n <span\n class=\"icon\"\n [ngClass]=\"item.iconClass\"\n ></span>\n <span class=\"label\">{{ item.label }}</span>\n </div>\n @if (item.submenu) {\n <span\n class=\"submenu-icon fas\"\n [class.fa-chevron-left]=\"item.isOpen\"\n [class.fa-chevron-right]=\"!item.isOpen\"\n >\n </span>\n }\n</div>\n", styles: [".tiered-menu-item{align-items:center;display:flex;justify-content:space-between;padding:8px 16px;-webkit-user-select:none;user-select:none;cursor:pointer}.tiered-menu-item .tiered-menu-item-content .label{margin:0 12px}.tiered-menu-item:hover{background-color:#e9e6e6}.tiered-menu-item--focused{background-color:#ccc!important}.tiered-menu-item--open{background-color:#e4e2e2}.tiered-menu-item--disabled{opacity:50%;cursor:default}\n"] }]
127
+ args: [{ selector: 's-tiered-menu-item', standalone: true, imports: [NgClass], template: "<div\n [id]=\"item.id\"\n class=\"tiered-menu-item\"\n role=\"menuitem\"\n [tabindex]=\"focused && !item.disabled ? 0 : -1\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.aria-haspopup]=\"item.submenu ? 'menu' : null\"\n [attr.aria-expanded]=\"item.submenu ? item.isOpen : null\"\n [attr.data-key]=\"item.dataKey\"\n [ngClass]=\"{\n 'tiered-menu-item--open': item.isOpen,\n 'tiered-menu-item--focused': focused,\n 'tiered-menu-item--disabled': item.disabled,\n }\"\n>\n <div class=\"tiered-menu-item-content\">\n <span\n class=\"icon\"\n [ngClass]=\"item.iconClass\"\n ></span>\n <span class=\"label\">{{ item.label }}</span>\n </div>\n @if (item.submenu) {\n <span\n class=\"submenu-icon fas\"\n [class.fa-chevron-left]=\"item.isOpen\"\n [class.fa-chevron-right]=\"!item.isOpen\"\n >\n </span>\n }\n</div>\n", styles: [".tiered-menu-item{align-items:center;display:flex;justify-content:space-between;padding:8px 16px;-webkit-user-select:none;user-select:none;cursor:pointer}.tiered-menu-item .tiered-menu-item-content .label{margin:0 12px;white-space:nowrap}.tiered-menu-item:hover{background-color:#e9e6e6}.tiered-menu-item--focused{background-color:#ccc!important}.tiered-menu-item--open{background-color:#e4e2e2}.tiered-menu-item--disabled{opacity:50%;cursor:default}\n"] }]
125
128
  }], propDecorators: { item: [{
126
129
  type: Input,
127
130
  args: [{ required: true }]
@@ -159,16 +162,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
159
162
  class TieredMenuNestedComponent {
160
163
  tieredMenuService = inject(TieredMenuService);
161
164
  _tieredMenuEventService = inject(TieredMenuEventService);
165
+ _elementRef = inject(ElementRef);
166
+ _focusTrapFactory = inject(FocusTrapFactory);
162
167
  items;
163
- top = 0;
164
- left = 0;
165
- // public maxHeight = 0;
166
168
  // Saves the last opened item to close it when a new one is opened
167
169
  _lastOpenItem;
170
+ _focusTrap = null;
168
171
  _unsubscribe$ = new Subject();
169
- onResize() {
170
- this._tieredMenuEventService.closeAllMenusEvent.emit();
171
- }
172
172
  onDocumentClick(event) {
173
173
  // Closing menu when clicked outside.
174
174
  const target = event.target;
@@ -180,6 +180,7 @@ class TieredMenuNestedComponent {
180
180
  onKeydownHandler(event) {
181
181
  switch (event.key) {
182
182
  case 'Escape':
183
+ this._tieredMenuEventService.restoreFocusEvent.emit();
183
184
  this._tieredMenuEventService.closeAllMenusEvent.emit();
184
185
  break;
185
186
  case ' ':
@@ -199,17 +200,37 @@ class TieredMenuNestedComponent {
199
200
  case 'ArrowDown':
200
201
  this._tieredMenuEventService.incrementCurrentItemEvent.emit();
201
202
  break;
203
+ case 'Tab':
204
+ // Move focus between items instead of letting Tab escape the (trapped) menu.
205
+ if (event.shiftKey) {
206
+ this._tieredMenuEventService.decrementCurrentItemEvent.emit();
207
+ }
208
+ else {
209
+ this._tieredMenuEventService.incrementCurrentItemEvent.emit();
210
+ }
211
+ event.preventDefault();
212
+ break;
202
213
  }
203
214
  }
204
215
  ngOnInit() {
205
216
  this.tieredMenuService.currentItems = this.items;
217
+ this.tieredMenuService.currentItem = this.items[0] ?? null;
206
218
  this._subscribeEvents();
207
- disableScroll();
219
+ this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
220
+ requestAnimationFrame(() => {
221
+ this._focusCurrentItem();
222
+ });
208
223
  }
209
224
  ngOnDestroy() {
210
225
  this._unsubscribe$.next();
211
226
  this._unsubscribe$.complete();
212
- enableScroll();
227
+ this._focusTrap?.destroy();
228
+ }
229
+ _focusCurrentItem() {
230
+ const id = this.tieredMenuService.currentItem?.id;
231
+ if (!id)
232
+ return;
233
+ document.getElementById(id)?.focus();
213
234
  }
214
235
  _incrementCurItem() {
215
236
  if (!this.tieredMenuService.currentItem) {
@@ -268,9 +289,11 @@ class TieredMenuNestedComponent {
268
289
  _subscribeEvents() {
269
290
  this._tieredMenuEventService.incrementCurrentItemEvent.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
270
291
  this._incrementCurItem();
292
+ this._focusCurrentItem();
271
293
  });
272
294
  this._tieredMenuEventService.decrementCurrentItemEvent.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
273
295
  this._decrementCurItem();
296
+ this._focusCurrentItem();
274
297
  });
275
298
  this._tieredMenuEventService.selectItemEvent
276
299
  .pipe(takeUntil(this._unsubscribe$))
@@ -305,15 +328,12 @@ class TieredMenuNestedComponent {
305
328
  });
306
329
  }
307
330
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuNestedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
308
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TieredMenuNestedComponent, isStandalone: true, selector: "ng-component", host: { listeners: { "window:resize": "onResize()", "document:click": "onDocumentClick($event)", "document:keydown": "onKeydownHandler($event)" } }, ngImport: i0, template: "<div\n class=\"menu menu--nested\"\n [ngStyle]=\"{\n top: top + 'px',\n left: left + 'px',\n }\"\n>\n <ng-container\n *ngTemplateOutlet=\"itemsTemplate; context: { $implicit: items }\"\n ></ng-container>\n</div>\n\n<ng-template\n #itemsTemplate\n let-items\n>\n @for (item of items; track item.id) {\n <div>\n @if (item.visible && !item.divider) {\n <s-tiered-menu-item\n [item]=\"item\"\n [focused]=\"item === tieredMenuService.currentItem\"\n [closeOnClick]=\"true\"\n >\n </s-tiered-menu-item>\n }\n @if (item.divider) {\n <s-tiered-menu-divider></s-tiered-menu-divider>\n }\n @if (item.submenu && item.isOpen) {\n <div>\n <div class=\"submenu\">\n <ng-container\n *ngTemplateOutlet=\"\n itemsTemplate;\n context: { $implicit: item.submenu }\n \"\n ></ng-container>\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:inline-flex;flex-direction:column;min-width:176px;padding:4px 0;position:absolute;z-index:9999;overflow:auto;width:calc(100vw - 8px)}.menu .submenu{margin-left:24px}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TieredMenuItemComponent, selector: "s-tiered-menu-item", inputs: ["item", "focused", "highlight", "triggerEvent", "closeOnClick"] }, { kind: "component", type: TieredMenuDividerComponent, selector: "s-tiered-menu-divider" }] });
331
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TieredMenuNestedComponent, isStandalone: true, selector: "ng-component", host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown": "onKeydownHandler($event)" } }, ngImport: i0, template: "<div class=\"menu menu--nested\">\n <ng-container\n *ngTemplateOutlet=\"itemsTemplate; context: { $implicit: items }\"\n ></ng-container>\n</div>\n\n<ng-template\n #itemsTemplate\n let-items\n>\n @for (item of items; track item.id) {\n <div>\n @if (item.visible && !item.divider) {\n <s-tiered-menu-item\n [item]=\"item\"\n [focused]=\"item === tieredMenuService.currentItem\"\n [closeOnClick]=\"true\"\n >\n </s-tiered-menu-item>\n }\n @if (item.divider) {\n <s-tiered-menu-divider></s-tiered-menu-divider>\n }\n @if (item.submenu && item.isOpen) {\n <div>\n <div class=\"submenu\">\n <ng-container\n *ngTemplateOutlet=\"\n itemsTemplate;\n context: { $implicit: item.submenu }\n \"\n ></ng-container>\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:flex;flex-direction:column;width:calc(100vw - 8px);padding:4px 0;overflow:auto}.menu .submenu{margin-left:24px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TieredMenuItemComponent, selector: "s-tiered-menu-item", inputs: ["item", "focused", "highlight", "triggerEvent", "closeOnClick"] }, { kind: "component", type: TieredMenuDividerComponent, selector: "s-tiered-menu-divider" }] });
309
332
  }
310
333
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuNestedComponent, decorators: [{
311
334
  type: Component,
312
- args: [{ standalone: true, imports: [NgStyle, NgTemplateOutlet, TieredMenuItemComponent, TieredMenuDividerComponent], template: "<div\n class=\"menu menu--nested\"\n [ngStyle]=\"{\n top: top + 'px',\n left: left + 'px',\n }\"\n>\n <ng-container\n *ngTemplateOutlet=\"itemsTemplate; context: { $implicit: items }\"\n ></ng-container>\n</div>\n\n<ng-template\n #itemsTemplate\n let-items\n>\n @for (item of items; track item.id) {\n <div>\n @if (item.visible && !item.divider) {\n <s-tiered-menu-item\n [item]=\"item\"\n [focused]=\"item === tieredMenuService.currentItem\"\n [closeOnClick]=\"true\"\n >\n </s-tiered-menu-item>\n }\n @if (item.divider) {\n <s-tiered-menu-divider></s-tiered-menu-divider>\n }\n @if (item.submenu && item.isOpen) {\n <div>\n <div class=\"submenu\">\n <ng-container\n *ngTemplateOutlet=\"\n itemsTemplate;\n context: { $implicit: item.submenu }\n \"\n ></ng-container>\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:inline-flex;flex-direction:column;min-width:176px;padding:4px 0;position:absolute;z-index:9999;overflow:auto;width:calc(100vw - 8px)}.menu .submenu{margin-left:24px}\n"] }]
313
- }], propDecorators: { onResize: [{
314
- type: HostListener,
315
- args: ['window:resize']
316
- }], onDocumentClick: [{
335
+ args: [{ standalone: true, imports: [NgTemplateOutlet, TieredMenuItemComponent, TieredMenuDividerComponent], template: "<div class=\"menu menu--nested\">\n <ng-container\n *ngTemplateOutlet=\"itemsTemplate; context: { $implicit: items }\"\n ></ng-container>\n</div>\n\n<ng-template\n #itemsTemplate\n let-items\n>\n @for (item of items; track item.id) {\n <div>\n @if (item.visible && !item.divider) {\n <s-tiered-menu-item\n [item]=\"item\"\n [focused]=\"item === tieredMenuService.currentItem\"\n [closeOnClick]=\"true\"\n >\n </s-tiered-menu-item>\n }\n @if (item.divider) {\n <s-tiered-menu-divider></s-tiered-menu-divider>\n }\n @if (item.submenu && item.isOpen) {\n <div>\n <div class=\"submenu\">\n <ng-container\n *ngTemplateOutlet=\"\n itemsTemplate;\n context: { $implicit: item.submenu }\n \"\n ></ng-container>\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:flex;flex-direction:column;width:calc(100vw - 8px);padding:4px 0;overflow:auto}.menu .submenu{margin-left:24px}\n"] }]
336
+ }], propDecorators: { onDocumentClick: [{
317
337
  type: HostListener,
318
338
  args: ['document:click', ['$event']]
319
339
  }], onKeydownHandler: [{
@@ -322,25 +342,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
322
342
  }] } });
323
343
 
324
344
  class TieredMenuComponent {
325
- _appRef = inject(ApplicationRef);
326
- _componentFactoryResolver = inject(ComponentFactoryResolver);
345
+ _overlay = inject(Overlay);
346
+ _elementRef = inject(ElementRef);
327
347
  _injector = inject(Injector);
348
+ _focusTrapFactory = inject(FocusTrapFactory);
328
349
  _changeDetectorRef = inject(ChangeDetectorRef);
329
350
  tieredMenuService = inject(TieredMenuService);
330
351
  _tieredMenuEventService = inject(TieredMenuEventService);
331
352
  ariaLabel;
332
353
  items = [];
333
- top = 0;
334
- left = 0;
335
354
  menuTriggerEvent = 'hover';
336
355
  isSubmenu = false;
356
+ _overlayRef = null;
337
357
  _componentRef = null;
338
- _menuDivElement = null;
358
+ _focusTrap = null;
339
359
  _unsubscribe$ = new Subject();
340
360
  destroyRequest = new EventEmitter();
341
- onResize() {
342
- this._tieredMenuEventService.closeAllMenusEvent.emit();
343
- }
344
361
  onDocumentClick(event) {
345
362
  // Closing menu when clicked outside.
346
363
  const target = event.target;
@@ -352,12 +369,10 @@ class TieredMenuComponent {
352
369
  onKeydownHandler(event) {
353
370
  switch (event.key) {
354
371
  case 'Escape':
372
+ this._tieredMenuEventService.restoreFocusEvent.emit();
355
373
  this._tieredMenuEventService.closeAllMenusEvent.emit();
356
374
  event.preventDefault();
357
375
  break;
358
- case 'Tab':
359
- this._tieredMenuEventService.closeAllMenusEvent.emit();
360
- break;
361
376
  case ' ':
362
377
  case 'Enter':
363
378
  if (this.tieredMenuService.currentItem &&
@@ -377,6 +392,17 @@ class TieredMenuComponent {
377
392
  event.preventDefault();
378
393
  event.stopImmediatePropagation();
379
394
  break;
395
+ case 'Tab':
396
+ // Move focus between items instead of letting Tab escape the (trapped) menu.
397
+ if (event.shiftKey) {
398
+ this._tieredMenuEventService.decrementCurrentItemEvent.emit();
399
+ }
400
+ else {
401
+ this._tieredMenuEventService.incrementCurrentItemEvent.emit();
402
+ }
403
+ event.preventDefault();
404
+ event.stopImmediatePropagation();
405
+ break;
380
406
  case 'ArrowLeft':
381
407
  if (this.tieredMenuService.currentItem && this.items.includes(this.tieredMenuService.currentItem)) {
382
408
  this._tieredMenuEventService.closeItemMenuEvent.emit(this.tieredMenuService.currentItem);
@@ -397,11 +423,11 @@ class TieredMenuComponent {
397
423
  }
398
424
  ngOnInit() {
399
425
  this._subscribeEvents();
426
+ this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
400
427
  // Submenus have their focus managed by the openItemMenuEvent handler.
401
428
  if (!this.isSubmenu) {
402
429
  this.tieredMenuService.currentItems = this.items;
403
430
  this.tieredMenuService.currentItem = this.items[0] ?? null;
404
- disableScroll();
405
431
  requestAnimationFrame(() => {
406
432
  this._focusCurrentItem();
407
433
  });
@@ -410,6 +436,7 @@ class TieredMenuComponent {
410
436
  ngOnDestroy() {
411
437
  this._unsubscribe$.next();
412
438
  this._unsubscribe$.complete();
439
+ this._focusTrap?.destroy();
413
440
  }
414
441
  _focusCurrentItem() {
415
442
  const id = this.tieredMenuService.currentItem?.id;
@@ -459,66 +486,74 @@ class TieredMenuComponent {
459
486
  this._decrementCurItem();
460
487
  }
461
488
  }
462
- _createMenu(items, position) {
463
- if (!this._componentRef && items) {
464
- const componentFactory = this._componentFactoryResolver.resolveComponentFactory(TieredMenuComponent);
465
- this._componentRef = componentFactory.create(this._injector);
466
- this._appRef.attachView(this._componentRef.hostView);
467
- const domElem = this._componentRef.hostView.rootNodes[0];
468
- document.body.appendChild(domElem);
489
+ static SUBMENU_POSITIONS = [
490
+ { originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top' },
491
+ { originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top' },
492
+ { originX: 'end', originY: 'bottom', overlayX: 'start', overlayY: 'bottom' },
493
+ { originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
494
+ ];
495
+ _createMenu(items, itemEl) {
496
+ if (!this._overlayRef && items) {
497
+ const positionStrategy = this._overlay
498
+ .position()
499
+ .flexibleConnectedTo(itemEl)
500
+ .withPositions(TieredMenuComponent.SUBMENU_POSITIONS)
501
+ .withFlexibleDimensions(false)
502
+ .withPush(false);
503
+ this._overlayRef = this._overlay.create({
504
+ positionStrategy,
505
+ scrollStrategy: this._overlay.scrollStrategies.reposition(),
506
+ });
507
+ const componentRef = this._overlayRef.attach(new ComponentPortal(TieredMenuComponent, null, this._injector));
508
+ this._componentRef = componentRef;
469
509
  // Setting the menu items.
470
- this._componentRef.instance.items = items;
471
- this._componentRef.instance.isSubmenu = true;
510
+ componentRef.instance.items = items;
511
+ componentRef.instance.isSubmenu = true;
512
+ // Render synchronously so item elements are queryable right away (needed by restoreOpenChain()).
513
+ componentRef.changeDetectorRef.detectChanges();
472
514
  // Subscribe menu events.
473
- this._componentRef.instance.destroyRequest.subscribe((propagate) => {
515
+ componentRef.instance.destroyRequest.subscribe((propagate) => {
474
516
  this._destroy(propagate);
475
517
  });
476
- this._menuDivElement = domElem.querySelector('.menu');
477
- this._setMenuPosition(position);
478
518
  }
479
519
  }
520
+ /**
521
+ * Recreates the flyout chain for whichever item is currently marked `isOpen` in `items`,
522
+ * recursively — used when switching from the nested/mobile menu back to desktop, where the
523
+ * open state lives on the shared item data but each tier is a separate overlay that must be
524
+ * explicitly recreated to match it.
525
+ */
526
+ restoreOpenChain() {
527
+ const openItem = this.items.find((item) => item.isOpen && item.submenu);
528
+ if (!openItem)
529
+ return;
530
+ const itemEl = document.querySelector(`#${openItem.id}`);
531
+ if (!itemEl)
532
+ return;
533
+ this._createMenu(openItem.submenu, itemEl);
534
+ this._componentRef?.instance.restoreOpenChain();
535
+ }
536
+ /**
537
+ * Disposes this tier's own submenu overlay chain (if any) without touching `isOpen` state —
538
+ * used when switching from desktop to nested, where the open state must survive but the
539
+ * flyout overlays themselves need to go away.
540
+ */
541
+ disposeChain() {
542
+ this._componentRef?.instance.disposeChain();
543
+ this._overlayRef?.dispose();
544
+ this._overlayRef = null;
545
+ this._componentRef = null;
546
+ }
480
547
  _destroy(propagate = true) {
481
- if (this._componentRef !== null) {
482
- this._appRef.detachView(this._componentRef.hostView);
483
- this._componentRef.destroy();
548
+ if (this._overlayRef !== null) {
549
+ this._overlayRef.dispose();
550
+ this._overlayRef = null;
484
551
  this._componentRef = null;
485
- this._menuDivElement = null;
486
552
  }
487
553
  if (propagate) {
488
554
  this.destroyRequest.emit();
489
555
  }
490
556
  }
491
- _setMenuPosition(position) {
492
- const ITEM_HEIGHT = 37;
493
- const DIVIDER_HEIGHT = 5;
494
- const PADDING = 8;
495
- if (this._componentRef !== null) {
496
- const { top, right, bottom, left } = position;
497
- const itemsCount = this._componentRef.instance.items?.reduce((count, item) => {
498
- return !item.divider ? count + 1 : count;
499
- }, 0);
500
- const dividersCount = this._componentRef.instance.items?.reduce((count, item) => {
501
- return item.divider ? count + 1 : count;
502
- }, 0);
503
- // I need to calculate the height of the component because the internal elements have not been created yet.
504
- const menuHeight = itemsCount * ITEM_HEIGHT + dividersCount * DIVIDER_HEIGHT + PADDING + 8;
505
- const menuWidth = this._menuDivElement?.getBoundingClientRect().width ?? 0;
506
- const rightFreeSpace = window.innerWidth - right;
507
- const bottomFreeSpace = window.innerHeight - bottom;
508
- if (rightFreeSpace > menuWidth) {
509
- this._componentRef.instance.left = right;
510
- }
511
- else {
512
- this._componentRef.instance.left = left - menuWidth;
513
- }
514
- if (bottomFreeSpace <= menuHeight) {
515
- this._componentRef.instance.top = Math.max(window.innerHeight - menuHeight, window.scrollY);
516
- }
517
- else {
518
- this._componentRef.instance.top = window.scrollY + top;
519
- }
520
- }
521
- }
522
557
  _subscribeEvents() {
523
558
  // Increment current item event.
524
559
  this._tieredMenuEventService.incrementCurrentItemEvent.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
@@ -550,7 +585,6 @@ class TieredMenuComponent {
550
585
  this.tieredMenuService.currentItem = null;
551
586
  this.tieredMenuService.currentItems = this.tieredMenuService.items;
552
587
  this.tieredMenuService.markAllItemsAsClosed(this.tieredMenuService.items);
553
- enableScroll();
554
588
  });
555
589
  // Open item menu event.
556
590
  this._tieredMenuEventService.openItemMenuEvent
@@ -581,9 +615,7 @@ class TieredMenuComponent {
581
615
  const itemEl = document.querySelector(`#${item.id}`);
582
616
  if (!itemEl)
583
617
  return;
584
- const { top, right, left, bottom } = itemEl.getBoundingClientRect();
585
- const position = { top, right, left, bottom };
586
- this._createMenu(item.submenu, position);
618
+ this._createMenu(item.submenu, itemEl);
587
619
  this.tieredMenuService.currentItems = item.submenu;
588
620
  this.tieredMenuService.currentItem = item.submenu[0];
589
621
  item.isOpen = true;
@@ -602,9 +634,6 @@ class TieredMenuComponent {
602
634
  if (item.parent) {
603
635
  item.parent.isOpen = false;
604
636
  }
605
- else {
606
- enableScroll();
607
- }
608
637
  this.tieredMenuService.currentItems = item?.parent?.parent?.submenu || this.tieredMenuService.items;
609
638
  this.tieredMenuService.currentItem = item.parent;
610
639
  this.destroyRequest.emit(false);
@@ -615,18 +644,15 @@ class TieredMenuComponent {
615
644
  });
616
645
  }
617
646
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
618
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TieredMenuComponent, isStandalone: true, selector: "s-tiered-menu", inputs: { ariaLabel: "ariaLabel" }, outputs: { destroyRequest: "destroyRequest" }, host: { listeners: { "window:resize": "onResize()", "document:click": "onDocumentClick($event)", "document:keydown": "onKeydownHandler($event)" } }, ngImport: i0, template: "<div\r\n class=\"menu\"\r\n role=\"application\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n [ngStyle]=\"{\r\n left: left + 'px',\r\n top: top + 'px',\r\n }\"\r\n>\r\n @for (item of items; track $index) {\r\n @if (item.visible && !item.divider) {\r\n <s-tiered-menu-item\r\n [item]=\"item\"\r\n [focused]=\"item === tieredMenuService.currentItem\"\r\n [highlight]=\"item.isOpen\"\r\n fieldUtils.triggerEvent=\"hover\"\r\n [closeOnClick]=\"false\"\r\n />\r\n }\r\n @if (item.divider) {\r\n <s-tiered-menu-divider />\r\n }\r\n }\r\n</div>\r\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:inline-flex;flex-direction:column;max-height:100vh;min-width:176px;padding:4px 0;overflow-y:auto;position:absolute;z-index:9999}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: TieredMenuItemComponent, selector: "s-tiered-menu-item", inputs: ["item", "focused", "highlight", "triggerEvent", "closeOnClick"] }, { kind: "component", type: TieredMenuDividerComponent, selector: "s-tiered-menu-divider" }] });
647
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TieredMenuComponent, isStandalone: true, selector: "s-tiered-menu", inputs: { ariaLabel: "ariaLabel" }, outputs: { destroyRequest: "destroyRequest" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown": "onKeydownHandler($event)" } }, ngImport: i0, template: "<div\r\n class=\"menu\"\r\n role=\"application\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n>\r\n @for (item of items; track $index) {\r\n @if (item.visible && !item.divider) {\r\n <s-tiered-menu-item\r\n [item]=\"item\"\r\n [focused]=\"item === tieredMenuService.currentItem\"\r\n [highlight]=\"item.isOpen\"\r\n fieldUtils.triggerEvent=\"hover\"\r\n [closeOnClick]=\"false\"\r\n />\r\n }\r\n @if (item.divider) {\r\n <s-tiered-menu-divider />\r\n }\r\n }\r\n</div>\r\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:inline-flex;flex-direction:column;max-height:100vh;min-width:200px;padding:4px 0;overflow-y:auto}\n"], dependencies: [{ kind: "component", type: TieredMenuItemComponent, selector: "s-tiered-menu-item", inputs: ["item", "focused", "highlight", "triggerEvent", "closeOnClick"] }, { kind: "component", type: TieredMenuDividerComponent, selector: "s-tiered-menu-divider" }] });
619
648
  }
620
649
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuComponent, decorators: [{
621
650
  type: Component,
622
- args: [{ selector: 's-tiered-menu', standalone: true, imports: [NgStyle, TieredMenuItemComponent, TieredMenuDividerComponent], template: "<div\r\n class=\"menu\"\r\n role=\"application\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n [ngStyle]=\"{\r\n left: left + 'px',\r\n top: top + 'px',\r\n }\"\r\n>\r\n @for (item of items; track $index) {\r\n @if (item.visible && !item.divider) {\r\n <s-tiered-menu-item\r\n [item]=\"item\"\r\n [focused]=\"item === tieredMenuService.currentItem\"\r\n [highlight]=\"item.isOpen\"\r\n fieldUtils.triggerEvent=\"hover\"\r\n [closeOnClick]=\"false\"\r\n />\r\n }\r\n @if (item.divider) {\r\n <s-tiered-menu-divider />\r\n }\r\n }\r\n</div>\r\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:inline-flex;flex-direction:column;max-height:100vh;min-width:176px;padding:4px 0;overflow-y:auto;position:absolute;z-index:9999}\n"] }]
651
+ args: [{ selector: 's-tiered-menu', standalone: true, imports: [TieredMenuItemComponent, TieredMenuDividerComponent], template: "<div\r\n class=\"menu\"\r\n role=\"application\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n>\r\n @for (item of items; track $index) {\r\n @if (item.visible && !item.divider) {\r\n <s-tiered-menu-item\r\n [item]=\"item\"\r\n [focused]=\"item === tieredMenuService.currentItem\"\r\n [highlight]=\"item.isOpen\"\r\n fieldUtils.triggerEvent=\"hover\"\r\n [closeOnClick]=\"false\"\r\n />\r\n }\r\n @if (item.divider) {\r\n <s-tiered-menu-divider />\r\n }\r\n }\r\n</div>\r\n", styles: [".menu{background-color:#fff;border:1px solid #ccc;border-radius:6px;box-shadow:0 2px 12px #0000001a;display:inline-flex;flex-direction:column;max-height:100vh;min-width:200px;padding:4px 0;overflow-y:auto}\n"] }]
623
652
  }], propDecorators: { ariaLabel: [{
624
653
  type: Input
625
654
  }], destroyRequest: [{
626
655
  type: Output
627
- }], onResize: [{
628
- type: HostListener,
629
- args: ['window:resize']
630
656
  }], onDocumentClick: [{
631
657
  type: HostListener,
632
658
  args: ['document:click', ['$event']]
@@ -650,8 +676,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
650
676
  */
651
677
  class TieredMenuDirective {
652
678
  _elementRef = inject(ElementRef);
653
- _appRef = inject(ApplicationRef);
654
- _componentFactoryResolver = inject(ComponentFactoryResolver);
679
+ _overlay = inject(Overlay);
655
680
  _injector = inject(Injector);
656
681
  _tieredMenuEventService = inject(TieredMenuEventService);
657
682
  _tieredMenuService = inject(TieredMenuService);
@@ -679,6 +704,7 @@ class TieredMenuDirective {
679
704
  ariaLabel;
680
705
  tieredMenuOpened = new EventEmitter();
681
706
  tieredMenuClosed = new EventEmitter();
707
+ _overlayRef = null;
682
708
  _componentRef = null;
683
709
  _showTimeout;
684
710
  _isNested = false;
@@ -687,10 +713,6 @@ class TieredMenuDirective {
687
713
  _lastActiveElement;
688
714
  _previousItems = [];
689
715
  _itemsAreValid;
690
- _resizeObserver = new ResizeObserver(() => {
691
- this._setMenuPosition();
692
- this._componentRef?.hostView.detectChanges();
693
- });
694
716
  ngOnInit() {
695
717
  this._itemsAreValid = !!this.items?.length;
696
718
  if (!this._itemsAreValid)
@@ -739,6 +761,28 @@ class TieredMenuDirective {
739
761
  event.stopPropagation();
740
762
  }
741
763
  }
764
+ onWindowResize() {
765
+ if (!this._overlayRef)
766
+ return;
767
+ const isNestedNow = document.body.clientWidth < 600;
768
+ if (isNestedNow === this._isNested)
769
+ return;
770
+ // Crossing the 600px breakpoint while open switches between the desktop (flyout) and
771
+ // nested (fullscreen accordion) menu components. Unlike a real close, this must preserve
772
+ // which items are currently open — _disposeOverlay() only tears down the CDK overlay(s),
773
+ // it doesn't touch `isOpen` state or emit closeAllMenusEvent (which would reset it).
774
+ this._isNested = isNestedNow;
775
+ this._disposeOverlay();
776
+ this._isNested ? this._createNestedMenu() : this._createTieredMenu();
777
+ }
778
+ _disposeOverlay() {
779
+ if (this._componentRef?.instance instanceof TieredMenuComponent) {
780
+ this._componentRef.instance.disposeChain();
781
+ }
782
+ this._overlayRef?.dispose();
783
+ this._overlayRef = null;
784
+ this._componentRef = null;
785
+ }
742
786
  _createMenu() {
743
787
  this._updateServiceItems();
744
788
  if (!this._componentRef && this._tieredMenuService.items?.length > 0) {
@@ -751,102 +795,92 @@ class TieredMenuDirective {
751
795
  this._isNested ? this._createNestedMenu() : this._createTieredMenu();
752
796
  }
753
797
  }
798
+ static DESKTOP_POSITIONS = [
799
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
800
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
801
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
802
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
803
+ ];
804
+ // Single position, no flip fallback: the nested/mobile panel can accumulate several
805
+ // simultaneously open branches (restored from desktop mode) and grow much taller than the
806
+ // trigger's surroundings. Falling back to a "top" position in that case just misplaces the
807
+ // panel; instead it always opens below the trigger and relies on `withFlexibleDimensions` +
808
+ // `.menu`'s own `overflow: auto` to scroll internally when it doesn't fit.
809
+ static NESTED_POSITIONS = [
810
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
811
+ ];
754
812
  _createTieredMenu() {
755
- if (!this._componentRef && this._tieredMenuService.items?.length > 0) {
756
- this._componentRef = this._createComponent(TieredMenuComponent);
757
- this._appRef.attachView(this._componentRef.hostView);
758
- const domElem = this._componentRef.location.nativeElement;
759
- document.body.appendChild(domElem);
813
+ if (!this._overlayRef && this._tieredMenuService.items?.length > 0) {
814
+ const positionStrategy = this._overlay
815
+ .position()
816
+ .flexibleConnectedTo(this._elementRef)
817
+ .withPositions(TieredMenuDirective.DESKTOP_POSITIONS)
818
+ .withFlexibleDimensions(false)
819
+ .withPush(false);
820
+ this._overlayRef = this._overlay.create({
821
+ positionStrategy,
822
+ scrollStrategy: this._overlay.scrollStrategies.reposition(),
823
+ });
824
+ const componentRef = this._overlayRef.attach(new ComponentPortal(TieredMenuComponent, null, this._injector));
825
+ this._componentRef = componentRef;
760
826
  this._setMenuComponentProperties();
761
- this._componentRef.instance.destroyRequest.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
827
+ componentRef.instance.ariaLabel = this.ariaLabel;
828
+ // Render synchronously so item elements are queryable right away, then recreate any
829
+ // flyout chain that was already open (e.g. coming from nested/mobile mode via resize).
830
+ componentRef.changeDetectorRef.detectChanges();
831
+ componentRef.instance.restoreOpenChain();
832
+ componentRef.instance.destroyRequest.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
762
833
  this._destroy();
763
834
  });
764
- this._setMenuPosition();
765
835
  }
766
836
  }
767
837
  _createNestedMenu() {
768
- if (!this._componentRef && this._tieredMenuService.items?.length > 0) {
769
- const componentFactory = this._componentFactoryResolver.resolveComponentFactory(TieredMenuNestedComponent);
770
- this._componentRef = componentFactory.create(this._injector);
771
- this._appRef.attachView(this._componentRef.hostView);
772
- const domElem = this._componentRef.hostView.rootNodes[0];
773
- document.body.appendChild(domElem);
838
+ if (!this._overlayRef && this._tieredMenuService.items?.length > 0) {
839
+ // The nested/mobile panel is near full-width (see .menu--nested CSS), so it doesn't
840
+ // need to flip horizontally like the desktop menu — `withPush` clamps it into the
841
+ // viewport instead. `withFlexibleDimensions(true)` lets it shrink to the remaining
842
+ // viewport space below the trigger (scrolling internally via `.menu`'s own
843
+ // `overflow: auto`) instead of flipping above it, which would misplace a panel that's
844
+ // taller than the trigger's surroundings.
845
+ const positionStrategy = this._overlay
846
+ .position()
847
+ .flexibleConnectedTo(this._elementRef)
848
+ .withPositions(TieredMenuDirective.NESTED_POSITIONS)
849
+ .withFlexibleDimensions(true)
850
+ .withPush(true);
851
+ this._overlayRef = this._overlay.create({
852
+ positionStrategy,
853
+ scrollStrategy: this._overlay.scrollStrategies.reposition(),
854
+ });
855
+ this._componentRef = this._overlayRef.attach(new ComponentPortal(TieredMenuNestedComponent, null, this._injector));
774
856
  this._setMenuComponentProperties();
775
- this._setMenuPosition();
776
857
  }
777
858
  }
778
- _createComponent(componentClass) {
779
- const componentFactory = this._componentFactoryResolver.resolveComponentFactory(componentClass);
780
- const _componentRef = componentFactory.create(this._injector);
781
- this._resizeObserver.observe(_componentRef.location.nativeElement.querySelector('.menu'));
782
- _componentRef.onDestroy(() => {
783
- this._resizeObserver.disconnect();
784
- });
785
- return _componentRef;
786
- }
787
859
  _destroy() {
788
- if (this._componentRef) {
860
+ if (this._overlayRef) {
789
861
  this._isOpen = false;
790
862
  this.tieredMenuClosed.emit();
791
863
  window.clearTimeout(this._showTimeout);
792
- this._appRef.detachView(this._componentRef.hostView);
793
- this._componentRef.destroy();
864
+ this._overlayRef.dispose();
865
+ this._overlayRef = null;
794
866
  this._componentRef = null;
795
867
  this._tieredMenuService.currentItems = this._tieredMenuService.items;
796
868
  this._tieredMenuService.currentItem = this._tieredMenuService.items[0];
797
869
  this._tieredMenuEventService.closeAllMenusEvent.emit();
798
870
  }
799
871
  }
800
- _setMenuPosition() {
801
- if (!this._componentRef) {
802
- return;
803
- }
804
- this._changeDetectorRef.detectChanges();
805
- const ITEM_HEIGHT = 37;
806
- const ITEM_WIDTH = this._componentRef.location.nativeElement?.querySelector('.menu')?.clientWidth || 176;
807
- const DIVIDER_HEIGHT = 5;
808
- const PADDING = 8;
809
- const MARGIN = 4;
810
- this._componentRef.instance.top = 8;
811
- let { bottom, left, right } = this._elementRef.nativeElement.getBoundingClientRect();
812
- const itemsCount = this._componentRef.instance.items?.reduce((count, item) => {
813
- return !item.divider ? count + 1 : count;
814
- }, 0);
815
- const dividersCount = this._componentRef.instance.items?.reduce((count, item) => {
816
- return item.divider ? count + 1 : count;
817
- }, 0);
818
- const menuHeight = itemsCount * ITEM_HEIGHT + dividersCount * DIVIDER_HEIGHT + PADDING + MARGIN;
819
- const rightFreeSpace = window.innerWidth - right;
820
- const bottomFreeSpace = window.innerHeight - bottom;
821
- this._componentRef.instance.top = bottom;
822
- this._componentRef.instance.left = left;
823
- if (bottomFreeSpace <= menuHeight) {
824
- this._componentRef.instance.top = Math.max(scrollY + bottom - menuHeight, 0);
825
- }
826
- else {
827
- this._componentRef.instance.top = window.scrollY + bottom + MARGIN;
828
- }
829
- if (rightFreeSpace > 176) {
830
- this._componentRef.instance.left = window.scrollX + left;
831
- }
832
- else {
833
- this._componentRef.instance.left = window.scrollX + right - ITEM_WIDTH;
834
- }
835
- if (this._isNested) {
836
- this._componentRef.instance.left = MARGIN;
837
- }
838
- }
839
872
  _setMenuComponentProperties() {
840
873
  if (this._componentRef != null) {
841
874
  this._componentRef.instance.items = this._tieredMenuService.items;
842
- this._componentRef.instance.ariaLabel = this.ariaLabel;
843
875
  }
844
876
  }
845
877
  _subscribeEvents() {
846
878
  this._tieredMenuEventService.closeAllMenusEvent.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
847
879
  this._tieredMenuService.items = this._tieredMenuService.markAllItemsAsClosed(this._tieredMenuService.items);
848
880
  this._destroy();
849
- enableScroll();
881
+ });
882
+ this._tieredMenuEventService.restoreFocusEvent.pipe(takeUntil(this._unsubscribe$)).subscribe(() => {
883
+ this._lastActiveElement?.focus();
850
884
  });
851
885
  }
852
886
  _compareItems(item1, item2) {
@@ -863,7 +897,7 @@ class TieredMenuDirective {
863
897
  this._tieredMenuService.items[0];
864
898
  }
865
899
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
866
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: TieredMenuDirective, isStandalone: true, selector: "[sTieredMenu]", inputs: { items: "items", focusedItem: "focusedItem", triggerEvent: "triggerEvent", ariaLabel: "ariaLabel" }, outputs: { tieredMenuOpened: "tieredMenuOpened", tieredMenuClosed: "tieredMenuClosed" }, host: { listeners: { "click": "onClick($event)" } }, providers: [TieredMenuEventService, TieredMenuService], usesOnChanges: true, ngImport: i0 });
900
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: TieredMenuDirective, isStandalone: true, selector: "[sTieredMenu]", inputs: { items: "items", focusedItem: "focusedItem", triggerEvent: "triggerEvent", ariaLabel: "ariaLabel" }, outputs: { tieredMenuOpened: "tieredMenuOpened", tieredMenuClosed: "tieredMenuClosed" }, host: { listeners: { "click": "onClick($event)", "window:resize": "onWindowResize()" } }, providers: [TieredMenuEventService, TieredMenuService], usesOnChanges: true, ngImport: i0 });
867
901
  }
868
902
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TieredMenuDirective, decorators: [{
869
903
  type: Directive,
@@ -887,6 +921,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
887
921
  }], onClick: [{
888
922
  type: HostListener,
889
923
  args: ['click', ['$event']]
924
+ }], onWindowResize: [{
925
+ type: HostListener,
926
+ args: ['window:resize']
890
927
  }] } });
891
928
 
892
929
  class TieredMenuModule {