diff --git a/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.spec.ts b/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.spec.ts new file mode 100644 index 00000000..9b3794d2 --- /dev/null +++ b/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.spec.ts @@ -0,0 +1,107 @@ +import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop' +import { Component } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { By } from '@angular/platform-browser' +import { provideNoopAnimations } from '@angular/platform-browser/animations' +import { MatDialog, MatDialogModule } from '@angular/material/dialog' +import { DraggableDialogDirective, provideDraggableDialogRootElementSelector } from './draggable-dialog.directive' + +@Component({ + template: ` +
+

Draggable dialog

+
+ `, + imports: [CdkDragHandle, DraggableDialogDirective, MatDialogModule] +}) +class TestDraggableDialogComponent {} + +@Component({ + template: ` +
+

Draggable dialog

+
+ `, + imports: [CdkDragHandle, DraggableDialogDirective] +}) +class TestConfiguredDraggableDialogComponent { + rootElementSelector = '.custom-dialog-root' +} + +describe('DraggableDialogDirective', () => { + afterEach(() => { + TestBed.inject(MatDialog, null)?.closeAll() + document.querySelectorAll('.cdk-overlay-container').forEach((element) => element.remove()) + }) + + it('should use the CDK overlay pane as drag root by default', () => { + const fixture = createComponent(TestDraggableDialogComponent) + const cdkDrag = fixture.debugElement.query(By.directive(CdkDrag)).injector.get(CdkDrag) + + expect(cdkDrag.rootElementSelector).toBe('.cdk-overlay-pane') + }) + + it('should allow configuring the drag root selector', () => { + const fixture = createComponent(TestConfiguredDraggableDialogComponent) + const cdkDrag = fixture.debugElement.query(By.directive(CdkDrag)).injector.get(CdkDrag) + + expect(cdkDrag.rootElementSelector).toBe('.custom-dialog-root') + }) + + it('should allow configuring the application default drag root selector', () => { + TestBed.configureTestingModule({ + imports: [TestDraggableDialogComponent], + providers: [provideNoopAnimations(), provideDraggableDialogRootElementSelector('.application-dialog-root')] + }) + + const fixture = TestBed.createComponent(TestDraggableDialogComponent) + fixture.detectChanges() + const cdkDrag = fixture.debugElement.query(By.directive(CdkDrag)).injector.get(CdkDrag) + + expect(cdkDrag.rootElementSelector).toBe('.application-dialog-root') + }) + + it('should start dragging an opened Material dialog without crashing', () => { + TestBed.configureTestingModule({ + imports: [MatDialogModule, TestDraggableDialogComponent], + providers: [provideNoopAnimations()] + }) + + const dialog = TestBed.inject(MatDialog) + dialog.open(TestDraggableDialogComponent) + + const handle = document.querySelector('h2') as HTMLElement + expect(handle).toBeTruthy() + + expect(() => { + dispatchDragEvent(handle, 'mousedown', 10, 10) + dispatchDragEvent(document, 'mousemove', 40, 40) + dispatchDragEvent(document, 'mouseup', 40, 40) + }).not.toThrow() + }) +}) + +function createComponent(component: new () => T): ComponentFixture { + TestBed.configureTestingModule({ + imports: [component], + providers: [provideNoopAnimations()] + }) + + const fixture = TestBed.createComponent(component) + fixture.detectChanges() + + return fixture +} + +function dispatchDragEvent(target: EventTarget, type: string, clientX: number, clientY: number): void { + target.dispatchEvent( + new MouseEvent(type, { + bubbles: true, + cancelable: true, + clientX, + clientY, + button: 0, + buttons: type === 'mouseup' ? 0 : 1 + }) + ) +} diff --git a/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.ts b/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.ts index f2cc0966..f6110980 100644 --- a/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.ts +++ b/projects/ppwcode/ng-dialogs/src/lib/directives/draggable-dialog.directive.ts @@ -1,17 +1,31 @@ import { CdkDrag } from '@angular/cdk/drag-drop' -import { Directive, ElementRef, inject, OnInit, Renderer2 } from '@angular/core' +import { Directive, ElementRef, inject, InjectionToken, input, OnInit, Renderer2, ValueProvider } from '@angular/core' + +export const DRAGGABLE_DIALOG_ROOT_ELEMENT_SELECTOR = new InjectionToken( + 'Draggable dialog root element selector', + { factory: () => '.cdk-overlay-pane' } +) + +export const provideDraggableDialogRootElementSelector = (rootElementSelector: string): ValueProvider => ({ + provide: DRAGGABLE_DIALOG_ROOT_ELEMENT_SELECTOR, + useValue: rootElementSelector +}) @Directive({ selector: '[ppwDraggableDialog]', hostDirectives: [CdkDrag] }) export class DraggableDialogDirective implements OnInit { + public readonly rootElementSelector = input(inject(DRAGGABLE_DIALOG_ROOT_ELEMENT_SELECTOR), { + alias: 'ppwDraggableDialogRootElementSelector' + }) + #el: ElementRef = inject(ElementRef) #renderer: Renderer2 = inject(Renderer2) #cdkDrag: CdkDrag = inject(CdkDrag) ngOnInit(): void { this.#renderer.addClass(this.#el.nativeElement, 'ppw-draggable-dialog') - this.#cdkDrag.rootElementSelector = '.cdk-overlay-pane' + this.#cdkDrag.rootElementSelector = this.rootElementSelector() } }