Skip to content

Commit 7cd3f02

Browse files
committed
fix(cdk/drag-drop): use native popover to avoid stacking issues with 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.
1 parent ebab924 commit 7cd3f02

File tree

2 files changed

+88
-45
lines changed

2 files changed

+88
-45
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,7 +2489,8 @@ describe('CdkDrag', () => {
24892489

24902490
startDraggingViaMouse(fixture, item);
24912491

2492-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2492+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
2493+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
24932494
const previewRect = preview.getBoundingClientRect();
24942495
const zeroPxRegex = /^0(px)?$/;
24952496

@@ -2511,12 +2512,14 @@ describe('CdkDrag', () => {
25112512
.withContext('Expected element to be removed from layout')
25122513
.toBe('-999em');
25132514
expect(item.style.opacity).withContext('Expected element to be invisible').toBe('0');
2514-
expect(preview).withContext('Expected preview to be in the DOM').toBeTruthy();
2515+
expect(previewContainer)
2516+
.withContext('Expected preview container to be in the DOM')
2517+
.toBeTruthy();
25152518
expect(preview.textContent!.trim())
25162519
.withContext('Expected preview content to match element')
25172520
.toContain('One');
2518-
expect(preview.getAttribute('dir'))
2519-
.withContext('Expected preview element to inherit the directionality.')
2521+
expect(previewContainer.getAttribute('dir'))
2522+
.withContext('Expected preview container element to inherit the directionality.')
25202523
.toBe('ltr');
25212524
expect(previewRect.width)
25222525
.withContext('Expected preview width to match element')
@@ -2527,8 +2530,8 @@ describe('CdkDrag', () => {
25272530
expect(preview.style.pointerEvents)
25282531
.withContext('Expected pointer events to be disabled on the preview')
25292532
.toBe('none');
2530-
expect(preview.style.zIndex)
2531-
.withContext('Expected preview to have a high default zIndex.')
2533+
expect(previewContainer.style.zIndex)
2534+
.withContext('Expected preview container to have a high default zIndex.')
25322535
.toBe('1000');
25332536
// Use a regex here since some browsers normalize 0 to 0px, but others don't.
25342537
// Use a regex here since some browsers normalize 0 to 0px, but others don't.
@@ -2549,8 +2552,8 @@ describe('CdkDrag', () => {
25492552
expect(item.style.top).withContext('Expected element to be within the layout').toBeFalsy();
25502553
expect(item.style.left).withContext('Expected element to be within the layout').toBeFalsy();
25512554
expect(item.style.opacity).withContext('Expected element to be visible').toBeFalsy();
2552-
expect(preview.parentNode)
2553-
.withContext('Expected preview to be removed from the DOM')
2555+
expect(previewContainer.parentNode)
2556+
.withContext('Expected preview container to be removed from the DOM')
25542557
.toBeFalsy();
25552558
}));
25562559

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

2571-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2574+
const preview = document.querySelector('.cdk-drag-preview-container')! as HTMLElement;
25722575
expect(preview.style.zIndex).toBe('3000');
25732576
}));
25742577

@@ -2613,9 +2616,11 @@ describe('CdkDrag', () => {
26132616
startDraggingViaMouse(fixture, item);
26142617
flush();
26152618

2616-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2619+
const previewContainer = document.querySelector(
2620+
'.cdk-drag-preview-container',
2621+
)! as HTMLElement;
26172622

2618-
expect(preview.parentNode).toBe(fakeDocument.fullscreenElement);
2623+
expect(previewContainer.parentNode).toBe(fakeDocument.fullscreenElement);
26192624
fakeDocument.fullscreenElement.remove();
26202625
}));
26212626

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

2917-
expect(document.querySelector('.cdk-drag-preview')!.getAttribute('dir'))
2918-
.withContext('Expected preview element to inherit the directionality.')
2922+
expect(document.querySelector('.cdk-drag-preview-container')!.getAttribute('dir'))
2923+
.withContext('Expected preview container to inherit the directionality.')
29192924
.toBe('rtl');
29202925
}));
29212926

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

29272932
startDraggingViaMouse(fixture, item);
29282933

2929-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2934+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
2935+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
29302936

