From c3aa5b3cea25fd62a11767b19415044a2bba770d Mon Sep 17 00:00:00 2001 From: inga Date: Tue, 16 Apr 2024 10:55:15 +0200 Subject: [PATCH 01/50] feat: add dialog component --- apps/demo-app/src/app/app.routes.ts | 8 ++ .../dialog-sample/dialog-sample.component.css | 0 .../dialog-sample.component.html | 16 +++ .../dialog-sample.component.spec.ts | 21 ++++ .../dialog-sample/dialog-sample.component.ts | 18 +++ .../pages/overview/overview.component.html | 3 + .../app/pages/overview/overview.component.ts | 3 +- apps/demo-app/src/styles.css | 3 + libs/sketch/src/index.ts | 1 + .../components/dialog/dialog.component.css | 4 + .../components/dialog/dialog.component.html | 50 +++++++++ .../dialog/dialog.component.spec.ts | 21 ++++ .../lib/components/dialog/dialog.component.ts | 105 ++++++++++++++++++ .../dialog/service/dialog.service.spec.ts | 16 +++ .../dialog/service/dialog.service.ts | 73 ++++++++++++ src/lib/dialog.service.spec.ts | 16 +++ src/lib/dialog.service.ts | 9 ++ 17 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css create mode 100644 apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html create mode 100644 apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.spec.ts create mode 100644 apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts create mode 100644 libs/sketch/src/lib/components/dialog/dialog.component.css create mode 100644 libs/sketch/src/lib/components/dialog/dialog.component.html create mode 100644 libs/sketch/src/lib/components/dialog/dialog.component.spec.ts create mode 100644 libs/sketch/src/lib/components/dialog/dialog.component.ts create mode 100644 libs/sketch/src/lib/components/dialog/service/dialog.service.spec.ts create mode 100644 libs/sketch/src/lib/components/dialog/service/dialog.service.ts create mode 100644 src/lib/dialog.service.spec.ts create mode 100644 src/lib/dialog.service.ts diff --git a/apps/demo-app/src/app/app.routes.ts b/apps/demo-app/src/app/app.routes.ts index 0708c1d..790ef5c 100644 --- a/apps/demo-app/src/app/app.routes.ts +++ b/apps/demo-app/src/app/app.routes.ts @@ -66,4 +66,12 @@ export const appRoutes: Route[] = [ }, ], }, + { + path: 'dialog-sample', + loadComponent: () => + import('./pages/dialog-sample/dialog-sample.component').then( + (m) => m.DialogSampleComponent + ), + }, + // TODO: Add routes for the other samples ]; diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css new file mode 100644 index 0000000..e69de29 diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html new file mode 100644 index 0000000..971123b --- /dev/null +++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html @@ -0,0 +1,16 @@ + + + + + +

here comes some content

