@ifsworld/granite-components 18.0.2-beta.1 → 18.1.0-beta.2

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, Injectable, Optional, Pipe } from '@angular/core';
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';
@@ -665,55 +665,51 @@ const graniteMenuTouchAnimations = {
665
665
  const GRANITE_MENU_PANEL = new InjectionToken('GRANITE_MENU_PANEL');
666
666
 
667
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.
668
+ * Stack of open touch menus so outside-click / close affects only the top
669
+ * panel.
670
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);
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
678
  if (index === -1) {
679
679
  this.stack.push(menu);
680
680
  }
681
681
  }
682
- /**
683
- * Remove a menu from the stack when it closes
684
- */
685
- static pop(menu) {
686
- const index = this.stack.indexOf(menu);
682
+ /** Remove a menu from the stack when it closes */
683
+ pop(menu) {
684
+ const index = this.stack.findIndex((m) => m.panelId === menu.panelId);
687
685
  if (index !== -1) {
688
686
  this.stack.splice(index, 1);
689
687
  }
690
688
  }
691
- /**
692
- * Get the topmost menu in the stack
693
- */
694
- static getTop() {
689
+ /** Topmost menu in the stack */
690
+ getTop() {
695
691
  return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
696
692
  }
697
- /**
698
- * Check if a menu is the topmost one
699
- */
700
- static isTop(menu) {
693
+ /** Whether the menu is the topmost one */
694
+ isTop(menu) {
701
695
  const top = this.getTop();
702
696
  return top !== null && top.panelId === menu.panelId;
703
697
  }
704
- /**
705
- * Clear the entire stack (useful for cleanup)
706
- */
707
- static clear() {
698
+ /** Clear the stack (e.g. tests) */
699
+ clear() {
708
700
  this.stack = [];
709
701
  }
710
- /**
711
- * Get the current stack size
712
- */
713
- static size() {
702
+ /** Current stack depth */
703
+ size() {
714
704
  return this.stack.length;
715
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' }); }
716
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
+ }] });
717
713
 
718
714
  /**
719
715
  * @license
@@ -966,7 +962,10 @@ class _MenuBaseComponent {
966
962
  this._transformMenu = new BehaviorSubject(transformMenuDefault);
967
963
  /** Emits whenever an animation on the menu completes. */
968
964
  this._animationDone = new Subject();
965
+ /** Whether this menu participates in the menu stack. */
966
+ this._usesMenuStack = false;
969
967
  this._changeDetectorRef = inject(ChangeDetectorRef);
968
+ this._menuStack = inject(GraniteMenuStackService);
970
969
  this._menuEmpty$ = new BehaviorSubject(false);
971
970
  // eslint-disable-next-line @typescript-eslint/member-ordering
972
971
  this._isMenuEmpty$ = combineLatest([
@@ -1211,7 +1210,7 @@ class _MenuBaseComponent {
1211
1210
  //#region --- Touch device customizations ---
1212
1211
  if (this._clientOutput?.device === 'touch') {
1213
1212
  event?.stopPropagation();
1214
- if (!MenuStack.isTop(this)) {
1213
+ if (this._usesMenuStack && !this._menuStack.isTop(this)) {
1215
1214
  return;
1216
1215
  }
1217
1216
  }
@@ -1404,6 +1403,7 @@ class GraniteMenuTriggerForDirective {
1404
1403
  // Tracking input type is necessary so it's possible to only auto-focus
1405
1404
  // the first item of the list when the menu is opened via the keyboard
1406
1405
  this.openedBy = null;
1406
+ this._isInCustomTemplate = false;
1407
1407
  this._hoverSubscription = Subscription.EMPTY;
1408
1408
  this._menuCloseSubscription = Subscription.EMPTY;
1409
1409
  this._closingActionsSubscription = Subscription.EMPTY;
@@ -1428,6 +1428,7 @@ class GraniteMenuTriggerForDirective {
1428
1428
  });
1429
1429
  this._dir = inject(Directionality, { optional: true });
1430
1430
  this._focusMonitor = inject(FocusMonitor);
1431
+ this._menuStack = inject(GraniteMenuStackService);
1431
1432
  this._touchTouchingElement = false;
1432
1433
  /**
1433
1434
  * Handles touch start events on the trigger.
@@ -1544,9 +1545,11 @@ class GraniteMenuTriggerForDirective {
1544
1545
  if (this._overlayRef) {
1545
1546
  //#region --- Touch device customizations ---
1546
1547
  this.removeOverlayListeners();
1547
- // Remove from stack on destroy
1548
- if (this.menu && this._clientOutput?.device === 'touch') {
1549
- MenuStack.pop(this.menu);
1548
+ if (this.menu &&
1549
+ this._clientOutput?.device === 'touch' &&
1550
+ this._isInCustomTemplate) {
1551
+ this._menuStack.pop(this.menu);
1552
+ this.menu._usesMenuStack = false;
1550
1553
  }
1551
1554
  //#endregion --- Touch device customizations ---
1552
1555
  this._overlayRef.dispose();
@@ -1571,8 +1574,10 @@ class GraniteMenuTriggerForDirective {
1571
1574
  this.menu._isClosing = true;
1572
1575
  // Get rid of the menu and tell any parent to restore its position
1573
1576
  if (this._clientOutput.device === 'touch') {
1574
- // Remove from stack when closing
1575
- MenuStack.pop(this.menu);
1577
+ if (this._isInCustomTemplate) {
1578
+ this._menuStack.pop(this.menu);
1579
+ this.menu._usesMenuStack = false;
1580
+ }
1576
1581
  // First we wait for any running animation to complete
1577
1582
  const runningAnimationDone = this.menu._isAnimating
1578
1583
  ? this.menu._animationDone
@@ -1590,11 +1595,11 @@ class GraniteMenuTriggerForDirective {
1590
1595
  //#endregion --- Touch device customizations ---
1591
1596
  this._destroyMenu();
1592
1597
  }
1593
- // If a click closed the menu, we should close the entire chain of nested menus.
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
+ const isTouch = this._clientOutput.device === 'touch';
1599
+ const shouldPropagate = this._parentMenu &&
1600
+ (isTouch
1601
+ ? !this._isInCustomTemplate && reason !== 'keydown'
1602
+ : reason === 'click' || reason === 'tab');
1598
1603
  if (shouldPropagate) {
1599
1604
  if (!this.menu.preventParentClose) {
1600
1605
  this._parentMenu.closed.emit(reason);
@@ -1616,12 +1621,11 @@ class GraniteMenuTriggerForDirective {
1616
1621
  this.menu.parentMenu = this.triggersSubmenu()
1617
1622
  ? this._parentMenu
1618
1623
  : undefined;
1619
- // Check if the menu is in a custom template
1620
- if (this._parentMenu && this._viewContainerRef?.element?.nativeElement) {
1621
- const isInCustomTemplate = this._viewContainerRef.element.nativeElement.closest('.granite-menu-custom-template');
1622
- if (isInCustomTemplate) {
1623
- this.menu.preventParentClose = true;
1624
- }
1624
+ const isInCustomTemplate = !!(this._parentMenu &&
1625
+ this._viewContainerRef?.element?.nativeElement?.closest('.granite-menu-custom-template'));
1626
+ this._isInCustomTemplate = isInCustomTemplate;
1627
+ if (isInCustomTemplate) {
1628
+ this.menu.preventParentClose = true;
1625
1629
  }
1626
1630
  this.menu.direction = this._dir.value === 'rtl' ? 'rtl' : 'ltr';
1627
1631
  if (this._parentMenu) {
@@ -1647,6 +1651,12 @@ class GraniteMenuTriggerForDirective {
1647
1651
  if (this._clientOutput.device === 'touch') {
1648
1652
  panelClass.push('granite-overlay-pane-fill-width-bottom');
1649
1653
  }
1654
+ if (isInCustomTemplate) {
1655
+ panelClass.push('from-custom-template');
1656
+ }
1657
+ if (this.menu.customTemplate) {
1658
+ panelClass.push('has-custom-template');
1659
+ }
1650
1660
  // setting scrollStrategy options to overlay
1651
1661
  const scrollStrategy = this.setScrollStrategyToOverylay(this._clientOutput.device, this.menu.scrollStrategy);
1652
1662
  const hasBackdrop = this._clientOutput.device === 'touch' && !this.triggersSubmenu();
@@ -1674,6 +1684,18 @@ class GraniteMenuTriggerForDirective {
1674
1684
  if (this._clientOutput.device === 'touch') {
1675
1685
  this.menu._panelAnimationState = 'void';
1676
1686
  }
1687
+ if (isInCustomTemplate) {
1688
+ this._overlayRef.addPanelClass('from-custom-template');
1689
+ }
1690
+ else {
1691
+ this._overlayRef.removePanelClass('from-custom-template');
1692
+ }
1693
+ if (this.menu.customTemplate) {
1694
+ this._overlayRef.addPanelClass('has-custom-template');
1695
+ }
1696
+ else {
1697
+ this._overlayRef.removePanelClass('has-custom-template');
1698
+ }
1677
1699
  }
1678
1700
  // Create portal from associated menu's template
1679
1701
  if (!this._portal || this._portal.templateRef !== this.menu.templateRef) {
@@ -1685,9 +1707,10 @@ class GraniteMenuTriggerForDirective {
1685
1707
  this._closingActionsSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
1686
1708
  this.animateOpenMenu();
1687
1709
  //#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);
1710
+ // Add menu to stack when opening on touch devices from custom template
1711
+ if (this._clientOutput.device === 'touch' && isInCustomTemplate) {
1712
+ this._menuStack.push(this.menu);
1713
+ this.menu._usesMenuStack = true;
1691
1714
  }
1692
1715
  //#endregion --- Touch device customizations ---
1693
1716
  this._setIsMenuOpen(true);
@@ -1951,13 +1974,20 @@ class GraniteMenuTriggerForDirective {
1951
1974
  // root menu only.
1952
1975
  // For touch devices, handle outside clicks for all menus (not just root)
1953
1976
  // but only close the topmost menu in the stack
1954
- const outsideClick = this._clientOutput.device === 'touch'
1955
- ? fromEvent(this._document, 'click').pipe(filter((e) => e.target !== this._element.nativeElement &&
1956
- e.target.closest('.granite-menu') === null &&
1957
- MenuStack.isTop(this.menu)), filter(() => !this.menu._isAnimating))
1977
+ const isTouchCustom = this._clientOutput.device === 'touch' && this._isInCustomTemplate;
1978
+ const outsideClick = isTouchCustom
1979
+ ? fromEvent(this._document, 'click').pipe(filter((e) => {
1980
+ const el = e.target;
1981
+ return (el !== this._element.nativeElement &&
1982
+ el.closest('.granite-menu') === null &&
1983
+ this._menuStack.isTop(this.menu));
1984
+ }), filter(() => !this.menu._isAnimating))
1958
1985
  : !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))
1986
+ ? fromEvent(this._document, 'click').pipe(filter((e) => {
1987
+ const el = e.target;
1988
+ return (el !== this._element.nativeElement &&
1989
+ el.closest('.granite-menu') === null);
1990
+ }), filter(() => !this.menu._isAnimating), filter(() => !this.menu.customTemplate || this._menuStack.size() === 0))
1961
1991
  : of(null);
1962
1992
  return merge(detachments, hover, parentClose, outsideClick).pipe(filter((event) => event !== null));
1963
1993
  }