@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.
- package/bignumber-input/lib/bignumber-input/bignumber-input.directive.d.ts +2 -1
- package/code-editor/lib/code-editor/core/facades/core-facade.d.ts +3 -1
- package/code-editor/lib/code-editor/infra/cores/codemirror-6/codemirror-6-core.d.ts +12 -2
- package/drawer/README.md +119 -0
- package/drawer/index.d.ts +5 -0
- package/{sidebar/lib/sidebar/sidebar.component.d.ts → drawer/lib/drawer/drawer.component.d.ts} +18 -15
- package/drawer/lib/drawer/drawer.module.d.ts +8 -0
- package/drawer/public-api.d.ts +2 -0
- package/esm2022/bignumber-input/lib/bignumber-input/bignumber-input.directive.mjs +6 -3
- package/esm2022/code-editor/lib/code-editor/core/facades/core-facade.mjs +7 -1
- package/esm2022/code-editor/lib/code-editor/infra/cores/codemirror-6/codemirror-6-core.mjs +47 -22
- package/esm2022/drawer/lib/drawer/drawer.component.mjs +192 -0
- package/esm2022/drawer/lib/drawer/drawer.module.mjs +17 -0
- package/esm2022/drawer/public-api.mjs +3 -0
- package/esm2022/drawer/seniorsistemas-angular-components-drawer.mjs +5 -0
- package/esm2022/dynamic-form/dynamic-form/form-field/fields/bignumber/bignumber-field.component.mjs +4 -5
- package/esm2022/lib/locale/fallback.mjs +3 -2
- package/esm2022/select/lib/select/select.component.mjs +11 -14
- package/esm2022/side-sheet/lib/side-sheet/models/side-sheet-config.mjs +2 -0
- package/esm2022/side-sheet/lib/side-sheet/models/side-sheet-size.mjs +2 -0
- package/esm2022/side-sheet/lib/side-sheet/services/side-sheet.service.mjs +82 -0
- package/esm2022/side-sheet/lib/side-sheet/side-sheet.component.mjs +171 -0
- package/esm2022/side-sheet/public-api.mjs +3 -0
- package/esm2022/side-sheet/seniorsistemas-angular-components-side-sheet.mjs +5 -0
- package/esm2022/sidebar/public-api.mjs +12 -3
- package/esm2022/switch/lib/switch/switch.component.mjs +5 -3
- package/esm2022/switch/lib/switch/switch.module.mjs +2 -2
- package/esm2022/tabs/lib/tab-item/tab-item.component.mjs +4 -3
- package/esm2022/tabs/lib/tabs/tabs.component.mjs +14 -10
- package/esm2022/tiered-menu/lib/tiered-menu/components/tiered-menu/tiered-menu.component.mjs +82 -75
- package/esm2022/tiered-menu/lib/tiered-menu/components/tiered-menu-item/tiered-menu-item.component.mjs +3 -3
- package/esm2022/tiered-menu/lib/tiered-menu/components/tiered-menu-nested/tiered-menu-nested.component.mjs +35 -18
- package/esm2022/tiered-menu/lib/tiered-menu/services/tiered-menu.event.service.mjs +2 -1
- package/esm2022/tiered-menu/lib/tiered-menu/tiered-menu.directive.mjs +89 -77
- package/esm2022/toast/lib/toast/toast.service.mjs +2 -2
- package/fesm2022/seniorsistemas-angular-components-bignumber-input.mjs +5 -2
- package/fesm2022/seniorsistemas-angular-components-bignumber-input.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-code-editor.mjs +52 -21
- package/fesm2022/seniorsistemas-angular-components-code-editor.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-drawer.mjs +211 -0
- package/fesm2022/seniorsistemas-angular-components-drawer.mjs.map +1 -0
- package/fesm2022/seniorsistemas-angular-components-dynamic-form.mjs +3 -4
- package/fesm2022/seniorsistemas-angular-components-dynamic-form.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-select.mjs +10 -13
- package/fesm2022/seniorsistemas-angular-components-select.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-side-sheet.mjs +257 -0
- package/fesm2022/seniorsistemas-angular-components-side-sheet.mjs.map +1 -0
- package/fesm2022/seniorsistemas-angular-components-sidebar.mjs +10 -180
- package/fesm2022/seniorsistemas-angular-components-sidebar.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-switch.mjs +5 -3
- package/fesm2022/seniorsistemas-angular-components-switch.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-tabs.mjs +16 -11
- package/fesm2022/seniorsistemas-angular-components-tabs.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-tiered-menu.mjs +201 -164
- package/fesm2022/seniorsistemas-angular-components-tiered-menu.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components-toast.mjs +1 -1
- package/fesm2022/seniorsistemas-angular-components-toast.mjs.map +1 -1
- package/fesm2022/seniorsistemas-angular-components.mjs +2 -1
- package/fesm2022/seniorsistemas-angular-components.mjs.map +1 -1
- package/package.json +19 -7
- package/select/lib/select/select.component.d.ts +2 -1
- package/side-sheet/README.md +184 -0
- package/side-sheet/index.d.ts +5 -0
- package/side-sheet/lib/side-sheet/models/side-sheet-config.d.ts +20 -0
- package/side-sheet/lib/side-sheet/models/side-sheet-size.d.ts +1 -0
- package/side-sheet/lib/side-sheet/services/side-sheet.service.d.ts +49 -0
- package/side-sheet/lib/side-sheet/side-sheet.component.d.ts +61 -0
- package/side-sheet/public-api.d.ts +4 -0
- package/sidebar/README.md +20 -93
- package/sidebar/public-api.d.ts +11 -2
- package/tabs/lib/tab-item/tab-item.component.d.ts +1 -0
- package/tabs/lib/tabs/tabs.component.d.ts +2 -1
- package/tiered-menu/lib/tiered-menu/components/tiered-menu/tiered-menu.component.d.ts +19 -7
- package/tiered-menu/lib/tiered-menu/components/tiered-menu-nested/tiered-menu-nested.component.d.ts +4 -3
- package/tiered-menu/lib/tiered-menu/services/tiered-menu.event.service.d.ts +1 -0
- package/tiered-menu/lib/tiered-menu/tiered-menu.directive.d.ts +6 -5
- package/esm2022/sidebar/lib/sidebar/sidebar.component.mjs +0 -169
- package/esm2022/sidebar/lib/sidebar/sidebar.module.mjs +0 -17
- 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,
|
|
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 {
|
|
6
|
-
import { NgClass,
|
|
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
|
-
|
|
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
|
-
|
|
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: { "
|
|
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: [
|
|
313
|
-
}], propDecorators: {
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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.
|
|
482
|
-
this.
|
|
483
|
-
this.
|
|
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
|
-
|
|
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: { "
|
|
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: [
|
|
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
|
-
|
|
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.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
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.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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.
|
|
860
|
+
if (this._overlayRef) {
|
|
789
861
|
this._isOpen = false;
|
|
790
862
|
this.tieredMenuClosed.emit();
|
|
791
863
|
window.clearTimeout(this._showTimeout);
|
|
792
|
-
this.
|
|
793
|
-
this.
|
|
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
|
-
|
|
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 {
|