Skip to content

Commit

Permalink
fix(cdk/drag-drop): use native popover to avoid stacking issues with …
Browse files Browse the repository at this point in the history
…preview

Wraps the preview element in a native popover which allows it to always be rendered on top of everything and to avoid issues when the parent element has a `transform`.

Fixes #28889.
  • Loading branch information
crisbeto committed Apr 24, 2024
1 parent ebab924 commit 7cd3f02
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 45 deletions.
88 changes: 48 additions & 40 deletions src/cdk/drag-drop/directives/drag.spec.ts
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
45 changes: 40 additions & 5 deletions src/cdk/drag-drop/preview-ref.ts
Expand Up @@ -39,6 +39,9 @@ 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,
Expand All @@ -55,14 +58,21 @@ export class PreviewRef {
) {}

attach(parent: HTMLElement): void {
this._wrapper = this._createWrapper();
this._preview = this._createPreview();
parent.appendChild(this._preview);
this._wrapper.appendChild(this._preview);
parent.appendChild(this._wrapper);

// The null check is necessary for browsers that don't support the popover API.
if (this._wrapper.showPopover) {
this._wrapper.showPopover();
}
}

destroy(): void {
this._preview?.remove();
this._wrapper?.remove();
this._previewEmbeddedView?.destroy();
this._preview = this._previewEmbeddedView = null!;
this._preview = this._wrapper = this._previewEmbeddedView = null!;
}

setTransform(value: string): void {
Expand All @@ -89,6 +99,33 @@ 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',
});
toggleNativeDragInteractions(wrapper, false);

return wrapper;
}

private _createPreview(): HTMLElement {
const previewConfig = this._previewTemplate;
const previewClass = this._previewClass;
Expand Down Expand Up @@ -134,14 +171,12 @@ export class PreviewRef {
'position': 'absolute',
'top': '0',
'left': '0',
'z-index': `${this._zIndex}`,
},
importantProperties,
);

toggleNativeDragInteractions(preview, false);
preview.classList.add('cdk-drag-preview');
preview.setAttribute('dir', this._direction);

if (previewClass) {
if (Array.isArray(previewClass)) {
Expand Down

0 comments on commit 7cd3f02

Please sign in to comment.