Skip to content

Commit 15ee9de

Browse files
authored
feat(nav-drawer): use popover API for top layer without z-index hassle (#16913)
1 parent b7b1ff2 commit 15ee9de

8 files changed

Lines changed: 118 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ All notable changes for each version of this project will be documented in this
4545
<igx-icon [igxTooltipTarget]="tooltipRef" [showTriggers]="'click,focus'" [hideTriggers]="'keypress,blur'">info</igx-icon>
4646
<span #tooltipRef="tooltip" igxTooltip>Hello there, I am a tooltip!</span>
4747
```
48+
- `IgxNavigationDrawer` - Integrated HTML Popover API to place overlay elements when not pinned in the top layer, eliminating z-index stacking issues.
4849

4950
### General
5051

projects/igniteui-angular/core/src/core/styles/components/navdrawer/_navdrawer-theme.scss

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,19 @@
7171
min-height: 100%;
7272
overflow-x: hidden;
7373
background: var-get($theme, 'background');
74-
top: 0;
75-
bottom: 0;
74+
inset: 0 auto;
7675
width: var(--igx-nav-drawer-size);
7776
inset-inline-start: 0;
78-
z-index: 999;
7977
transition: width, padding, transform;
8078
transition-timing-function: $in-out-quad;
8179
box-shadow: var-get($theme, 'elevation');
8280
padding: $aside-padding;
8381

82+
// additional popover overrides:
83+
margin: 0;
84+
border: none;
85+
display: block;
86+
8487
@if $variant != 'fluent' {
8588
border-inline-end: rem(1px) solid var-get($theme, 'border-color');
8689
} @else {
@@ -98,7 +101,6 @@
98101
%aside--pinned {
99102
position: relative;
100103
box-shadow: none;
101-
z-index: 0;
102104
}
103105

104106
%aside--collapsed--right {
@@ -178,11 +180,16 @@
178180
transition-delay: 0s, 0s;
179181
position: absolute;
180182
inset-inline-start: 0;
181-
top: 0;
183+
inset: 0;
182184
width: 100%;
183185
height: 100%;
184186
visibility: visible;
185-
z-index: 999;
187+
188+
// additional popover overrides:
189+
margin: 0;
190+
padding: 0;
191+
border: none;
192+
display: block;
186193
}
187194

188195
%overlay-panning {

projects/igniteui-angular/navigation-drawer/src/navigation-drawer/navigation-drawer.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
</ng-template>
1010

1111
<div [hidden]="pin"
12+
[attr.popover]="pin ? null : 'manual'"
1213
class="igx-nav-drawer__overlay"
1314
[class.igx-nav-drawer__overlay--hidden]="!isOpen"
1415
[class.igx-nav-drawer--disable-animation]="disableAnimation"
1516
(click)="close()" #overlay>
1617
</div>
1718
<nav
1819
class="igx-nav-drawer__aside"
20+
[attr.popover]="pin ? null : 'manual'"
1921
[class.igx-nav-drawer__aside--collapsed]="!miniTemplate && !isOpen"
2022
[class.igx-nav-drawer__aside--mini]="miniTemplate && !isOpen"
2123
[class.igx-nav-drawer__aside--normal]="!miniTemplate || isOpen"

projects/igniteui-angular/navigation-drawer/src/navigation-drawer/navigation-drawer.component.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ describe('Navigation Drawer', () => {
8585
expect(fixture.componentInstance.navDrawer.styleDummy.classList).toContain('igx-nav-drawer__style-dummy');
8686
expect(fixture.componentInstance.navDrawer.hasAnimateWidth).toBeFalsy();
8787

88+
expect(fixture.componentInstance.navDrawer.drawer.getAttribute('popover')).toBe('manual');
89+
expect(fixture.componentInstance.navDrawer.overlay.getAttribute('popover')).toBe('manual');
90+
8891
fixture.componentInstance.navDrawer.id = 'customNavDrawer';
8992
fixture.detectChanges();
9093

@@ -198,6 +201,7 @@ describe('Navigation Drawer', () => {
198201
expect(fixture.componentInstance.navDrawer.hasAnimateWidth).toBeTruthy();
199202
expect(fixture.debugElement.query(By.css('.igx-nav-drawer__aside')).nativeElement.classList)
200203
.toContain('igx-nav-drawer__aside--mini');
204+
expect(fixture.componentInstance.navDrawer.drawer.getAttribute('popover')).toBe('manual');
201205
}).catch((reason) => Promise.reject(reason));
202206
}));
203207

@@ -259,6 +263,8 @@ describe('Navigation Drawer', () => {
259263
expect(fixture.componentInstance.navDrawer.pin).toBeTruthy();
260264
expect(fixture.debugElement.query(By.css('.igx-nav-drawer__aside')).nativeElement.classList)
261265
.toContain('igx-nav-drawer__aside--pinned');
266+
expect(fixture.componentInstance.navDrawer.drawer.getAttribute('popover')).toBeNull();
267+
expect(fixture.componentInstance.navDrawer.overlay.getAttribute('popover')).toBeNull();
262268

263269
expect(fixture.componentInstance.navDrawer.enableGestures).toBe(false);
264270

@@ -272,6 +278,7 @@ describe('Navigation Drawer', () => {
272278
it('should stay at 100% parent height when pinned', waitForAsync(() => {
273279
const template = `<div style="height: 100%; position: relative;">
274280
<igx-nav-drawer
281+
[isOpen]="true"
275282
[pin]="pin"
276283
pinThreshold="false"
277284
[enableGestures]="enableGestures">
@@ -291,10 +298,12 @@ describe('Navigation Drawer', () => {
291298
fixture.componentInstance.pin = false;
292299
fixture.detectChanges();
293300
expect(navdrawer.clientHeight).toEqual(windowHeight);
301+
expect(navdrawer.getAttribute('popover')).toBe('manual');
294302

295303
fixture.componentInstance.pin = true;
296304
fixture.detectChanges();
297305
expect(navdrawer.clientHeight).toEqual(container.clientHeight);
306+
expect(navdrawer.getAttribute('popover')).toBeNull();
298307

299308
container.style.height = `${windowHeight - 50}px`;
300309
fixture.detectChanges();
@@ -304,6 +313,7 @@ describe('Navigation Drawer', () => {
304313
fixture.componentInstance.pin = false;
305314
fixture.detectChanges();
306315
expect(navdrawer.clientHeight).toEqual(windowHeight);
316+
expect(navdrawer.getAttribute('popover')).toBe('manual');
307317
});
308318
}));
309319

