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