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; }) );