diff --git a/src/cdk/drag-drop/BUILD.bazel b/src/cdk/drag-drop/BUILD.bazel index 40c207693373..f4b49fb1d8f0 100644 --- a/src/cdk/drag-drop/BUILD.bazel +++ b/src/cdk/drag-drop/BUILD.bazel @@ -4,6 +4,7 @@ load( "ng_module", "ng_test_library", "ng_web_test_suite", + "sass_binary", ) package(default_visibility = ["//visibility:public"]) @@ -14,6 +15,9 @@ ng_module( ["**/*.ts"], exclude = ["**/*.spec.ts"], ), + assets = [ + ":resets_scss", + ], deps = [ "//src:dev_mode_types", "//src/cdk/a11y", @@ -44,6 +48,11 @@ ng_test_library( ], ) +sass_binary( + name = "resets_scss", + src = "resets.scss", +) + ng_web_test_suite( name = "unit_tests", deps = [":unit_test_sources"], diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index efd93105af46..c486764856f0 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -23,7 +23,6 @@ import { ViewEncapsulation, } from '@angular/core'; import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; -import {DOCUMENT} from '@angular/common'; import {ViewportRuler, CdkScrollableModule} from '@angular/cdk/scrolling'; import {_supportsShadowDom} from '@angular/cdk/platform'; import {of as observableOf} from 'rxjs'; @@ -2490,7 +2489,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; const previewRect = preview.getBoundingClientRect(); const zeroPxRegex = /^0(px)?$/; @@ -2512,23 +2510,18 @@ describe('CdkDrag', () => { .withContext('Expected element to be removed from layout') .toBe('-999em'); expect(item.style.opacity).withContext('Expected element to be invisible').toBe('0'); - expect(previewContainer) - .withContext('Expected preview container to be in the DOM') - .toBeTruthy(); - expect(previewContainer.style.color) - .withContext('Expected preview container to reset user agent color') - .toBe('inherit'); - expect(previewContainer.style.margin) - .withContext('Expected preview container to reset user agent margin') - .toMatch(zeroPxRegex); - expect(previewContainer.style.padding) - .withContext('Expected preview container to reset user agent padding') + expect(preview).withContext('Expected preview to be in the DOM').toBeTruthy(); + expect(preview.getAttribute('popover')) + .withContext('Expected preview to be a popover') + .toBe('manual'); + expect(preview.style.margin) + .withContext('Expected preview to reset the margin') .toMatch(zeroPxRegex); expect(preview.textContent!.trim()) .withContext('Expected preview content to match element') .toContain('One'); - expect(previewContainer.getAttribute('dir')) - .withContext('Expected preview container element to inherit the directionality.') + expect(preview.getAttribute('dir')) + .withContext('Expected preview element to inherit the directionality.') .toBe('ltr'); expect(previewRect.width) .withContext('Expected preview width to match element') @@ -2539,8 +2532,8 @@ describe('CdkDrag', () => { expect(preview.style.pointerEvents) .withContext('Expected pointer events to be disabled on the preview') .toBe('none'); - expect(previewContainer.style.zIndex) - .withContext('Expected preview container to have a high default zIndex.') + expect(preview.style.zIndex) + .withContext('Expected preview to have a high default zIndex.') .toBe('1000'); // Use a regex here since some browsers normalize 0 to 0px, but others don't. // Use a regex here since some browsers normalize 0 to 0px, but others don't. @@ -2561,8 +2554,8 @@ describe('CdkDrag', () => { expect(item.style.top).withContext('Expected element to be within the layout').toBeFalsy(); expect(item.style.left).withContext('Expected element to be within the layout').toBeFalsy(); expect(item.style.opacity).withContext('Expected element to be visible').toBeFalsy(); - expect(previewContainer.parentNode) - .withContext('Expected preview container to be removed from the DOM') + expect(preview.parentNode) + .withContext('Expected preview to be removed from the DOM') .toBeFalsy(); })); @@ -2580,59 +2573,10 @@ describe('CdkDrag', () => { const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; startDraggingViaMouse(fixture, item); - const preview = document.querySelector('.cdk-drag-preview-container')! as HTMLElement; + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; expect(preview.style.zIndex).toBe('3000'); })); - it('should create the preview inside the fullscreen element when in fullscreen mode', fakeAsync(() => { - // Provide a limited stub of the document since we can't trigger fullscreen - // mode in unit tests and there are some issues with doing it in e2e tests. - const fakeDocument = { - body: document.body, - documentElement: document.documentElement, - fullscreenElement: document.createElement('div'), - ELEMENT_NODE: Node.ELEMENT_NODE, - querySelectorAll: (...args: [string]) => document.querySelectorAll(...args), - querySelector: (...args: [string]) => document.querySelector(...args), - createElement: (...args: [string]) => document.createElement(...args), - createTextNode: (...args: [string]) => document.createTextNode(...args), - addEventListener: ( - ...args: [ - string, - EventListenerOrEventListenerObject, - (boolean | AddEventListenerOptions | undefined)?, - ] - ) => document.addEventListener(...args), - removeEventListener: ( - ...args: [ - string, - EventListenerOrEventListenerObject, - (boolean | AddEventListenerOptions | undefined)?, - ] - ) => document.addEventListener(...args), - createComment: (text: string) => document.createComment(text), - }; - const fixture = createComponent(DraggableInDropZone, [ - { - provide: DOCUMENT, - useFactory: () => fakeDocument, - }, - ]); - fixture.detectChanges(); - const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; - - document.body.appendChild(fakeDocument.fullscreenElement); - startDraggingViaMouse(fixture, item); - flush(); - - const previewContainer = document.querySelector( - '.cdk-drag-preview-container', - )! as HTMLElement; - - expect(previewContainer.parentNode).toBe(fakeDocument.fullscreenElement); - fakeDocument.fullscreenElement.remove(); - })); - it('should be able to constrain the preview position', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.componentInstance.boundarySelector = '.cdk-drop-list'; @@ -2928,8 +2872,8 @@ describe('CdkDrag', () => { const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; startDraggingViaMouse(fixture, item); - expect(document.querySelector('.cdk-drag-preview-container')!.getAttribute('dir')) - .withContext('Expected preview container to inherit the directionality.') + expect(document.querySelector('.cdk-drag-preview')!.getAttribute('dir')) + .withContext('Expected preview to inherit the directionality.') .toBe('rtl'); })); @@ -2941,7 +2885,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; // Add a duration since the tests won't include one. preview.style.transitionDuration = '500ms'; @@ -2954,13 +2897,13 @@ describe('CdkDrag', () => { fixture.detectChanges(); tick(250); - expect(previewContainer.parentNode) + expect(preview.parentNode) .withContext('Expected preview to be in the DOM mid-way through the transition') .toBeTruthy(); tick(500); - expect(previewContainer.parentNode) + expect(preview.parentNode) .withContext('Expected preview to be removed from the DOM if the transition timed out') .toBeFalsy(); })); @@ -3064,7 +3007,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; preview.style.transition = 'opacity 500ms ease'; dispatchMouseEvent(document, 'mousemove', 50, 50); @@ -3074,8 +3016,8 @@ describe('CdkDrag', () => { fixture.detectChanges(); tick(0); - expect(previewContainer.parentNode) - .withContext('Expected preview container to be removed from the DOM immediately') + expect(preview.parentNode) + .withContext('Expected preview to be removed from the DOM immediately') .toBeFalsy(); })); @@ -3087,7 +3029,6 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; preview.style.transition = 'opacity 500ms ease, transform 1000ms ease'; dispatchMouseEvent(document, 'mousemove', 50, 50); @@ -3097,17 +3038,15 @@ describe('CdkDrag', () => { fixture.detectChanges(); tick(500); - expect(previewContainer.parentNode) - .withContext( - 'Expected preview container to be in the DOM at the end of the opacity transition', - ) + expect(preview.parentNode) + .withContext('Expected preview to be in the DOM at the end of the opacity transition') .toBeTruthy(); tick(1000); - expect(previewContainer.parentNode) + expect(preview.parentNode) .withContext( - 'Expected preview container to be removed from the DOM at the end of the transform transition', + 'Expected preview to be removed from the DOM at the end of the transform transition', ) .toBeFalsy(); })); @@ -3149,8 +3088,8 @@ describe('CdkDrag', () => { const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; startDraggingViaMouse(fixture, item); - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; - expect(previewContainer.parentNode).toBe(document.body); + const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; + expect(preview.parentNode).toBe(document.body); })); it('should insert the preview into the parent node if previewContainer is set to `parent`', fakeAsync(() => { @@ -3161,9 +3100,9 @@ describe('CdkDrag', () => { const list = fixture.nativeElement.querySelector('.drop-list'); startDraggingViaMouse(fixture, item); - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; + const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; expect(list).toBeTruthy(); - expect(previewContainer.parentNode).toBe(list); + expect(preview.parentNode).toBe(list); })); it('should insert the preview into a particular element, if specified', fakeAsync(() => { @@ -3177,10 +3116,8 @@ describe('CdkDrag', () => { fixture.detectChanges(); startDraggingViaMouse(fixture, item); - const previewContainerElement = document.querySelector( - '.cdk-drag-preview-container', - ) as HTMLElement; - expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement); + const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; + expect(preview.parentNode).toBe(previewContainer.nativeElement); })); it('should remove the id from the placeholder', fakeAsync(() => { @@ -3692,17 +3629,15 @@ describe('CdkDrag', () => { startDraggingViaMouse(fixture, item); - const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement; + const preview = document.querySelector('.cdk-drag-preview') as HTMLElement; - expect(previewContainer.parentNode) - .withContext('Expected preview container to be in the DOM') - .toBeTruthy(); + expect(preview.parentNode).withContext('Expected preview to be in the DOM').toBeTruthy(); expect(item.parentNode).withContext('Expected drag item to be in the DOM').toBeTruthy(); fixture.destroy(); - expect(previewContainer.parentNode) - .withContext('Expected preview container to be removed from the DOM') + expect(preview.parentNode) + .withContext('Expected preview to be removed from the DOM') .toBeFalsy(); expect(item.parentNode) .withContext('Expected drag item to be removed from the DOM') diff --git a/src/cdk/drag-drop/drag-drop.ts b/src/cdk/drag-drop/drag-drop.ts index c83c219a59e9..910f150c44cf 100644 --- a/src/cdk/drag-drop/drag-drop.ts +++ b/src/cdk/drag-drop/drag-drop.ts @@ -6,7 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Inject, NgZone, ElementRef} from '@angular/core'; +import { + Injectable, + Inject, + NgZone, + ElementRef, + Component, + ViewEncapsulation, + ChangeDetectionStrategy, + ApplicationRef, + inject, + createComponent, + EnvironmentInjector, +} from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {ViewportRuler} from '@angular/cdk/scrolling'; import {DragRef, DragRefConfig} from './drag-ref'; @@ -19,11 +31,31 @@ const DEFAULT_CONFIG = { pointerDirectionChangeThreshold: 5, }; +/** Keeps track of the apps currently containing badges. */ +const activeApps = new Set(); + +/** + * Component used to load the drag&drop reset styles. + * @docs-private + */ +@Component({ + standalone: true, + styleUrl: 'resets.css', + encapsulation: ViewEncapsulation.None, + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + host: {'cdk-drag-resets-container': ''}, +}) +export class _ResetsLoader {} + /** * Service that allows for drag-and-drop functionality to be attached to DOM elements. */ @Injectable({providedIn: 'root'}) export class DragDrop { + private _appRef = inject(ApplicationRef); + private _environmentInjector = inject(EnvironmentInjector); + constructor( @Inject(DOCUMENT) private _document: any, private _ngZone: NgZone, @@ -40,6 +72,7 @@ export class DragDrop { element: ElementRef | HTMLElement, config: DragRefConfig = DEFAULT_CONFIG, ): DragRef { + this._loadResets(); return new DragRef( element, config, @@ -63,4 +96,23 @@ export class DragDrop { this._viewportRuler, ); } + + // TODO(crisbeto): abstract this away into something reusable. + /** Loads the CSS resets needed for the module to work correctly. */ + private _loadResets() { + if (!activeApps.has(this._appRef)) { + activeApps.add(this._appRef); + + const componentRef = createComponent(_ResetsLoader, { + environmentInjector: this._environmentInjector, + }); + + this._appRef.onDestroy(() => { + activeApps.delete(this._appRef); + if (activeApps.size === 0) { + componentRef.destroy(); + } + }); + } + } } diff --git a/src/cdk/drag-drop/preview-ref.ts b/src/cdk/drag-drop/preview-ref.ts index ed403ee913c2..3e302f8adc2e 100644 --- a/src/cdk/drag-drop/preview-ref.ts +++ b/src/cdk/drag-drop/preview-ref.ts @@ -39,9 +39,6 @@ export class PreviewRef { /** Reference to the preview element. */ private _preview: HTMLElement; - /** Reference to the preview wrapper. */ - private _wrapper: HTMLElement; - constructor( private _document: Document, private _rootElement: HTMLElement, @@ -58,22 +55,20 @@ export class PreviewRef { ) {} attach(parent: HTMLElement): void { - this._wrapper = this._createWrapper(); this._preview = this._createPreview(); - this._wrapper.appendChild(this._preview); - parent.appendChild(this._wrapper); + parent.appendChild(this._preview); // The null check is necessary for browsers that don't support the popover API. // Note that we use a string access for compatibility with Closure. - if ('showPopover' in this._wrapper) { - this._wrapper['showPopover'](); + if ('showPopover' in this._preview) { + this._preview['showPopover'](); } } destroy(): void { - this._wrapper?.remove(); + this._preview.remove(); this._previewEmbeddedView?.destroy(); - this._preview = this._wrapper = this._previewEmbeddedView = null!; + this._preview = this._previewEmbeddedView = null!; } setTransform(value: string): void { @@ -100,34 +95,6 @@ export class PreviewRef { this._preview.removeEventListener(name, handler); } - private _createWrapper(): HTMLElement { - const wrapper = this._document.createElement('div'); - wrapper.setAttribute('popover', 'manual'); - wrapper.setAttribute('dir', this._direction); - wrapper.classList.add('cdk-drag-preview-container'); - - extendStyles(wrapper.style, { - // This is redundant, but we need it for browsers that don't support the popover API. - 'position': 'fixed', - 'top': '0', - 'left': '0', - 'width': '100%', - 'height': '100%', - 'z-index': this._zIndex + '', - - // Reset the user agent styles. - 'background': 'none', - 'border': 'none', - 'pointer-events': 'none', - 'margin': '0', - 'padding': '0', - 'color': 'inherit', - }); - toggleNativeDragInteractions(wrapper, false); - - return wrapper; - } - private _createPreview(): HTMLElement { const previewConfig = this._previewTemplate; const previewClass = this._previewClass; @@ -170,15 +137,18 @@ export class PreviewRef { 'pointer-events': 'none', // We have to reset the margin, because it can throw off positioning relative to the viewport. 'margin': '0', - 'position': 'absolute', + 'position': 'fixed', 'top': '0', 'left': '0', + 'z-index': this._zIndex + '', }, importantProperties, ); toggleNativeDragInteractions(preview, false); preview.classList.add('cdk-drag-preview'); + preview.setAttribute('popover', 'manual'); + preview.setAttribute('dir', this._direction); if (previewClass) { if (Array.isArray(previewClass)) { diff --git a/src/cdk/drag-drop/resets.scss b/src/cdk/drag-drop/resets.scss new file mode 100644 index 000000000000..f218c05e51cc --- /dev/null +++ b/src/cdk/drag-drop/resets.scss @@ -0,0 +1,8 @@ +@layer cdk-resets { + .cdk-drag-preview { + background: none; + border: none; + padding: 0; + color: inherit; + } +}