29312937
// Add a duration since the tests won't include one.
29322938
preview.style.transitionDuration = '500ms';
@@ -2939,13 +2945,13 @@ describe('CdkDrag', () => {
29392945
fixture.detectChanges();
29402946
tick(250);
29412947

2942-
expect(preview.parentNode)
2948+
expect(previewContainer.parentNode)
29432949
.withContext('Expected preview to be in the DOM mid-way through the transition')
29442950
.toBeTruthy();
29452951

29462952
tick(500);
29472953

2948-
expect(preview.parentNode)
2954+
expect(previewContainer.parentNode)
29492955
.withContext('Expected preview to be removed from the DOM if the transition timed out')
29502956
.toBeFalsy();
29512957
}));
@@ -3049,6 +3055,7 @@ describe('CdkDrag', () => {
30493055
startDraggingViaMouse(fixture, item);
30503056

30513057
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3058+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
30523059
preview.style.transition = 'opacity 500ms ease';
30533060

30543061
dispatchMouseEvent(document, 'mousemove', 50, 50);
@@ -3058,8 +3065,8 @@ describe('CdkDrag', () => {
30583065
fixture.detectChanges();
30593066
tick(0);
30603067

3061-
expect(preview.parentNode)
3062-
.withContext('Expected preview to be removed from the DOM immediately')
3068+
expect(previewContainer.parentNode)
3069+
.withContext('Expected preview container to be removed from the DOM immediately')
30633070
.toBeFalsy();
30643071
}));
30653072

@@ -3071,6 +3078,7 @@ describe('CdkDrag', () => {
30713078
startDraggingViaMouse(fixture, item);
30723079

30733080
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3081+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
30743082
preview.style.transition = 'opacity 500ms ease, transform 1000ms ease';
30753083

30763084
dispatchMouseEvent(document, 'mousemove', 50, 50);
@@ -3080,15 +3088,17 @@ describe('CdkDrag', () => {
30803088
fixture.detectChanges();
30813089
tick(500);
30823090

3083-
expect(preview.parentNode)
3084-
.withContext('Expected preview to be in the DOM at the end of the opacity transition')
3091+
expect(previewContainer.parentNode)
3092+
.withContext(
3093+
'Expected preview container to be in the DOM at the end of the opacity transition',
3094+
)
30853095
.toBeTruthy();
30863096

30873097
tick(1000);
30883098

3089-
expect(preview.parentNode)
3099+
expect(previewContainer.parentNode)
30903100
.withContext(
3091-
'Expected preview to be removed from the DOM at the end of the ' + 'transform transition',
3101+
'Expected preview container to be removed from the DOM at the end of the transform transition',
30923102
)
30933103
.toBeFalsy();
30943104
}));
@@ -3130,8 +3140,8 @@ describe('CdkDrag', () => {
31303140
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
31313141

31323142
startDraggingViaMouse(fixture, item);
3133-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3134-
expect(preview.parentNode).toBe(document.body);
3143+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
3144+
expect(previewContainer.parentNode).toBe(document.body);
31353145
}));
31363146

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

31443154
startDraggingViaMouse(fixture, item);
3145-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3155+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
31463156
expect(list).toBeTruthy();
3147-
expect(preview.parentNode).toBe(list);
3157+
expect(previewContainer.parentNode).toBe(list);
31483158
}));
31493159

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

31603170
startDraggingViaMouse(fixture, item);
3161-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3162-
expect(preview.parentNode).toBe(previewContainer.nativeElement);
3171+
const previewContainerElement = document.querySelector(
3172+
'.cdk-drag-preview-container',
3173+
) as HTMLElement;
3174+
expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement);
31633175
}));
31643176

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

36723684
startDraggingViaMouse(fixture, item);
36733685

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

3676-
expect(preview.parentNode).withContext('Expected preview to be in the DOM').toBeTruthy();
3688+
expect(previewContainer.parentNode)
3689+
.withContext('Expected preview container to be in the DOM')
3690+
.toBeTruthy();
36773691
expect(item.parentNode).withContext('Expected drag item to be in the DOM').toBeTruthy();
36783692

