@ifsworld/granite-components 18.0.1 → 18.0.2-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.
@@ -664,6 +664,57 @@ const graniteMenuTouchAnimations = {
664
664
  */
665
665
  const GRANITE_MENU_PANEL = new InjectionToken('GRANITE_MENU_PANEL');
666
666
 
667
+ /**
668
+ * Utility class to manage a stack of open menus for touch devices.
669
+ * This ensures that when clicking outside or closing, only the topmost menu closes.
670
+ */
671
+ class MenuStack {
672
+ static { this.stack = []; }
673
+ /**
674
+ * Push a menu onto the stack when it opens
675
+ */
676
+ static push(menu) {
677
+ const index = this.stack.indexOf(menu);
678
+ if (index === -1) {
679
+ this.stack.push(menu);
680
+ }
681
+ }
682
+ /**
683
+ * Remove a menu from the stack when it closes
684
+ */
685
+ static pop(menu) {
686
+ const index = this.stack.indexOf(menu);
687
+ if (index !== -1) {
688
+ this.stack.splice(index, 1);
689
+ }
690
+ }
691
+ /**
692
+ * Get the topmost menu in the stack
693
+ */
694
+ static getTop() {
695
+ return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
696
+ }
697
+ /**
698
+ * Check if a menu is the topmost one
699
+ */
700
+ static isTop(menu) {
701
+ const top = this.getTop();
702
+ return top !== null && top.panelId === menu.panelId;
703
+ }
704
+ /**
705
+ * Clear the entire stack (useful for cleanup)
706
+ */
707
+ static clear() {
708
+ this.stack = [];
709
+ }
710
+ /**
711
+ * Get the current stack size
712
+ */
713
+ static size() {
714
+ return this.stack.length;
715
+ }
716
+ }
717
+
667
718
  /**
668
719
  * @license
669
720
  * Copyright Google LLC All Rights Reserved.
@@ -1153,9 +1204,18 @@ class _MenuBaseComponent {
1153
1204
  }
1154
1205
  /**
1155
1206
  * Handle click on the close button by emitting on the `closed` emitter
1156
- * without any particular reason
1207
+ * without any particular reason.
1208
+ * For touch devices, only closes if this is the topmost menu in the stack.
1157
1209
  */
1158
- _handleCloseClick() {
1210
+ _handleCloseClick(event) {
1211
+ //#region --- Touch device customizations ---
1212
+ if (this._clientOutput?.device === 'touch') {
1213
+ event?.stopPropagation();
1214
+ if (!MenuStack.isTop(this)) {
1215
+ return;
1216
+ }
1217
+ }
1218
+ //#endregion --- Touch device customizations ---
1159
1219
  this.closed.emit();
1160
1220
  }
1161
1221
  /**
@@ -1309,7 +1369,7 @@ class GraniteMenuComponent extends _MenuBaseComponent {
1309
1369
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: GraniteMenuComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1310
1370
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.9", type: GraniteMenuComponent, isStandalone: false, selector: "granite-menu", providers: [
1311
1371
  { 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: [
1372
+ ], 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
1373
  graniteMenuDesktopAnimations.transformMenuDesktop,
1314
1374
  graniteMenuTouchAnimations.transformMenuTouch,
1315
1375
  graniteMenuTouchAnimations.transformCloseButton,
@@ -1323,7 +1383,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1323
1383
  graniteMenuTouchAnimations.transformCloseButton,
1324
1384
  ], providers: [
1325
1385
  { 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"] }]
1386
+ ], 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
1387
  }] });
1328
1388
 
1329
1389
  /** Options for binding a passive event listener. */
@@ -1484,6 +1544,10 @@ class GraniteMenuTriggerForDirective {
1484
1544
  if (this._overlayRef) {
1485
1545
  //#region --- Touch device customizations ---
1486
1546
  this.removeOverlayListeners();
1547
+ // Remove from stack on destroy
1548
+ if (this.menu && this._clientOutput?.device === 'touch') {
1549
+ MenuStack.pop(this.menu);
1550
+ }
1487
1551
  //#endregion --- Touch device customizations ---
1488
1552
  this._overlayRef.dispose();
1489
1553
  this._overlayRef = null;
@@ -1507,6 +1571,8 @@ class GraniteMenuTriggerForDirective {
1507
1571
  this.menu._isClosing = true;
1508
1572
  // Get rid of the menu and tell any parent to restore its position
1509
1573
  if (this._clientOutput.device === 'touch') {
1574
+ // Remove from stack when closing
1575
+ MenuStack.pop(this.menu);
1510
1576
  // First we wait for any running animation to complete
1511
1577
  const runningAnimationDone = this.menu._isAnimating
1512
1578
  ? this.menu._animationDone
@@ -1525,7 +1591,11 @@ class GraniteMenuTriggerForDirective {
1525
1591
  this._destroyMenu();
1526
1592
  }
1527
1593
  // If a click closed the menu, we should close the entire chain of nested menus.
1528
- if ((reason === 'click' || reason === 'tab') && this._parentMenu) {
1594
+ // For touch devices, only propagate if it's not an outside click (handled by stack)
1595
+ const shouldPropagate = this._clientOutput.device !== 'touch' &&
1596
+ (reason === 'click' || reason === 'tab') &&
1597
+ this._parentMenu;
1598
+ if (shouldPropagate) {
1529
1599
  if (!this.menu.preventParentClose) {
1530
1600
  this._parentMenu.closed.emit(reason);
1531
1601
  }
@@ -1614,6 +1684,12 @@ class GraniteMenuTriggerForDirective {
1614
1684
  // Subscribe to stream that emits whenever an action that should close the menu occurs
1615
1685
  this._closingActionsSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
1616
1686
  this.animateOpenMenu();
1687
+ //#region --- Touch device customizations ---
1688
+ // Add menu to stack when opening on touch devices
1689
+ if (this._clientOutput.device === 'touch') {
1690
+ MenuStack.push(this.menu);
1691
+ }
1692
+ //#endregion --- Touch device customizations ---
1617
1693
  this._setIsMenuOpen(true);
1618
1694
  this.menu.focusFirstItem(this.openedBy || 'program');
1619
1695
  }
@@ -1873,10 +1949,16 @@ class GraniteMenuTriggerForDirective {
1873
1949
  // Note: Quick fix. Feature reportedly exists in CDK for Angular 10
1874
1950
  // Filter to prevent closing when animating added though. Applied to
1875
1951
  // root menu only.
1876
- const outsideClick = !this._parentMenu
1952
+ // For touch devices, handle outside clicks for all menus (not just root)
1953
+ // but only close the topmost menu in the stack
1954
+ const outsideClick = this._clientOutput.device === 'touch'
1877
1955
  ? fromEvent(this._document, 'click').pipe(filter((e) => e.target !== this._element.nativeElement &&
1878
- e.target.closest('.granite-menu') === null), filter(() => !this.menu._isAnimating))
1879
- : of(null);
1956
+ e.target.closest('.granite-menu') === null &&
1957
+ MenuStack.isTop(this.menu)), filter(() => !this.menu._isAnimating))
1958
+ : !this._parentMenu
1959
+ ? fromEvent(this._document, 'click').pipe(filter((e) => e.target !== this._element.nativeElement &&
1960
+ e.target.closest('.granite-menu') === null), filter(() => !this.menu._isAnimating))
1961
+ : of(null);
1880
1962
  return merge(detachments, hover, parentClose, outsideClick).pipe(filter((event) => event !== null));
1881
1963
  }
1882
1964
  /**