@@ -499,6 +509,9 @@ describe('Navigation Drawer', () => {
499509
// Standard default:
500510
expect(asideWidth).toBe('240px');
501511

512+
// Not pinned and open: popover should be shown
513+
expect(asideElem.matches(':popover-open')).toBeTruthy();
514+
502515
// Change sizes:
503516
fixture.componentInstance.drawerMiniWidth = '80px';
504517
fixture.componentInstance.drawerWidth = '250px';
@@ -637,6 +650,37 @@ describe('Navigation Drawer', () => {
637650
expect(navbarEl.offsetLeft).toEqual(parseInt(flexBasis));
638651
});
639652

653+
it('should not hide popover during closing animation', async () => {
654+
const template = `<igx-nav-drawer pinThreshold="false"></igx-nav-drawer>`;
655+
TestBed.overrideComponent(TestComponentDIComponent, {
656+
set: { template }
657+
});
658+
await TestBed.compileComponents();
659+
const fixture = TestBed.createComponent(TestComponentDIComponent);
660+
fixture.detectChanges();
661+
const drawer = fixture.componentInstance.navDrawer;
662+
663+
drawer.open();
664+
fixture.detectChanges();
665+
await wait(50);
666+
667+
// Begin close - _closingAnimation should prevent immediate popover hide
668+
drawer.close();
669+
fixture.detectChanges();
670+
await wait(50);
671+
672+
// The drawer popover should still be showing during the closing animation
673+
expect(drawer.isOpen).toBeFalsy();
674+
expect(drawer.drawer.matches(':popover-open')).toBeTruthy();
675+
676+
// Complete the transition
677+
fixture.debugElement.children[0].nativeElement.dispatchEvent(new Event('transitionend'));
678+
fixture.detectChanges();
679+
await wait(50);
680+
681+
expect(drawer.drawer.matches(':popover-open')).toBeFalsy();
682+
});
683+
640684
const swipe = (element, posX, posY, duration, deltaX, deltaY) => {
641685
const swipeOptions = {
642686
deltaX,

projects/igniteui-angular/navigation-drawer/src/navigation-drawer/navigation-drawer.component.ts

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AfterContentInit, Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange, ViewChild, Renderer2, booleanAttribute, inject } from '@angular/core';
1+
import { AfterContentInit, afterRenderEffect, Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange, ViewChild, Renderer2, booleanAttribute, inject, signal, computed } from '@angular/core';
22
import { fromEvent, interval, Subscription } from 'rxjs';
33
import { debounce } from 'rxjs/operators';
44
import { IgxNavigationService, IToggleView } from 'igniteui-angular/core';
@@ -49,10 +49,18 @@ export class IgxNavigationDrawerComponent implements
4949
OnChanges {
5050
private elementRef = inject<ElementRef>(ElementRef);
5151
private _state = inject(IgxNavigationService, { optional: true });
52-
protected renderer = inject(Renderer2);
52+
private renderer = inject(Renderer2);
5353
private _touchManager = inject(HammerGesturesManager);
5454
private platformUtil = inject(PlatformUtil);
5555

56+
private _isOpen = signal(false);
57+
private _pinned = signal(false);
58+
private _miniTemplate = signal<IgxNavDrawerMiniTemplateDirective | undefined>(undefined);
59+
private _visibleOverlay = computed<boolean>(() => {
60+
return !this._pinned() && (this._isOpen() || !!this._miniTemplate());
61+
});
62+
private _closingAnimation = signal(false);
63+
5664

5765
/** @hidden @internal */
5866
@HostBinding('class.igx-nav-drawer')
@@ -140,7 +148,14 @@ export class IgxNavigationDrawerComponent implements
140148
* <igx-nav-drawer [pin]="false"></igx-nav-drawer>
141149
* ```
142150
*/
143-
@Input({ transform: booleanAttribute }) public pin = false;
151+
@Input({ transform: booleanAttribute })
152+
public get pin(): boolean {
153+
return this._pinned();
154+
}
155+
public set pin(v: boolean) {
156+
this._pinned.set(v);
157+
}
158+
144159

145160
/**
146161
* Width of the drawer in its open state.
@@ -237,11 +252,9 @@ export class IgxNavigationDrawerComponent implements
237252
@ContentChild(IgxNavDrawerTemplateDirective, { read: IgxNavDrawerTemplateDirective })
238253
protected contentTemplate: IgxNavDrawerTemplateDirective;
239254

240-
@ViewChild('aside', { static: true }) private _drawer: ElementRef;
241-
@ViewChild('overlay', { static: true }) private _overlay: ElementRef;
242-
@ViewChild('dummy', { static: true }) private _styleDummy: ElementRef;
243-
244-
private _isOpen = false;
255+
@ViewChild('aside', { static: true }) private _drawer: ElementRef<HTMLElement>;
256+
@ViewChild('overlay', { static: true }) private _overlay: ElementRef<HTMLElement>;
257+
@ViewChild('dummy', { static: true }) private _styleDummy: ElementRef<HTMLElement>;
245258

246259
/**
247260
* State of the drawer.
@@ -264,11 +277,11 @@ export class IgxNavigationDrawerComponent implements
264277
*/
265278
@Input({ transform: booleanAttribute })
266279
public get isOpen() {
267-
return this._isOpen;
280+
return this._isOpen();
268281
}
269282
public set isOpen(value) {
270-
this._isOpen = value;
271-
this.isOpenChange.emit(this._isOpen);
283+
this._isOpen.set(value);
284+
this.isOpenChange.emit(value);
272285
}
273286

274287
/**
@@ -290,27 +303,25 @@ export class IgxNavigationDrawerComponent implements
290303
return this.contentTemplate.template;
291304
}
292305
}
293-
294-
private _miniTemplate: IgxNavDrawerMiniTemplateDirective;
295306
/**
296307
* @hidden
297308
*/
298309
public get miniTemplate(): IgxNavDrawerMiniTemplateDirective {
299-
return this._miniTemplate;
310+
return this._miniTemplate();
300311
}
301312

302313
/**
303314
* @hidden
304315
*/
305316
@ContentChild(IgxNavDrawerMiniTemplateDirective, { read: IgxNavDrawerMiniTemplateDirective })
306317
public set miniTemplate(v: IgxNavDrawerMiniTemplateDirective) {
307-
this._miniTemplate = v;
318+
this._miniTemplate.set(v);
308319
}
309320

310321
/** @hidden @internal */
311322
@HostBinding('class.igx-nav-drawer--mini')
312323
public get isMini(): boolean {
313-
return !!this._miniTemplate && !this.isOpen;
324+
return !!this.miniTemplate && !this.isOpen;
314325
}
315326

316327
/** @hidden @internal */
@@ -361,14 +372,14 @@ export class IgxNavigationDrawerComponent implements
361372
* @hidden
362373
*/
363374
public get drawer() {
364-
return this._drawer.nativeElement;
375+
return this._drawer?.nativeElement;
365376
}
366377

367378
/**
368379
* @hidden
369380
*/
370381
public get overlay() {
371-
return this._overlay.nativeElement;
382+
return this._overlay?.nativeElement;
372383
}
373384

374385
/**
@@ -440,6 +451,13 @@ export class IgxNavigationDrawerComponent implements
440451
return this._state;
441452
}
442453

454+
constructor() {
455+
afterRenderEffect(() => {
456+
if (this._closingAnimation()) return;
457+
this.togglePopover(this._visibleOverlay());
458+
});
459+
}
460+
443461
/**
444462
* @hidden
445463
*/
@@ -572,6 +590,8 @@ export class IgxNavigationDrawerComponent implements
572590

573591
this.closing.emit();
574592

593+
// TODO: intersection observer for close popover instead of transitionend
594+
this._closingAnimation.set(true);
575595
this.isOpen = false;
576596
this.elementRef.nativeElement.addEventListener('transitionend', this.toggleClosedEvent, false);
577597
}
@@ -827,13 +847,30 @@ export class IgxNavigationDrawerComponent implements
827847
});
828848
}
829849

850+
private togglePopover(show: boolean) {
851+
// check element and functionality exist:
852+
if (typeof this.drawer?.showPopover !== 'function') return;
853+
854+
const popoverOpen = this.drawer.matches(':popover-open');
855+
if (show === popoverOpen) return;
856+
857+
if (show) {
858+
this.overlay.showPopover();
859+
this.drawer.showPopover();
860+
} else {
861+
this.overlay.hidePopover();
862+
this.drawer.hidePopover();
863+
}
864+
}
865+
830866
private toggleOpenedEvent = () => {
831867
this.elementRef.nativeElement.removeEventListener('transitionend', this.toggleOpenedEvent, false);
832868
this.opened.emit();
833869
};
834870

835871
private toggleClosedEvent = () => {
836872
this.elementRef.nativeElement.removeEventListener('transitionend', this.toggleClosedEvent, false);
873+
this._closingAnimation.set(false);
837874
this.closed.emit();
838875
};
839876
}

src/app/app.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div class="main">
22
<igx-nav-drawer #navdrawer
33
[enableGestures]="drawerState.enableGestures" id="navdrawer"
4-
[isOpen]="drawerState.open"
4+
[(isOpen)]="drawerState.open"
55
[(pin)]="drawerState.pin" [position]="drawerState.position">
66

77
<ng-template igxDrawer>

src/app/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class AppComponent implements OnInit {
6767

6868
public drawerState = {
6969
enableGestures: true,
70-
open: true,
70+
open: signal(true),
7171
pin: false,
7272
pinThreshold: 768,
7373
position: 'left',

src/app/navdrawer/navdrawer.sample.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ <h4 class="sample-title">Settings</h4>
44
<article>
55
<h5 class="setting-title">Visibility</h5>
66
<div class="setting-container">
7-
<igx-switch (ngModelChange)="toggle()" [checked]="this.app.navdrawer.isOpen" [(ngModel)]="this.app.drawerState.open">
7+
<igx-switch [(ngModel)]="this.app.drawerState.open">
88
Show Navdrawer
99
</igx-switch>
1010
</div>

0 commit comments

Comments
 (0)