36793693
fixture.destroy();
36803694

3681-
expect(preview.parentNode)
3682-
.withContext('Expected preview to be removed from the DOM')
3695+
expect(previewContainer.parentNode)
3696+
.withContext('Expected preview container to be removed from the DOM')
36833697
.toBeFalsy();
36843698
expect(item.parentNode)
36853699
.withContext('Expected drag item to be removed from the DOM')
@@ -6548,21 +6562,15 @@ describe('CdkDrag', () => {
65486562
startDraggingViaMouse(fixture, item.element.nativeElement);
65496563
fixture.detectChanges();
65506564

6551-
const initialSelectStart = dispatchFakeEvent(
6552-
shadowRoot,
6553-
'selectstart',
6554-
);
6565+
const initialSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart');
65556566
fixture.detectChanges();
65566567
expect(initialSelectStart.defaultPrevented).toBe(true);
65576568

65586569
dispatchMouseEvent(document, 'mouseup');
65596570
fixture.detectChanges();
65606571
flush();
65616572

6562-
const afterDropSelectStart = dispatchFakeEvent(
6563-
shadowRoot,
6564-
'selectstart',
6565-
);
6573+
const afterDropSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart');
65666574
fixture.detectChanges();
65676575
expect(afterDropSelectStart.defaultPrevented).toBe(false);
65686576
}));

src/cdk/drag-drop/preview-ref.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export class PreviewRef {
3939
/** Reference to the preview element. */
4040
private _preview: HTMLElement;
4141

42+
/** Reference to the preview wrapper. */
43+
private _wrapper: HTMLElement;
44+
4245
constructor(
4346
private _document: Document,
4447
private _rootElement: HTMLElement,
@@ -55,14 +58,21 @@ export class PreviewRef {
5558
) {}
5659

5760
attach(parent: HTMLElement): void {
61+
this._wrapper = this._createWrapper();
5862
this._preview = this._createPreview();
59-
parent.appendChild(this._preview);
63+
this._wrapper.appendChild(this._preview);
64+
parent.appendChild(this._wrapper);
65+
66+
// The null check is necessary for browsers that don't support the popover API.
67+
if (this._wrapper.showPopover) {
68+
this._wrapper.showPopover();
69+
}
6070
}
6171

6272
destroy(): void {
63-
this._preview?.remove();
73+
this._wrapper?.remove();
6474
this._previewEmbeddedView?.destroy();
65-
this._preview = this._previewEmbeddedView = null!;
75+
this._preview = this._wrapper = this._previewEmbeddedView = null!;
6676
}
6777

6878
setTransform(value: string): void {
@@ -89,6 +99,33 @@ export class PreviewRef {
8999
this._preview.removeEventListener(name, handler);
90100
}
91101

102+
private _createWrapper(): HTMLElement {
103+
const wrapper = this._document.createElement('div');
104+
wrapper.setAttribute('popover', 'manual');
105+
wrapper.setAttribute('dir', this._direction);
106+
wrapper.classList.add('cdk-drag-preview-container');
107+
108+
extendStyles(wrapper.style, {
109+
// This is redundant, but we need it for browsers that don't support the popover API.
110+
'position': 'fixed',
111+
'top': '0',
112+
'left': '0',
113+
'width': '100%',
114+
'height': '100%',
115+
'z-index': this._zIndex + '',
116+
117+
// Reset the user agent styles.
118+
'background': 'none',
119+
'border': 'none',
120+
'pointer-events': 'none',
121+
'margin': '0',
122+
'padding': '0',
123+
});
124+
toggleNativeDragInteractions(wrapper, false);
125+
126+
return wrapper;
127+
}
128+
92129
private _createPreview(): HTMLElement {
93130
const previewConfig = this._previewTemplate;
94131
const previewClass = this._previewClass;
@@ -134,14 +171,12 @@ export class PreviewRef {
134171
'position': 'absolute',
135172
'top': '0',
136173
'left': '0',
137-
'z-index': `${this._zIndex}`,
138174
},
139175
importantProperties,
140176
);
141177

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

146181
if (previewClass) {
147182
if (Array.isArray(previewClass)) {

0 commit comments

Comments
 (0)