diff --git a/.vscode/settings.json b/.vscode/settings.json
index d4cfe13c..bfdf84c8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -23,7 +23,7 @@
"editor.defaultFormatter": "vscode.html-language-features"
},
"[scss]": {
- "editor.defaultFormatter": "sibiraj-s.vscode-scss-formatter"
+ "editor.defaultFormatter": "HookyQR.beautify"
},
"npm.exclude": "**/{dist,tmp}{,/**}",
"npm.scriptExplorerExclude": [
diff --git a/projects/demo-app/src/app/app.component.html b/projects/demo-app/src/app/app.component.html
index 0f5bda97..b0ecf3fc 100644
--- a/projects/demo-app/src/app/app.component.html
+++ b/projects/demo-app/src/app/app.component.html
@@ -1 +1,31 @@
-Hello World
+
+
+
+ @hug/ngx-components Demo
+
+
+
+
+
+
+ menu
+ Overlay
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/demo-app/src/app/app.component.scss b/projects/demo-app/src/app/app.component.scss
index e69de29b..40d009e5 100644
--- a/projects/demo-app/src/app/app.component.scss
+++ b/projects/demo-app/src/app/app.component.scss
@@ -0,0 +1,33 @@
+:host {
+ .demo-container {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
+ .demo-is-mobile .demo-toolbar {
+ position: fixed;
+ /* Make sure the toolbar will stay on top of the content as it scrolls past. */
+ z-index: 2;
+ }
+
+ h1.demo-app-name {
+ margin-left: 8px;
+ }
+
+ .demo-sidenav-container {
+ /* When the sidenav is not fixed, stretch the sidenav container to fill the available space. This
+ causes `` to act as our scrolling element for desktop layouts. */
+ flex: 1;
+ }
+
+ .demo-is-mobile .demo-sidenav-container {
+ /* When the sidenav is fixed, don't constrain the height of the sidenav container. This allows the
+ `` to be our scrolling element for mobile layouts. */
+ flex: 1 0 auto;
+ }
+}
diff --git a/projects/demo-app/src/app/app.component.spec.ts b/projects/demo-app/src/app/app.component.spec.ts
deleted file mode 100644
index d747e171..00000000
--- a/projects/demo-app/src/app/app.component.spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { TestBed } from '@angular/core/testing';
-
-import { AppComponent } from './app.component';
-
-describe('AppComponent', () => {
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [AppComponent]
- }).compileComponents();
- });
-
- it('should create the app', () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app).toBeTruthy();
- });
-
- it('should have the \'demo-app\' title property', () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app.title).toEqual('demo-app');
- });
-
- it('should render example', () => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('b')?.textContent).toContain('Hello World');
- });
-});
diff --git a/projects/demo-app/src/app/app.component.ts b/projects/demo-app/src/app/app.component.ts
index 81e6cd67..6b247cbb 100644
--- a/projects/demo-app/src/app/app.component.ts
+++ b/projects/demo-app/src/app/app.component.ts
@@ -1,14 +1,44 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { MediaMatcher } from '@angular/cdk/layout';
+import { NgFor, NgIf } from '@angular/common';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy } from '@angular/core';
+import { MatIconModule } from '@angular/material/icon';
+import { MatListModule } from '@angular/material/list';
+import { MatSidenavModule } from '@angular/material/sidenav';
+import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterOutlet } from '@angular/router';
+
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [RouterOutlet],
templateUrl: './app.component.html',
- styleUrls: ['./app.component.scss']
+ styleUrls: ['./app.component.scss'],
+ imports: [
+ MatIconModule,
+ MatListModule,
+ MatSidenavModule,
+ MatToolbarModule,
+ NgIf,
+ NgFor,
+ RouterOutlet
+ ]
})
-export class AppComponent {
- public title = 'demo-app';
+export class AppComponent implements OnDestroy {
+ protected mobileQuery: MediaQueryList;
+
+ private _mobileQueryListener: () => void;
+
+ private changeDetectorRef = inject(ChangeDetectorRef);
+ private media = inject(MediaMatcher);
+
+ public constructor() {
+ this.mobileQuery = this.media.matchMedia('(max-width: 600px)');
+ this._mobileQueryListener = (): void => this.changeDetectorRef.detectChanges();
+ this.mobileQuery.addListener(this._mobileQueryListener);
+ }
+
+ public ngOnDestroy(): void {
+ this.mobileQuery.removeListener(this._mobileQueryListener);
+ }
}
diff --git a/projects/demo-app/src/app/app.routes.ts b/projects/demo-app/src/app/app.routes.ts
index dc39edb5..7ce24d14 100644
--- a/projects/demo-app/src/app/app.routes.ts
+++ b/projects/demo-app/src/app/app.routes.ts
@@ -1,3 +1,7 @@
import { Routes } from '@angular/router';
-export const routes: Routes = [];
+export const appRoutes: Routes = [
+ { path: '', redirectTo: 'overlay', pathMatch: 'full' },
+ { path: 'overlay', loadComponent: () => import('./overlay/overlay-demo.component').then(m => m.OverlayDemoComponent), data: { title: 'Overlay' } },
+ { path: '**', redirectTo: 'overlay', pathMatch: 'prefix' }
+];
diff --git a/projects/demo-app/src/app/overlay/overlay-demo.component.html b/projects/demo-app/src/app/overlay/overlay-demo.component.html
new file mode 100644
index 00000000..77892800
--- /dev/null
+++ b/projects/demo-app/src/app/overlay/overlay-demo.component.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+ TODO
+
+
+
+
+
+
+
+ Overlay
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/demo-app/src/app/overlay/overlay-demo.component.scss b/projects/demo-app/src/app/overlay/overlay-demo.component.scss
new file mode 100644
index 00000000..a98e8d8f
--- /dev/null
+++ b/projects/demo-app/src/app/overlay/overlay-demo.component.scss
@@ -0,0 +1,30 @@
+overlay-demo {
+ #demo-deja-menu {
+ display: flex;
+ flex-flow: row;
+
+ .menu-section {
+ width: 300px;
+ margin: 0.5rem;
+ }
+
+ .end-icon {
+ align-items: flex-end;
+ }
+ }
+}
+
+.overlay-container {
+ .deja-menu-content {
+ anchorMenu {
+ .menu-item {
+ white-space: nowrap;
+ padding: 0.5rem 2rem;
+ }
+ }
+
+ .mat-icon {
+ margin-right: 0.5rem;
+ }
+ }
+}
diff --git a/projects/demo-app/src/app/overlay/overlay-demo.component.ts b/projects/demo-app/src/app/overlay/overlay-demo.component.ts
new file mode 100644
index 00000000..8506dc0e
--- /dev/null
+++ b/projects/demo-app/src/app/overlay/overlay-demo.component.ts
@@ -0,0 +1,55 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component, ViewChild, ViewEncapsulation } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTabsModule } from '@angular/material/tabs';
+import { MatToolbarModule } from '@angular/material/toolbar';
+import { OverlayComponent } from '@hug/ngx-overlay';
+
+
+@Component({
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'app-overlay-demo',
+ styleUrls: ['./overlay-demo.component.scss'],
+ templateUrl: './overlay-demo.component.html',
+ standalone: true,
+ imports: [
+ CommonModule,
+ OverlayComponent,
+ FormsModule,
+ MatButtonModule,
+ MatCardModule,
+ MatIconModule,
+ MatTabsModule,
+ MatToolbarModule
+ ]
+})
+export class OverlayDemoComponent {
+ @ViewChild('contextMenu')
+ private contextMenu?: OverlayComponent;
+
+ public selected = '';
+ public items = [
+ { text: 'Refresh' },
+ { text: 'Settings' },
+ { text: 'Help', disabled: true },
+ { text: 'Sign Out' }
+ ];
+
+ public tabIndex = 1;
+
+ public select(text: string): void {
+ this.selected = text;
+ }
+
+ public onContextMenu(event: MouseEvent): boolean {
+ const parent = event.currentTarget as HTMLElement;
+ const parentRect = parent.getBoundingClientRect();
+ this.contextMenu?.show(event.pageX - parentRect.left, event.pageY - parentRect.top);
+ event.preventDefault();
+ return false;
+ }
+}
diff --git a/projects/demo-app/src/main.ts b/projects/demo-app/src/main.ts
index 4a61f92e..c68b56b6 100644
--- a/projects/demo-app/src/main.ts
+++ b/projects/demo-app/src/main.ts
@@ -1,8 +1,10 @@
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
+import { PreloadAllModules, provideRouter, withPreloading } from '@angular/router';
import { AppComponent } from './app/app.component';
+import { appRoutes } from './app/app.routes';
import { environment } from './environments/environment';
if (environment.production) {
@@ -10,5 +12,8 @@ if (environment.production) {
}
bootstrapApplication(AppComponent, {
- providers: [provideAnimations()]
+ providers: [
+ provideAnimations(),
+ provideRouter(appRoutes, withPreloading(PreloadAllModules))
+ ]
}).catch(err => console.error(err));
diff --git a/projects/demo-app/src/styles.scss b/projects/demo-app/src/styles.scss
index c99331b8..472dc441 100644
--- a/projects/demo-app/src/styles.scss
+++ b/projects/demo-app/src/styles.scss
@@ -44,6 +44,7 @@ html,
body {
height: 100%;
}
+
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
diff --git a/projects/overlay/src/overlay.component.ts b/projects/overlay/src/overlay.component.ts
index 4a06e4b4..84e7b18c 100644
--- a/projects/overlay/src/overlay.component.ts
+++ b/projects/overlay/src/overlay.component.ts
@@ -1,9 +1,9 @@
import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
-import { CdkConnectedOverlay, CdkOverlayOrigin, OverlayContainer, OverlayModule } from '@angular/cdk/overlay';
+import { CdkConnectedOverlay, CdkOverlayOrigin, OverlayContainer, OverlayModule, OverlayRef } from '@angular/cdk/overlay';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, Input, OnChanges, SimpleChanges, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { MediaService } from '@hug/ngx-core';
-import { BehaviorSubject, combineLatestWith, delay, distinctUntilChanged, EMPTY, map, mergeWith, Observable, of, ReplaySubject, shareReplay, startWith, Subject, switchMap } from 'rxjs';
+import { BehaviorSubject, combineLatestWith, distinctUntilChanged, EMPTY, map, mergeWith, Observable, of, ReplaySubject, shareReplay, startWith, Subject, switchMap, take } from 'rxjs';
import { defaultConnectionPositionPair, OverlayConnectionPositionPair } from './connection-position-pair';
@@ -50,11 +50,18 @@ export class OverlayComponent implements OnChanges {
@ContentChild('content') protected contentTemplate?: TemplateRef;
/** Overlay pane containing the options. */
- @ViewChild(CdkConnectedOverlay, { static: true }) private overlay?: CdkConnectedOverlay;
+ @ViewChild(CdkConnectedOverlay) protected set overlay(value: CdkConnectedOverlay | undefined) {
+ if (!value) {
+ return;
+ }
+
+ this.overlayRef$.next(value.overlayRef);
+ }
public readonly isVisible$: Observable;
protected overlayInfos$: Observable;
+ protected overlayRef$ = new ReplaySubject(1);
private show$ = new ReplaySubject(1);
private hide$ = new Subject();
@@ -115,19 +122,22 @@ export class OverlayComponent implements OnChanges {
const info$ = this.ownerElement$.pipe(
combineLatestWith(isMobile$),
- map(([ownerElement, isMobile]) => ({
- offsetX: showParams.offsetX && +showParams.offsetX || 0,
- offsetY: showParams.offsetY && +showParams.offsetY || 0,
- origin: new CdkOverlayOrigin(new ElementRef((isMobile && document.body) ?? showParams.event?.target ?? ownerElement ?? this.elementRef.nativeElement)),
- width: isMobile ? this.widthForMobile : this.width,
- context: showParams.context
- } as OverlayInfos))
+ map(([ownerElement, isMobile]) => {
+ const mobileElement = isMobile ? document.body : undefined;
+ return {
+ offsetX: showParams.offsetX && +showParams.offsetX || 0,
+ offsetY: showParams.offsetY && +showParams.offsetY || 0,
+ origin: new CdkOverlayOrigin(new ElementRef(mobileElement ?? showParams.event?.target ?? ownerElement ?? this.elementRef.nativeElement)),
+ width: isMobile ? this.widthForMobile : this.width,
+ context: showParams.context
+ } as OverlayInfos;
+ })
);
- const updatePosition$ = info$.pipe(
- delay(1),
- switchMap(() => {
- this.overlay?.overlayRef?.updatePosition();
+ const updatePosition$ = this.overlayRef$.pipe(
+ take(1),
+ switchMap(overlayRef => {
+ overlayRef.updatePosition();
return EMPTY;
})
);