Skip to content

fix(cdk/drag-drop): use native popover to avoid stacking issues with preview #28945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 48 additions & 40 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2489,7 +2489,8 @@ describe('CdkDrag', () => {

startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
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)?$/;

Expand All @@ -2511,12 +2512,14 @@ 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(preview).withContext('Expected preview to be in the DOM').toBeTruthy();
expect(previewContainer)
.withContext('Expected preview container to be in the DOM')
.toBeTruthy();
expect(preview.textContent!.trim())
.withContext('Expected preview content to match element')
.toContain('One');
expect(preview.getAttribute('dir'))
.withContext('Expected preview element to inherit the directionality.')
expect(previewContainer.getAttribute('dir'))
.withContext('Expected preview container element to inherit the directionality.')
.toBe('ltr');
expect(previewRect.width)
.withContext('Expected preview width to match element')
Expand All @@ -2527,8 +2530,8 @@ describe('CdkDrag', () => {
expect(preview.style.pointerEvents)
.withContext('Expected pointer events to be disabled on the preview')
.toBe('none');
expect(preview.style.zIndex)
.withContext('Expected preview to have a high default zIndex.')
expect(previewContainer.style.zIndex)
.withContext('Expected preview container 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.
Expand All @@ -2549,8 +2552,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(preview.parentNode)
.withContext('Expected preview to be removed from the DOM')
expect(previewContainer.parentNode)
.withContext('Expected preview container to be removed from the DOM')
.toBeFalsy();
}));

Expand All @@ -2568,7 +2571,7 @@ describe('CdkDrag', () => {
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
const preview = document.querySelector('.cdk-drag-preview-container')! as HTMLElement;
expect(preview.style.zIndex).toBe('3000');
}));

Expand Down Expand Up @@ -2613,9 +2616,11 @@ describe('CdkDrag', () => {
startDraggingViaMouse(fixture, item);
flush();

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
const previewContainer = document.querySelector(
'.cdk-drag-preview-container',
)! as HTMLElement;

expect(preview.parentNode).toBe(fakeDocument.fullscreenElement);
expect(previewContainer.parentNode).toBe(fakeDocument.fullscreenElement);
fakeDocument.fullscreenElement.remove();
}));

Expand Down Expand Up @@ -2914,8 +2919,8 @@ describe('CdkDrag', () => {
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
startDraggingViaMouse(fixture, item);

expect(document.querySelector('.cdk-drag-preview')!.getAttribute('dir'))
.withContext('Expected preview element to inherit the directionality.')
expect(document.querySelector('.cdk-drag-preview-container')!.getAttribute('dir'))
.withContext('Expected preview container to inherit the directionality.')
.toBe('rtl');
}));

Expand All @@ -2926,7 +2931,8 @@ describe('CdkDrag', () => {

startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
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';
Expand All @@ -2939,13 +2945,13 @@ describe('CdkDrag', () => {
fixture.detectChanges();
tick(250);

expect(preview.parentNode)
expect(previewContainer.parentNode)
.withContext('Expected preview to be in the DOM mid-way through the transition')
.toBeTruthy();

tick(500);

expect(preview.parentNode)
expect(previewContainer.parentNode)
.withContext('Expected preview to be removed from the DOM if the transition timed out')
.toBeFalsy();
}));
Expand Down Expand Up @@ -3049,6 +3055,7 @@ 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);
Expand All @@ -3058,8 +3065,8 @@ describe('CdkDrag', () => {
fixture.detectChanges();
tick(0);

expect(preview.parentNode)
.withContext('Expected preview to be removed from the DOM immediately')
expect(previewContainer.parentNode)
.withContext('Expected preview container to be removed from the DOM immediately')
.toBeFalsy();
}));

Expand All @@ -3071,6 +3078,7 @@ 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);
Expand All @@ -3080,15 +3088,17 @@ describe('CdkDrag', () => {
fixture.detectChanges();
tick(500);

expect(preview.parentNode)
.withContext('Expected preview to be in the DOM at the end of the opacity transition')
expect(previewContainer.parentNode)
.withContext(
'Expected preview container to be in the DOM at the end of the opacity transition',
)
.toBeTruthy();

tick(1000);

expect(preview.parentNode)
expect(previewContainer.parentNode)
.withContext(
'Expected preview to be removed from the DOM at the end of the ' + 'transform transition',
'Expected preview container to be removed from the DOM at the end of the transform transition',
)
.toBeFalsy();
}));
Expand Down Expand Up @@ -3130,8 +3140,8 @@ describe('CdkDrag', () => {
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.parentNode).toBe(document.body);
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
expect(previewContainer.parentNode).toBe(document.body);
}));

