Skip to content

Commit e33c436

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. (cherry picked from commit 7cd3f02)
1 parent 4826a14 commit e33c436

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
@@ -2482,7 +2482,8 @@ describe('CdkDrag', () => {
24822482

24832483
startDraggingViaMouse(fixture, item);
24842484

2485-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2485+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
2486+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
24862487
const previewRect = preview.getBoundingClientRect();
24872488
const zeroPxRegex = /^0(px)?$/;
24882489

@@ -2504,12 +2505,14 @@ describe('CdkDrag', () => {
25042505
.withContext('Expected element to be removed from layout')
25052506
.toBe('-999em');
25062507
expect(item.style.opacity).withContext('Expected element to be invisible').toBe('0');
2507-
expect(preview).withContext('Expected preview to be in the DOM').toBeTruthy();
2508+
expect(previewContainer)
2509+
.withContext('Expected preview container to be in the DOM')
2510+
.toBeTruthy();
25082511
expect(preview.textContent!.trim())
25092512
.withContext('Expected preview content to match element')
25102513
.toContain('One');
2511-
expect(preview.getAttribute('dir'))
2512-
.withContext('Expected preview element to inherit the directionality.')
2514+
expect(previewContainer.getAttribute('dir'))
2515+
.withContext('Expected preview container element to inherit the directionality.')
25132516
.toBe('ltr');
25142517
expect(previewRect.width)
25152518
.withContext('Expected preview width to match element')
@@ -2520,8 +2523,8 @@ describe('CdkDrag', () => {
25202523
expect(preview.style.pointerEvents)
25212524
.withContext('Expected pointer events to be disabled on the preview')
25222525
.toBe('none');
2523-
expect(preview.style.zIndex)
2524-
.withContext('Expected preview to have a high default zIndex.')
2526+
expect(previewContainer.style.zIndex)
2527+
.withContext('Expected preview container to have a high default zIndex.')
25252528
.toBe('1000');
25262529
// Use a regex here since some browsers normalize 0 to 0px, but others don't.
25272530
// Use a regex here since some browsers normalize 0 to 0px, but others don't.
@@ -2542,8 +2545,8 @@ describe('CdkDrag', () => {
25422545
expect(item.style.top).withContext('Expected element to be within the layout').toBeFalsy();
25432546
expect(item.style.left).withContext('Expected element to be within the layout').toBeFalsy();
25442547
expect(item.style.opacity).withContext('Expected element to be visible').toBeFalsy();
2545-
expect(preview.parentNode)
2546-
.withContext('Expected preview to be removed from the DOM')
2548+
expect(previewContainer.parentNode)
2549+
.withContext('Expected preview container to be removed from the DOM')
25472550
.toBeFalsy();
25482551
}));
25492552

@@ -2561,7 +2564,7 @@ describe('CdkDrag', () => {
25612564
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
25622565
startDraggingViaMouse(fixture, item);
25632566

2564-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2567+
const preview = document.querySelector('.cdk-drag-preview-container')! as HTMLElement;
25652568
expect(preview.style.zIndex).toBe('3000');
25662569
}));
25672570

@@ -2606,9 +2609,11 @@ describe('CdkDrag', () => {
26062609
startDraggingViaMouse(fixture, item);
26072610
flush();
26082611

2609-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2612+
const previewContainer = document.querySelector(
2613+
'.cdk-drag-preview-container',
2614+
)! as HTMLElement;
26102615

2611-
expect(preview.parentNode).toBe(fakeDocument.fullscreenElement);
2616+
expect(previewContainer.parentNode).toBe(fakeDocument.fullscreenElement);
26122617
fakeDocument.fullscreenElement.remove();
26132618
}));
26142619

@@ -2907,8 +2912,8 @@ describe('CdkDrag', () => {
29072912
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
29082913
startDraggingViaMouse(fixture, item);
29092914

2910-
expect(document.querySelector('.cdk-drag-preview')!.getAttribute('dir'))
2911-
.withContext('Expected preview element to inherit the directionality.')
2915+
expect(document.querySelector('.cdk-drag-preview-container')!.getAttribute('dir'))
2916+
.withContext('Expected preview container to inherit the directionality.')
29122917
.toBe('rtl');
29132918
}));
29142919

@@ -2919,7 +2924,8 @@ describe('CdkDrag', () => {
29192924

29202925
startDraggingViaMouse(fixture, item);
29212926

2922-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2927+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
2928+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
29232929

29242930
// Add a duration since the tests won't include one.
29252931
preview.style.transitionDuration = '500ms';
@@ -2932,13 +2938,13 @@ describe('CdkDrag', () => {
29322938
fixture.detectChanges();
29332939
tick(250);
29342940

2935-
expect(preview.parentNode)
2941+
expect(previewContainer.parentNode)
29362942
.withContext('Expected preview to be in the DOM mid-way through the transition')
29372943
.toBeTruthy();
29382944

29392945
tick(500);
29402946

2941-
expect(preview.parentNode)
2947+
expect(previewContainer.parentNode)
29422948
.withContext('Expected preview to be removed from the DOM if the transition timed out')
29432949
.toBeFalsy();
29442950
}));
@@ -3042,6 +3048,7 @@ describe('CdkDrag', () => {
30423048
startDraggingViaMouse(fixture, item);
30433049

30443050
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3051+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
30453052
preview.style.transition = 'opacity 500ms ease';
30463053

30473054
dispatchMouseEvent(document, 'mousemove', 50, 50);
@@ -3051,8 +3058,8 @@ describe('CdkDrag', () => {
30513058
fixture.detectChanges();
30523059
tick(0);
30533060

3054-
expect(preview.parentNode)
3055-
.withContext('Expected preview to be removed from the DOM immediately')
3061+
expect(previewContainer.parentNode)
3062+
.withContext('Expected preview container to be removed from the DOM immediately')
30563063
.toBeFalsy();
30573064
}));
30583065

@@ -3064,6 +3071,7 @@ describe('CdkDrag', () => {
30643071
startDraggingViaMouse(fixture, item);
30653072

30663073
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3074+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
30673075
preview.style.transition = 'opacity 500ms ease, transform 1000ms ease';
30683076

30693077
dispatchMouseEvent(document, 'mousemove', 50, 50);
@@ -3073,15 +3081,17 @@ describe('CdkDrag', () => {
30733081
fixture.detectChanges();
30743082
tick(500);
30753083

3076-
expect(preview.parentNode)
3077-
.withContext('Expected preview to be in the DOM at the end of the opacity transition')
3084+
expect(previewContainer.parentNode)
3085+
.withContext(
3086+
'Expected preview container to be in the DOM at the end of the opacity transition',
3087+
)
30783088
.toBeTruthy();
30793089

30803090
tick(1000);
30813091

3082-
expect(preview.parentNode)
3092+
expect(previewContainer.parentNode)
30833093
.withContext(
3084-
'Expected preview to be removed from the DOM at the end of the ' + 'transform transition',
3094+
'Expected preview container to be removed from the DOM at the end of the transform transition',
30853095
)
30863096
.toBeFalsy();
30873097
}));
@@ -3123,8 +3133,8 @@ describe('CdkDrag', () => {
31233133
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
31243134

31253135
startDraggingViaMouse(fixture, item);
3126-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3127-
expect(preview.parentNode).toBe(document.body);
3136+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
3137+
expect(previewContainer.parentNode).toBe(document.body);
31283138
}));
31293139

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

31373147
startDraggingViaMouse(fixture, item);
3138-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3148+
const previewContainer = document.querySelector('.cdk-drag-preview-container') as HTMLElement;
31393149
expect(list).toBeTruthy();
3140-
expect(preview.parentNode).toBe(list);
3150+
expect(previewContainer.parentNode).toBe(list);
31413151
}));
31423152

31433153
it('should insert the preview into a particular element, if specified', fakeAsync(() => {
@@ -3151,8 +3161,10 @@ describe('CdkDrag', () => {
31513161
fixture.detectChanges();
31523162

31533163
startDraggingViaMouse(fixture, item);
3154-
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
3155-
expect(preview.parentNode).toBe(previewContainer.nativeElement);
3164+
const previewContainerElement = document.querySelector(
3165+
'.cdk-drag-preview-container',
3166+
) as HTMLElement;
3167+
expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement);
31563168
}));
31573169

31583170
it('should remove the id from the placeholder', fakeAsync(() => {
@@ -3664,15 +3676,17 @@ describe('CdkDrag', () => {
36643676

36653677
startDraggingViaMouse(fixture, item);
36663678

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

3669-
expect(preview.parentNode).withContext('Expected preview to be in the DOM').toBeTruthy();
3681+
expect(previewContainer.parentNode)
3682+
.withContext('Expected preview container to be in the DOM')
3683+
.toBeTruthy();
36703684
expect(item.parentNode).withContext('Expected drag item to be in the DOM').toBeTruthy();
36713685

36723686
fixture.destroy();
36733687

3674-
expect(preview.parentNode)
3675-
.withContext('Expected preview to be removed from the DOM')
3688+
expect(previewContainer.parentNode)
3689+
.withContext('Expected preview container to be removed from the DOM')
36763690
.toBeFalsy();
36773691
expect(item.parentNode)
36783692
.withContext('Expected drag item to be removed from the DOM')
@@ -6541,21 +6555,15 @@ describe('CdkDrag', () => {
65416555
startDraggingViaMouse(fixture, item.element.nativeElement);
65426556
fixture.detectChanges();
65436557

6544-
const initialSelectStart = dispatchFakeEvent(
6545-
shadowRoot,
6546-
'selectstart',
6547-
);
6558+
const initialSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart');
65486559
fixture.detectChanges();
65496560
expect(initialSelectStart.defaultPrevented).toBe(true);
65506561

65516562
dispatchMouseEvent(document, 'mouseup');
65526563
fixture.detectChanges();
65536564
flush();
65546565

6555-
const afterDropSelectStart = dispatchFakeEvent(
6556-
shadowRoot,
6557-
'selectstart',
6558-
);
6566+
const afterDropSelectStart = dispatchFakeEvent(shadowRoot, 'selectstart');
65596567
fixture.detectChanges();
65606568
expect(afterDropSelectStart.defaultPrevented).toBe(false);
65616569
}));

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)