@ifsworld/granite-components 18.0.1 → 18.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, ElementRef, ChangeDetectionStrategy, Component, Input, ContentChildren, HostBinding, NgModule, Renderer2, InjectionToken, HostAttributeToken, HostListener, Directive, EventEmitter, ChangeDetectorRef, QueryList, TemplateRef, ContentChild, Output, ViewChild, ViewContainerRef, NgZone,
|
|
2
|
+
import { inject, ElementRef, ChangeDetectionStrategy, Component, Input, ContentChildren, HostBinding, NgModule, Renderer2, InjectionToken, Injectable, HostAttributeToken, HostListener, Directive, EventEmitter, ChangeDetectorRef, QueryList, TemplateRef, ContentChild, Output, ViewChild, ViewContainerRef, NgZone, Optional, Pipe } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
5
5
|
import { coerceNumberProperty, coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
@@ -664,6 +664,53 @@ const graniteMenuTouchAnimations = {
|
|
|
664
664
|
*/
|
|
665
665
|
const GRANITE_MENU_PANEL = new InjectionToken('GRANITE_MENU_PANEL');
|
|
666
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Stack of open touch menus so outside-click / close affects only the top
|
|
669
|
+
* panel.
|
|
670
|
+
*/
|
|
671
|
+
class GraniteMenuStackService {
|
|
672
|
+
constructor() {
|
|
673
|
+
this.stack = [];
|
|
674
|
+
}
|
|
675
|
+
/** Push a menu onto the stack when it opens */
|
|
676
|
+
push(menu) {
|
|
677
|
+
const index = this.stack.findIndex((m) => m.panelId === menu.panelId);
|
|
678
|
+
if (index === -1) {
|
|
679
|
+
this.stack.push(menu);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/** Remove a menu from the stack when it closes */
|
|
683
|
+
pop(menu) {
|
|
684
|
+
const index = this.stack.findIndex((m) => m.panelId === menu.panelId);
|
|
685
|
+
if (index !== -1) {
|
|
686
|
+
this.stack.splice(index, 1);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/** Topmost menu in the stack */
|
|
690
|
+
getTop() {
|
|
691
|
+
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
|
|
692
|
+
}
|
|
693
|
+
/** Whether the menu is the topmost one */
|
|
694
|
+
isTop(menu) {
|
|
695
|
+
const top = this.getTop();
|
|
696
|
+
return top !== null && top.panelId === menu.panelId;
|
|
697
|
+
}
|
|
698
|
+
/** Clear the stack (e.g. tests) */
|
|
699
|
+
clear() {
|
|
700
|
+
this.stack = [];
|
|
701
|
+
}
|
|
702
|
+
/** Current stack depth */
|
|
703
|
+
size() {
|
|
704
|
+
return this.stack.length;
|
|
705
|
+
}
|
|
706
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: GraniteMenuStackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
707
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: GraniteMenuStackService, providedIn: 'root' }); }
|
|
708
|
+
}
|
|
709
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: GraniteMenuStackService, decorators: [{
|
|
710
|
+
type: Injectable,
|
|
711
|
+
args: [{ providedIn: 'root' }]
|
|
712
|
+
}] });
|
|
713
|
+
|
|
667
714
|
/**
|
|
668
715
|
* @license
|
|
669
716
|
* Copyright Google LLC All Rights Reserved.
|
|
@@ -916,6 +963,7 @@ class _MenuBaseComponent {
|
|
|
916
963
|
/** Emits whenever an animation on the menu completes. */
|
|
917
964
|
this._animationDone = new Subject();
|
|
918
965
|
this._changeDetectorRef = inject(ChangeDetectorRef);
|
|
966
|
+
this._menuStack = inject(GraniteMenuStackService);
|
|
919
967
|
this._menuEmpty$ = new BehaviorSubject(false);
|
|
920
968
|
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
921
969
|
this._isMenuEmpty$ = combineLatest([
|
|
@@ -1153,9 +1201,18 @@ class _MenuBaseComponent {
|
|
|
1153
1201
|
}
|
|
1154
1202
|
/**
|
|
1155
1203
|
* Handle click on the close button by emitting on the `closed` emitter
|
|
1156
|
-
* without any particular reason
|
|
1204
|
+
* without any particular reason.
|
|
1205
|
+
* For touch devices, only closes if this is the topmost menu in the stack.
|
|
1157
1206
|
*/
|
|
1158
|
-
_handleCloseClick() {
|
|
1207
|
+
_handleCloseClick(event) {
|
|
1208
|
+
//#region --- Touch device customizations ---
|
|
1209
|
+
if (this._clientOutput?.device === 'touch') {
|
|
1210
|
+
event?.stopPropagation();
|
|
1211
|
+
if (!this._menuStack.isTop(this)) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion --- Touch device customizations ---
|
|
1159
1216
|
this.closed.emit();
|
|
1160
1217
|
}
|
|
1161
1218
|
/**
|
|
@@ -1309,7 +1366,7 @@ class GraniteMenuComponent extends _MenuBaseComponent {
|
|
|
1309
1366
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: GraniteMenuComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1310
1367
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.9", type: GraniteMenuComponent, isStandalone: false, selector: "granite-menu", providers: [
|
|
1311
1368
|
{ provide: GRANITE_MENU_PANEL, useExisting: GraniteMenuComponent },
|
|
1312
|
-
], exportAs: ["graniteMenu"], usesInheritance: true, ngImport: i0, template: "<!--\n Using separate template part for desktop and touch output, because of\n animation triggers and slightly different content.\n-->\n<ng-template>\n <!-- Desktop -->\n <ng-container *ngIf=\"_clientOutput.device === 'desktop'\">\n <div\n #menu\n class=\"granite-menu\"\n [class.is-menu-empty]=\"_isMenuEmpty$ | async\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [@transformMenuDesktop]=\"_transformMenu | async\"\n (@transformMenuDesktop.start)=\"_onAnimationStart($event)\"\n (@transformMenuDesktop.done)=\"_onAnimationDone($event)\"\n [style]=\"styles\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n </div>\n </div>\n </ng-container>\n\n <!-- Touch -->\n <ng-container *ngIf=\"_clientOutput?.device === 'touch'\">\n <div\n #menu\n class=\"granite-menu granite-device-output-touch\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [style]=\"touchStyles\"\n [@transformMenuTouch]=\"_transformMenu | async\"\n (@transformMenuTouch.start)=\"_onAnimationStart($event)\"\n (@transformMenuTouch.done)=\"_onAnimationDone($event)\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <div *ngIf=\"showTitle\" class=\"header-container\">\n <button\n [disabled]=\"!showBackButton\"\n graniteMenuTouchTitleItem\n (click)=\"_handleBackClick($event)\"\n >\n {{ title }}\n </button>\n </div>\n\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n\n <div class=\"footer-container\"></div>\n </div>\n </div>\n\n <!-- Close button -->\n <div class=\"close\" [@transformCloseButton]=\"_transformMenu | async\">\n <button\n *ngIf=\"showCloseButton\"\n graniteMenuTouchCloseItem\n (click)=\"_handleCloseClick()\"\n >\n {{ closeLabel }}\n </button>\n </div>\n </ng-container>\n\n <!--\n Content template shared between desktop and touch parts, as <ng-content>\n can't be used in two places in the same template\n -->\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n</ng-template>\n", styles: [".granite-menu:not(.granite-device-output-touch){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);min-width:7rem;overflow-x:hidden;overflow-y:hidden}.granite-menu:not(.granite-device-output-touch).ng-animating{pointer-events:none}.granite-menu:not(.granite-device-output-touch):not(.is-menu-empty){min-height:2rem}.granite-menu:not(.granite-device-output-touch):hover{overflow-y:auto}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar{width:var(--granite-spacing-4)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-thumb{background-color:var(--granite-color-border-hard);border-radius:calc(var(--granite-spacing-16) * .125)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-track{background-color:var(--granite-color-background-hover)}.granite-menu.granite-device-output-touch{background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.granite-menu.granite-device-output-touch.ng-animating{pointer-events:none}.granite-menu.granite-device-output-touch:not(.is-menu-empty){min-height:3rem}.granite-menu.granite-device-output-touch:not(.close){margin:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch.close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch .header-container{position:sticky;top:0;background-color:var(--granite-color-background-variant);z-index:1}.granite-menu.granite-device-output-touch .footer-container{position:sticky;bottom:0;height:0}.close:not(:empty){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.close:not(:empty).ng-animating{pointer-events:none}.close:not(:empty):not(.is-menu-empty){min-height:3rem}.close:not(:empty):not(.close){margin:var(--granite-spacing-4)}.close:not(:empty).close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu-custom-template{display:block}.granite-menu-custom-template:not(button){cursor:default}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: GraniteMenuTouchCloseComponent, selector: "[graniteMenuTouchCloseItem]", exportAs: ["graniteMenuTouchCloseItem"] }, { kind: "component", type: GraniteMenuTouchTitleItemComponent, selector: "[graniteMenuTouchTitleItem]", exportAs: ["graniteMenuTouchTitleItem"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], animations: [
|
|
1369
|
+
], exportAs: ["graniteMenu"], usesInheritance: true, ngImport: i0, template: "<!--\n Using separate template part for desktop and touch output, because of\n animation triggers and slightly different content.\n-->\n<ng-template>\n <!-- Desktop -->\n <ng-container *ngIf=\"_clientOutput.device === 'desktop'\">\n <div\n #menu\n class=\"granite-menu\"\n [class.is-menu-empty]=\"_isMenuEmpty$ | async\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [@transformMenuDesktop]=\"_transformMenu | async\"\n (@transformMenuDesktop.start)=\"_onAnimationStart($event)\"\n (@transformMenuDesktop.done)=\"_onAnimationDone($event)\"\n [style]=\"styles\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n </div>\n </div>\n </ng-container>\n\n <!-- Touch -->\n <ng-container *ngIf=\"_clientOutput?.device === 'touch'\">\n <div\n #menu\n class=\"granite-menu granite-device-output-touch\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [style]=\"touchStyles\"\n [@transformMenuTouch]=\"_transformMenu | async\"\n (@transformMenuTouch.start)=\"_onAnimationStart($event)\"\n (@transformMenuTouch.done)=\"_onAnimationDone($event)\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <div *ngIf=\"showTitle\" class=\"header-container\">\n <button\n [disabled]=\"!showBackButton\"\n graniteMenuTouchTitleItem\n (click)=\"_handleBackClick($event)\"\n >\n {{ title }}\n </button>\n </div>\n\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n\n <div class=\"footer-container\"></div>\n </div>\n </div>\n\n <!-- Close button -->\n <div class=\"close\" [@transformCloseButton]=\"_transformMenu | async\">\n <button\n *ngIf=\"showCloseButton\"\n graniteMenuTouchCloseItem\n (click)=\"_handleCloseClick($event)\"\n >\n {{ closeLabel }}\n </button>\n </div>\n </ng-container>\n\n <!--\n Content template shared between desktop and touch parts, as <ng-content>\n can't be used in two places in the same template\n -->\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n</ng-template>\n", styles: [".granite-menu:not(.granite-device-output-touch){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);min-width:7rem;overflow-x:hidden;overflow-y:hidden}.granite-menu:not(.granite-device-output-touch).ng-animating{pointer-events:none}.granite-menu:not(.granite-device-output-touch):not(.is-menu-empty){min-height:2rem}.granite-menu:not(.granite-device-output-touch):hover{overflow-y:auto}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar{width:var(--granite-spacing-4)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-thumb{background-color:var(--granite-color-border-hard);border-radius:calc(var(--granite-spacing-16) * .125)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-track{background-color:var(--granite-color-background-hover)}.granite-menu.granite-device-output-touch{background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.granite-menu.granite-device-output-touch.ng-animating{pointer-events:none}.granite-menu.granite-device-output-touch:not(.is-menu-empty){min-height:3rem}.granite-menu.granite-device-output-touch:not(.close){margin:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch.close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch .header-container{position:sticky;top:0;background-color:var(--granite-color-background-variant);z-index:1}.granite-menu.granite-device-output-touch .footer-container{position:sticky;bottom:0;height:0}.close:not(:empty){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.close:not(:empty).ng-animating{pointer-events:none}.close:not(:empty):not(.is-menu-empty){min-height:3rem}.close:not(:empty):not(.close){margin:var(--granite-spacing-4)}.close:not(:empty).close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu-custom-template{display:block}.granite-menu-custom-template:not(button){cursor:default}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: GraniteMenuTouchCloseComponent, selector: "[graniteMenuTouchCloseItem]", exportAs: ["graniteMenuTouchCloseItem"] }, { kind: "component", type: GraniteMenuTouchTitleItemComponent, selector: "[graniteMenuTouchTitleItem]", exportAs: ["graniteMenuTouchTitleItem"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], animations: [
|
|
1313
1370
|
graniteMenuDesktopAnimations.transformMenuDesktop,
|
|
1314
1371
|
graniteMenuTouchAnimations.transformMenuTouch,
|
|
1315
1372
|
graniteMenuTouchAnimations.transformCloseButton,
|
|
@@ -1323,7 +1380,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
1323
1380
|
graniteMenuTouchAnimations.transformCloseButton,
|
|
1324
1381
|
], providers: [
|
|
1325
1382
|
{ provide: GRANITE_MENU_PANEL, useExisting: GraniteMenuComponent },
|
|
1326
|
-
], template: "<!--\n Using separate template part for desktop and touch output, because of\n animation triggers and slightly different content.\n-->\n<ng-template>\n <!-- Desktop -->\n <ng-container *ngIf=\"_clientOutput.device === 'desktop'\">\n <div\n #menu\n class=\"granite-menu\"\n [class.is-menu-empty]=\"_isMenuEmpty$ | async\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [@transformMenuDesktop]=\"_transformMenu | async\"\n (@transformMenuDesktop.start)=\"_onAnimationStart($event)\"\n (@transformMenuDesktop.done)=\"_onAnimationDone($event)\"\n [style]=\"styles\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n </div>\n </div>\n </ng-container>\n\n <!-- Touch -->\n <ng-container *ngIf=\"_clientOutput?.device === 'touch'\">\n <div\n #menu\n class=\"granite-menu granite-device-output-touch\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [style]=\"touchStyles\"\n [@transformMenuTouch]=\"_transformMenu | async\"\n (@transformMenuTouch.start)=\"_onAnimationStart($event)\"\n (@transformMenuTouch.done)=\"_onAnimationDone($event)\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <div *ngIf=\"showTitle\" class=\"header-container\">\n <button\n [disabled]=\"!showBackButton\"\n graniteMenuTouchTitleItem\n (click)=\"_handleBackClick($event)\"\n >\n {{ title }}\n </button>\n </div>\n\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n\n <div class=\"footer-container\"></div>\n </div>\n </div>\n\n <!-- Close button -->\n <div class=\"close\" [@transformCloseButton]=\"_transformMenu | async\">\n <button\n *ngIf=\"showCloseButton\"\n graniteMenuTouchCloseItem\n (click)=\"_handleCloseClick()\"\n >\n {{ closeLabel }}\n </button>\n </div>\n </ng-container>\n\n <!--\n Content template shared between desktop and touch parts, as <ng-content>\n can't be used in two places in the same template\n -->\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n</ng-template>\n", styles: [".granite-menu:not(.granite-device-output-touch){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);min-width:7rem;overflow-x:hidden;overflow-y:hidden}.granite-menu:not(.granite-device-output-touch).ng-animating{pointer-events:none}.granite-menu:not(.granite-device-output-touch):not(.is-menu-empty){min-height:2rem}.granite-menu:not(.granite-device-output-touch):hover{overflow-y:auto}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar{width:var(--granite-spacing-4)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-thumb{background-color:var(--granite-color-border-hard);border-radius:calc(var(--granite-spacing-16) * .125)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-track{background-color:var(--granite-color-background-hover)}.granite-menu.granite-device-output-touch{background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.granite-menu.granite-device-output-touch.ng-animating{pointer-events:none}.granite-menu.granite-device-output-touch:not(.is-menu-empty){min-height:3rem}.granite-menu.granite-device-output-touch:not(.close){margin:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch.close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch .header-container{position:sticky;top:0;background-color:var(--granite-color-background-variant);z-index:1}.granite-menu.granite-device-output-touch .footer-container{position:sticky;bottom:0;height:0}.close:not(:empty){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.close:not(:empty).ng-animating{pointer-events:none}.close:not(:empty):not(.is-menu-empty){min-height:3rem}.close:not(:empty):not(.close){margin:var(--granite-spacing-4)}.close:not(:empty).close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu-custom-template{display:block}.granite-menu-custom-template:not(button){cursor:default}\n"] }]
|
|
1383
|
+
], template: "<!--\n Using separate template part for desktop and touch output, because of\n animation triggers and slightly different content.\n-->\n<ng-template>\n <!-- Desktop -->\n <ng-container *ngIf=\"_clientOutput.device === 'desktop'\">\n <div\n #menu\n class=\"granite-menu\"\n [class.is-menu-empty]=\"_isMenuEmpty$ | async\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [@transformMenuDesktop]=\"_transformMenu | async\"\n (@transformMenuDesktop.start)=\"_onAnimationStart($event)\"\n (@transformMenuDesktop.done)=\"_onAnimationDone($event)\"\n [style]=\"styles\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n </div>\n </div>\n </ng-container>\n\n <!-- Touch -->\n <ng-container *ngIf=\"_clientOutput?.device === 'touch'\">\n <div\n #menu\n class=\"granite-menu granite-device-output-touch\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [style]=\"touchStyles\"\n [@transformMenuTouch]=\"_transformMenu | async\"\n (@transformMenuTouch.start)=\"_onAnimationStart($event)\"\n (@transformMenuTouch.done)=\"_onAnimationDone($event)\"\n (click)=\"_handleClick($event)\"\n (keydown)=\"_handleKeydown($event)\"\n >\n <div class=\"granite-menu-content\">\n <div *ngIf=\"showTitle\" class=\"header-container\">\n <button\n [disabled]=\"!showBackButton\"\n graniteMenuTouchTitleItem\n (click)=\"_handleBackClick($event)\"\n >\n {{ title }}\n </button>\n </div>\n\n <ng-container [ngTemplateOutlet]=\"content\"></ng-container>\n\n <div class=\"footer-container\"></div>\n </div>\n </div>\n\n <!-- Close button -->\n <div class=\"close\" [@transformCloseButton]=\"_transformMenu | async\">\n <button\n *ngIf=\"showCloseButton\"\n graniteMenuTouchCloseItem\n (click)=\"_handleCloseClick($event)\"\n >\n {{ closeLabel }}\n </button>\n </div>\n </ng-container>\n\n <!--\n Content template shared between desktop and touch parts, as <ng-content>\n can't be used in two places in the same template\n -->\n <ng-template #content>\n <ng-content></ng-content>\n </ng-template>\n</ng-template>\n", styles: [".granite-menu:not(.granite-device-output-touch){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);min-width:7rem;overflow-x:hidden;overflow-y:hidden}.granite-menu:not(.granite-device-output-touch).ng-animating{pointer-events:none}.granite-menu:not(.granite-device-output-touch):not(.is-menu-empty){min-height:2rem}.granite-menu:not(.granite-device-output-touch):hover{overflow-y:auto}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar{width:var(--granite-spacing-4)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-thumb{background-color:var(--granite-color-border-hard);border-radius:calc(var(--granite-spacing-16) * .125)}.granite-menu:not(.granite-device-output-touch)::-webkit-scrollbar-track{background-color:var(--granite-color-background-hover)}.granite-menu.granite-device-output-touch{background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.granite-menu.granite-device-output-touch.ng-animating{pointer-events:none}.granite-menu.granite-device-output-touch:not(.is-menu-empty){min-height:3rem}.granite-menu.granite-device-output-touch:not(.close){margin:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch.close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu.granite-device-output-touch .header-container{position:sticky;top:0;background-color:var(--granite-color-background-variant);z-index:1}.granite-menu.granite-device-output-touch .footer-container{position:sticky;bottom:0;height:0}.close:not(:empty){background-color:var(--granite-color-background-variant);color:var(--granite-color-text);overflow:auto;-webkit-overflow-scrolling:touch;outline:0;-webkit-user-select:none;user-select:none;box-shadow:var(--granite-shadow-l);border-radius:.25rem}.close:not(:empty).ng-animating{pointer-events:none}.close:not(:empty):not(.is-menu-empty){min-height:3rem}.close:not(:empty):not(.close){margin:var(--granite-spacing-4)}.close:not(:empty).close{margin-inline-start:var(--granite-spacing-4);margin-inline-end:var(--granite-spacing-4);margin-block-end:var(--granite-spacing-4)}.granite-menu-custom-template{display:block}.granite-menu-custom-template:not(button){cursor:default}\n"] }]
|
|
1327
1384
|
}] });
|
|
1328
1385
|
|
|
1329
1386
|
/** Options for binding a passive event listener. */
|
|
@@ -1368,6 +1425,7 @@ class GraniteMenuTriggerForDirective {
|
|
|
1368
1425
|
});
|
|
1369
1426
|
this._dir = inject(Directionality, { optional: true });
|
|
1370
1427
|
this._focusMonitor = inject(FocusMonitor);
|
|
1428
|
+
this._menuStack = inject(GraniteMenuStackService);
|
|
1371
1429
|
this._touchTouchingElement = false;
|
|
1372
1430
|
/**
|
|
1373
1431
|
* Handles touch start events on the trigger.
|
|
@@ -1484,6 +1542,10 @@ class GraniteMenuTriggerForDirective {
|
|
|
1484
1542
|
if (this._overlayRef) {
|
|
1485
1543
|
//#region --- Touch device customizations ---
|
|
1486
1544
|
this.removeOverlayListeners();
|
|
1545
|
+
// Remove from stack on destroy
|
|
1546
|
+
if (this.menu && this._clientOutput?.device === 'touch') {
|
|
1547
|
+
this._menuStack.pop(this.menu);
|
|
1548
|
+
}
|
|
1487
1549
|
//#endregion --- Touch device customizations ---
|
|
1488
1550
|
this._overlayRef.dispose();
|
|
1489
1551
|
this._overlayRef = null;
|
|
@@ -1507,6 +1569,8 @@ class GraniteMenuTriggerForDirective {
|
|
|
1507
1569
|
this.menu._isClosing = true;
|
|
1508
1570
|
// Get rid of the menu and tell any parent to restore its position
|
|
1509
1571
|
if (this._clientOutput.device === 'touch') {
|
|
1572
|
+
// Remove from stack when closing
|
|
1573
|
+
this._menuStack.pop(this.menu);
|
|
1510
1574
|
// First we wait for any running animation to complete
|
|
1511
1575
|
const runningAnimationDone = this.menu._isAnimating
|
|
1512
1576
|
? this.menu._animationDone
|
|
@@ -1525,7 +1589,11 @@ class GraniteMenuTriggerForDirective {
|
|
|
1525
1589
|
this._destroyMenu();
|
|
1526
1590
|
}
|
|
1527
1591
|
// If a click closed the menu, we should close the entire chain of nested menus.
|
|
1528
|
-
|
|
1592
|
+
// For touch devices, only propagate if it's not an outside click (handled by stack)
|
|
1593
|
+
const shouldPropagate = this._clientOutput.device !== 'touch' &&
|
|
1594
|
+
(reason === 'click' || reason === 'tab') &&
|
|
1595
|
+
this._parentMenu;
|
|
1596
|
+
if (shouldPropagate) {
|
|
1529
1597
|
if (!this.menu.preventParentClose) {
|
|
1530
1598
|
this._parentMenu.closed.emit(reason);
|
|
1531
1599
|
}
|
|
@@ -1614,6 +1682,12 @@ class GraniteMenuTriggerForDirective {
|
|
|
1614
1682
|
// Subscribe to stream that emits whenever an action that should close the menu occurs
|
|
1615
1683
|
this._closingActionsSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
|
|
1616
1684
|
this.animateOpenMenu();
|
|
1685
|
+
//#region --- Touch device customizations ---
|
|
1686
|
+
// Add menu to stack when opening on touch devices
|
|
1687
|
+
if (this._clientOutput.device === 'touch') {
|
|
1688
|
+
this._menuStack.push(this.menu);
|
|
1689
|
+
}
|
|
1690
|
+
//#endregion --- Touch device customizations ---
|
|
1617
1691
|
this._setIsMenuOpen(true);
|
|
1618
1692
|
this.menu.focusFirstItem(this.openedBy || 'program');
|
|
1619
1693
|
}
|
|
@@ -1873,10 +1947,16 @@ class GraniteMenuTriggerForDirective {
|
|
|
1873
1947
|
// Note: Quick fix. Feature reportedly exists in CDK for Angular 10
|
|
1874
1948
|
// Filter to prevent closing when animating added though. Applied to
|
|
1875
1949
|
// root menu only.
|
|
1876
|
-
|
|
1950
|
+
// For touch devices, handle outside clicks for all menus (not just root)
|
|
1951
|
+
// but only close the topmost menu in the stack
|
|
1952
|
+
const outsideClick = this._clientOutput.device === 'touch'
|
|
1877
1953
|
? fromEvent(this._document, 'click').pipe(filter((e) => e.target !== this._element.nativeElement &&
|
|
1878
|
-
e.target.closest('.granite-menu') === null
|
|
1879
|
-
|
|
1954
|
+
e.target.closest('.granite-menu') === null &&
|
|
1955
|
+
this._menuStack.isTop(this.menu)), filter(() => !this.menu._isAnimating))
|
|
1956
|
+
: !this._parentMenu
|
|
1957
|
+
? fromEvent(this._document, 'click').pipe(filter((e) => e.target !== this._element.nativeElement &&
|
|
1958
|
+
e.target.closest('.granite-menu') === null), filter(() => !this.menu._isAnimating))
|
|
1959
|
+
: of(null);
|
|
1880
1960
|
return merge(detachments, hover, parentClose, outsideClick).pipe(filter((event) => event !== null));
|
|
1881
1961
|
}
|
|
1882
1962
|
/**
|