it('should insert the preview into the parent node if previewContainer is set to `parent`', fakeAsync(() => {
Expand All @@ -3142,9 +3152,9 @@ describe('CdkDrag', () => {
const list = fixture.nativeElement.querySelector('.drop-list');

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
expect(list).toBeTruthy();
expect(preview.parentNode).toBe(list);
expect(previewContainer.parentNode).toBe(list);
}));

it('should insert the preview into a particular element, if specified', fakeAsync(() => {
Expand All @@ -3158,8 +3168,10 @@ describe('CdkDrag', () => {
fixture.detectChanges();

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.parentNode).toBe(previewContainer.nativeElement);
const previewContainerElement = document.querySelector(
'.cdk-drag-preview-container',
) as HTMLElement;
expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement);
}));

it('should remove the id from the placeholder', fakeAsync(() => {
Expand Down Expand Up @@ -3671,15 +3683,17 @@ describe('CdkDrag', () => {

startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;

expect(preview.parentNode).withContext('Expected preview to be in the DOM').toBeTruthy();
expect(previewContainer.parentNode)
.withContext('Expected preview container to be in the DOM')
.toBeTruthy();
expect(item.parentNode).withContext('Expected drag item to be in the DOM').toBeTruthy();

fixture.destroy();

expect(preview.parentNode)
.withContext('Expected preview to be removed from the DOM')
expect(previewContainer.parentNode)
.withContext('Expected preview container to be removed from the DOM')
.toBeFalsy();
expect(item.parentNode)
.withContext('Expected drag item to be removed from the DOM')
Expand Down Expand Up @@ -6548,21 +6562,15 @@ describe('CdkDrag', () => {
startDraggingViaMouse(fixture, item.element.nativeElement);
fixture.detectChanges();

const initialSelectStart = dispatchFakeEvent(
shadowRoot,
'selectstart',
);
const initialSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart');
fixture.detectChanges();
expect(initialSelectStart.defaultPrevented).toBe(true);

dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();

const afterDropSelectStart = dispatchFakeEvent(
shadowRoot,
'selectstart',
);
const afterDropSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart');
fixture.detectChanges();
expect(afterDropSelectStart.defaultPrevented).toBe(false);
}));
Expand Down
25 changes: 25 additions & 0 deletions src/cdk/drag-drop/dom/root-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {EmbeddedViewRef} from '@angular/core';

/**
* Gets the root HTML element of an embedded view.
* If the root is not an HTML element it gets wrapped in one.
*/
export function getRootNode(viewRef: EmbeddedViewRef<any>, _document: Document): HTMLElement {
const rootNodes: Node[] = viewRef.rootNodes;

if (rootNodes.length === 1 && rootNodes[0].nodeType === _document.ELEMENT_NODE) {
return rootNodes[0] as HTMLElement;
}

const wrapper = _document.createElement('div');
rootNodes.forEach(node => wrapper.appendChild(node));
return wrapper;
}
22 changes: 22 additions & 0 deletions src/cdk/drag-drop/dom/styling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,25 @@ export function combineTransforms(transform: string, initialTransform?: string):
? transform + ' ' + initialTransform
: transform;
}

/**
* Matches the target element's size to the source's size.
* @param target Element that needs to be resized.
* @param sourceRect Dimensions of the source element.
*/
export function matchElementSize(target: HTMLElement, sourceRect: DOMRect): void {
target.style.width = `${sourceRect.width}px`;
target.style.height = `${sourceRect.height}px`;
target.style.transform = getTransform(sourceRect.left, sourceRect.top);
}

/**
* Gets a 3d `transform` that can be applied to an element.
* @param x Desired position of the element along the X axis.
* @param y Desired position of the element along the Y axis.
*/
export function getTransform(x: number, y: number): string {
// Round the transforms since some browsers will
// blur the elements for sub-pixel transforms.
return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`;
}
Loading