+
+
diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.spec.ts b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.spec.ts new file mode 100644 index 0000000..320f843 --- /dev/null +++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DialogSampleComponent } from './dialog-sample.component'; + +describe('DialogSampleComponent', () => { + let component: DialogSampleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DialogSampleComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DialogSampleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts new file mode 100644 index 0000000..f2473a6 --- /dev/null +++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DialogComponent } from '@qupaya/sketch'; + +@Component({ + selector: 'app-dialog-sample', + standalone: true, + imports: [CommonModule, DialogComponent], + templateUrl: './dialog-sample.component.html', + styleUrl: './dialog-sample.component.css', +}) +export class DialogSampleComponent { + isDialogOpen = false; + + openDialog(): void { + this.isDialogOpen = true; + } +} diff --git a/apps/demo-app/src/app/pages/overview/overview.component.html b/apps/demo-app/src/app/pages/overview/overview.component.html index c972fa2..df7e4f4 100644 --- a/apps/demo-app/src/app/pages/overview/overview.component.html +++ b/apps/demo-app/src/app/pages/overview/overview.component.html @@ -7,3 +7,6 @@ Lorem ipsum dolor... + + Lorem ipsum dolor... + diff --git a/apps/demo-app/src/app/pages/overview/overview.component.ts b/apps/demo-app/src/app/pages/overview/overview.component.ts index 2d606a4..219e409 100644 --- a/apps/demo-app/src/app/pages/overview/overview.component.ts +++ b/apps/demo-app/src/app/pages/overview/overview.component.ts @@ -3,11 +3,12 @@ import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; import { CardComponent } from '../../components/card/card.component'; import { SAMPLE_DATA } from '../list-sample/list-sample.data'; +import { DialogComponent } from '@qupaya/sketch'; @Component({ selector: 'app-overview', standalone: true, - imports: [CommonModule, RouterLink, CardComponent], + imports: [CommonModule, RouterLink, CardComponent, DialogComponent], templateUrl: './overview.component.html', styleUrl: './overview.component.css', }) diff --git a/apps/demo-app/src/styles.css b/apps/demo-app/src/styles.css index 8f42865..3fcfc83 100644 --- a/apps/demo-app/src/styles.css +++ b/apps/demo-app/src/styles.css @@ -1,6 +1,9 @@ @import url('@angular/cdk/overlay-prebuilt.css'); /* You can add global styles to this file, and also import other style files */ + +@import '@angular/cdk/overlay-prebuilt.css'; + :root { --sk-top-bar-height: 84px; --sk-font-family: 'Montserrat', 'Helevetika Neue', sans-serif; diff --git a/libs/sketch/src/index.ts b/libs/sketch/src/index.ts index 2b6503c..93531b4 100644 --- a/libs/sketch/src/index.ts +++ b/libs/sketch/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/components/sketch/sketch.component'; export * from './lib/components/select'; export * from './lib/components/list'; +export * from './lib/components/dialog/dialog.component'; \ No newline at end of file diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.css b/libs/sketch/src/lib/components/dialog/dialog.component.css new file mode 100644 index 0000000..8b2d8af --- /dev/null +++ b/libs/sketch/src/lib/components/dialog/dialog.component.css @@ -0,0 +1,4 @@ +:host { + display: block; + position: relative; +} \ No newline at end of file diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.html b/libs/sketch/src/lib/components/dialog/dialog.component.html new file mode 100644 index 0000000..b6052e9 --- /dev/null +++ b/libs/sketch/src/lib/components/dialog/dialog.component.html @@ -0,0 +1,50 @@ + + + diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.spec.ts b/libs/sketch/src/lib/components/dialog/dialog.component.spec.ts new file mode 100644 index 0000000..124e351 --- /dev/null +++ b/libs/sketch/src/lib/components/dialog/dialog.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DialogComponent } from './dialog.component'; + +describe('DialogComponent', () => { + let component: DialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.ts b/libs/sketch/src/lib/components/dialog/dialog.component.ts new file mode 100644 index 0000000..765b377 --- /dev/null +++ b/libs/sketch/src/lib/components/dialog/dialog.component.ts @@ -0,0 +1,105 @@ +import { + Component, + HostBinding, + TemplateRef, + ViewContainerRef, + ViewEncapsulation, + effect, + inject, + input, + output, + signal, + viewChild, + OnDestroy, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CdkTrapFocus } from '@angular/cdk/a11y'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { DialogService } from './service/dialog.service'; + +@Component({ + selector: 'sk-dialog[dialogId]', + standalone: true, + imports: [CommonModule, CdkTrapFocus, OverlayModule], + providers: [DialogService], + templateUrl: './dialog.component.html', + styleUrl: './dialog.component.css', + encapsulation: ViewEncapsulation.ShadowDom, +}) +export class DialogComponent implements OnDestroy { + public static readonly defaultBackgroundClass = 'bg-[#00070ba1]'; + + private readonly dialogService = inject(DialogService); + private readonly viewContainerRef = inject(ViewContainerRef); + + readonly dialogContentTemplate = signal | undefined>( + undefined + ); + + @HostBinding('attr.data-dialog-tag') + private _dialogId!: string; + + open = input(false); + + backdropClass = input([DialogComponent.defaultBackgroundClass]); + + contentTemplate = input | null>(null); + + /** + * Whether the CDK directive cdkTrapFocusAutoCapture should automatically move focus into the trapped region upon initialization and return focus to the previous activeElement upon destruction. + * + * This can not be desired when the popup is really long and the first button is not visible at start, otherwise the popup will scroll to the first button position! + */ + readonly focusAutoCapture = input(true); + + /** + * Useful when you want to control how your content is scrolled. + */ + readonly preventContentScrolling = input(false); + + /** + * It will be appended to the rendered overlay in this way: + * ``` + * [dialogId]="my-tag-name" + * + *
+ * ``` + */ + readonly dialogId = input(''); + + readonly contentShadow = input(true); + + readonly showCloseButton = input(true); + + readonly fullscreen = input(false); + + readonly closeRequested = output(); + + private readonly contentWrapperTemplate = viewChild.required< + TemplateRef + >('contentWrapperTemplate'); + + protected readonly updateDialogId = effect( + () => (this._dialogId = this.dialogId()) + ); + + protected readonly openEvents = effect( + () => { + if (this.open()) { + this.dialogService.open({ + template: this.contentWrapperTemplate(), + viewContainerRef: this.viewContainerRef, + backdropClass: this.backdropClass(), + tag: this.dialogId(), + }); + } else { + this.dialogService.close(this.dialogId()); + } + }, + { allowSignalWrites: true } + ); + + ngOnDestroy(): void { + this.dialogService.close(this._dialogId); + } +} diff --git a/libs/sketch/src/lib/components/dialog/service/dialog.service.spec.ts b/libs/sketch/src/lib/components/dialog/service/dialog.service.spec.ts new file mode 100644 index 0000000..016507e --- /dev/null +++ b/libs/sketch/src/lib/components/dialog/service/dialog.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DialogService } from './dialog.service'; + +describe('DialogService', () => { + let service: DialogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DialogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/sketch/src/lib/components/dialog/service/dialog.service.ts b/libs/sketch/src/lib/components/dialog/service/dialog.service.ts new file mode 100644 index 0000000..0954299 --- /dev/null +++ b/libs/sketch/src/lib/components/dialog/service/dialog.service.ts @@ -0,0 +1,73 @@ +import { + Injectable, + Optional, + ViewContainerRef, + TemplateRef, +} from '@angular/core'; +import { + Overlay, + OverlayRef, + ScrollStrategyOptions, +} from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; + +interface OpenDialogParams { + template: TemplateRef; + viewContainerRef: ViewContainerRef; + backdropClass: string[]; + tag: string; +} + +@Injectable() +export class DialogService { + static dialogIdName = 'data-dialog-tag'; + + constructor( + @Optional() private readonly overlay: Overlay, + private readonly scrollStrategyOptions: ScrollStrategyOptions + ) { + if (!overlay) { + throw new Error( + 'The Dialog service needs that you inject OverlayModule into your (Lazy load module)' + ); + } + } + + private readonly overlaysMap = new Map(); + + open(params: OpenDialogParams): void { + if (this.overlaysMap.has(params.tag)) { + throw Error( + `The Dialog service has already an overlay with the same tag "${params.tag}"` + ); + } + const overlayRef = this.overlay.create({ + hasBackdrop: true, + backdropClass: params.backdropClass, + width: '100%', + height: '100%', + disposeOnNavigation: true, + scrollStrategy: this.scrollStrategyOptions.block(), + }); + const dialogPortal = new TemplatePortal( + params.template, + params.viewContainerRef + ); + overlayRef.attach(dialogPortal); + overlayRef.hostElement.setAttribute(DialogService.dialogIdName, params.tag); + this.overlaysMap.set(params.tag, overlayRef); + } + + close(tag: string): void { + if (this.overlaysMap.size > 0) { + if (this.overlaysMap.has(tag)) { + const overlay = this.overlaysMap.get(tag); + if (overlay) { + overlay.detach(); + overlay.dispose(); + this.overlaysMap.delete(tag); + } + } + } + } +} diff --git a/src/lib/dialog.service.spec.ts b/src/lib/dialog.service.spec.ts new file mode 100644 index 0000000..016507e --- /dev/null +++ b/src/lib/dialog.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DialogService } from './dialog.service'; + +describe('DialogService', () => { + let service: DialogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DialogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/lib/dialog.service.ts b/src/lib/dialog.service.ts new file mode 100644 index 0000000..7d06965 --- /dev/null +++ b/src/lib/dialog.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class DialogService { + + constructor() { } +} From e9caceddfc0c30e8376d65867de79ebbcac1a8c8 Mon Sep 17 00:00:00 2001 From: inga Date: Tue, 16 Apr 2024 11:33:44 +0200 Subject: [PATCH 02/50] refactor: change open to model --- .../dialog-sample.component.html | 16 ++-- .../components/dialog/dialog.component.html | 79 ++++++++++--------- .../lib/components/dialog/dialog.component.ts | 8 +- 3 files changed, 50 insertions(+), 53 deletions(-) diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html index 971123b..a2ca3e8 100644 --- a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html +++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html @@ -1,16 +1,16 @@ - - -

here comes some content

-
+ +

here comes some content

+
diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.html b/libs/sketch/src/lib/components/dialog/dialog.component.html index b6052e9..264c22e 100644 --- a/libs/sketch/src/lib/components/dialog/dialog.component.html +++ b/libs/sketch/src/lib/components/dialog/dialog.component.html @@ -1,50 +1,51 @@ + +
diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.ts b/libs/sketch/src/lib/components/dialog/dialog.component.ts index 765b377..920d0a9 100644 --- a/libs/sketch/src/lib/components/dialog/dialog.component.ts +++ b/libs/sketch/src/lib/components/dialog/dialog.component.ts @@ -8,9 +8,9 @@ import { inject, input, output, - signal, viewChild, OnDestroy, + model, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CdkTrapFocus } from '@angular/cdk/a11y'; @@ -32,14 +32,10 @@ export class DialogComponent implements OnDestroy { private readonly dialogService = inject(DialogService); private readonly viewContainerRef = inject(ViewContainerRef); - readonly dialogContentTemplate = signal | undefined>( - undefined - ); - @HostBinding('attr.data-dialog-tag') private _dialogId!: string; - open = input(false); + open = model(false); backdropClass = input([DialogComponent.defaultBackgroundClass]); From 5fc1986134ec3e3ba80ee3f68ae9d8dfe3924698 Mon Sep 17 00:00:00 2001 From: inga Date: Wed, 17 Apr 2024 09:39:31 +0200 Subject: [PATCH 03/50] style: position dialog in center --- .../dialog-sample/dialog-sample.component.css | 13 ++ .../dialog-sample.component.html | 14 +- .../components/dialog/dialog.component.css | 161 +++++++++++++++++- .../components/dialog/dialog.component.html | 16 +- .../lib/components/dialog/dialog.component.ts | 20 ++- .../dialog/service/dialog.service.ts | 8 + 6 files changed, 206 insertions(+), 26 deletions(-) diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css index e69de29..5dccc16 100644 --- a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css +++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.css @@ -0,0 +1,13 @@ +.my-backdrop { + background-color: green; +} + +button { + cursor: pointer; +} + +.my-super-styling-class { + background-color: red; + border-radius: 50px; + padding: 10px; +} diff --git a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html index a2ca3e8..7c7a4f7 100644 --- a/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html +++ b/apps/demo-app/src/app/pages/dialog-sample/dialog-sample.component.html @@ -2,15 +2,17 @@ - -

here comes some content

+
+

here comes some content

+
diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.css b/libs/sketch/src/lib/components/dialog/dialog.component.css index 8b2d8af..7d8f971 100644 --- a/libs/sketch/src/lib/components/dialog/dialog.component.css +++ b/libs/sketch/src/lib/components/dialog/dialog.component.css @@ -1,4 +1,161 @@ :host { - display: block; position: relative; -} \ No newline at end of file +} + +.dialog-outer-wrapper { + position: relative; + width: 100%; + height: 100vh; + max-width: 100%; + max-height: 100vh; +} + +.dialog-backdrop { + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; +} + +.dialog-outer-content-wrapper { + position: absolute; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + pointer-events: none; +} + +.hidden-overflow { + overflow: hidden !important; +} + +.q-common-cdk-dialog-projected-content-with-shadow { + box-shadow: -1px 3px 10px -2px #1f1f1f; +} + +.q-common-cdk-dialog-projected-content-wrapper { + position: relative; + pointer-events: all; + max-height: 100%; +} + +.q-common-cdk-dialog-projected-content { + overflow: auto; + max-width: calc(100vw - 1rem); + max-height: calc(95vh - 3rem); + max-height: calc(100dvh - 3rem); + border-radius: 4px; + margin: auto; +} + +.q-common-cdk-dialog-without-close-button +.q-common-cdk-dialog-projected-content { + max-height: calc(95vh - 1rem); + max-height: calc(100dvh - 1rem); +} + +.q-common-cdk-dialog-without-close-button.fullscreen +.q-common-cdk-dialog-projected-content { + max-width: 95vw; + max-width: 100dvw; + max-height: 95vh; + max-height: 100dvh; +} + +.q-common-cdk-dialog-projected-content.fullscreen { + max-width: 95vw; + max-width: 100dvw; + max-height: 95vh; + max-height: 100dvh; +} + +.q-common-cdk-dialog-without-close-button +.q-common-cdk-dialog-projected-content.fullscreen { + max-width: 95vw; + max-width: 100dvw; + max-height: 95vh; + max-height: 100dvh; +} + +.q-common-cdk-dialog-close-button { + top: -1.5rem; + right: 0; + fill: black; + position: absolute; + cursor: pointer; +} + +.q-common-cdk-dialog-close-button svg { + filter: drop-shadow(-1px 2px 2px #1f1f1f); +} + +.cdk-overlay-pane { + border-radius: 8px; +} + +/* +@screen sm { + .q-common-cdk-dialog-projected-content { + overflow: auto; + max-width: calc(100vw - 4rem); + max-height: calc(95vh - 4rem); + max-height: calc(100dvh - 4rem); + margin: auto; + } + + .q-common-cdk-dialog-without-close-button + .q-common-cdk-dialog-projected-content { + max-width: calc(100vw - 1rem); + max-height: calc(95vh - 1rem); + max-height: calc(100dvh - 1rem); + } + + .q-common-cdk-dialog-close-button { + top: -1.5rem; + right: -1.5rem; + } +} + +@screen md { + .q-common-cdk-dialog-projected-content-wrapper { + @apply relative m-8; + } +} + +html.cdk-global-scrollblock { + @apply !overscroll-none; +} + +!* print *! +@media print { + html.cdk-global-scrollblock { + @apply !relative !overflow-visible; + } + + html.cdk-global-scrollblock q-root, + html.cdk-global-scrollblock .cdk-overlay-backdrop { + @apply hidden; + } + + html.cdk-global-scrollblock .cdk-overlay-container, + html.cdk-global-scrollblock .cdk-overlay-pane, + html.cdk-global-scrollblock .q-common-cdk-dialog-projected-content { + @apply !relative !w-full !h-auto max-h-none max-w-none; + } + + html.cdk-global-scrollblock .q-common-cdk-dialog-projected-content-wrapper { + @apply m-0; + } +} */ diff --git a/libs/sketch/src/lib/components/dialog/dialog.component.html b/libs/sketch/src/lib/components/dialog/dialog.component.html index 264c22e..b658035 100644 --- a/libs/sketch/src/lib/components/dialog/dialog.component.html +++ b/libs/sketch/src/lib/components/dialog/dialog.component.html @@ -1,21 +1,19 @@