Skip to content

Commit 0309adf

Browse files
committed
fix(cdk/drag-drop): allow for the popover wrapper to be disabled
Adds an API that allows for the popover wrapper around the preview to be disabled. The popover can intefere with styling in some edge cases.
1 parent fdbfa95 commit 0309adf

File tree

6 files changed

+91
-20
lines changed

6 files changed

+91
-20
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
4444
listOrientation?: DropListOrientation;
4545
zIndex?: number;
4646
previewContainer?: 'global' | 'parent';
47+
disablePreviewPopover?: boolean;
4748
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3183,6 +3183,20 @@ describe('CdkDrag', () => {
31833183
expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement);
31843184
}));
31853185

3186+
it('should not create a popover wrapper if disablePreviewPopover is enabled', fakeAsync(() => {
3187+
const fixture = createComponent(DraggableInDropZone);
3188+
fixture.componentInstance.previewContainer = 'global';
3189+
fixture.componentInstance.disablePreviewPopover = true;
3190+
fixture.detectChanges();
3191+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
3192+
3193+
startDraggingViaMouse(fixture, item);
3194+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
3195+
expect(document.querySelector('.cdk-drag-preview-container')).toBeFalsy();
3196+
expect(preview).toBeTruthy();
3197+
expect(preview.parentElement).toBe(document.body);
3198+
}));
3199+
31863200
it('should remove the id from the placeholder', fakeAsync(() => {
31873201
const fixture = createComponent(DraggableInDropZone);
31883202
fixture.detectChanges();
@@ -6936,6 +6950,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
69366950
[cdkDragBoundary]="boundarySelector"
69376951
[cdkDragPreviewClass]="previewClass"
69386952
[cdkDragPreviewContainer]="previewContainer"
6953+
[cdkDragDisablePreviewPopover]="disablePreviewPopover"
69396954
[style.height.px]="item.height"
69406955
[style.margin-bottom.px]="item.margin"
69416956
(cdkDragStarted)="startedSpy($event)"
@@ -6965,6 +6980,7 @@ class DraggableInDropZone implements AfterViewInit {
69656980
});
69666981
startedSpy = jasmine.createSpy('started spy');
69676982
previewContainer: PreviewContainer = 'global';
6983+
disablePreviewPopover = false;
69686984

69696985
constructor(protected _elementRef: ElementRef) {}
69706986

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,20 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
159159
*/
160160
@Input('cdkDragPreviewContainer') previewContainer: PreviewContainer;
161161

162+
/**
163+
* By default the preview element is wrapped in a native popover in order to be compatible
164+
* with other native popovers and to avoid issues with `overflow: hidden`. In some edge cases
165+
* this can interfere with styling (e.g. CSS selectors targeting direct descendants). Enable
166+
* this option to remove the wrapper around the preview, but note that it can cause the following
167+
* issues when used with `cdkDragPreviewContainer` set to `parent` or a specific DOM node:
168+
* - The preview may be clipped by a parent with `overflow: hidden`.
169+
* - The preview isn't guaranteed to be on top of other elements, despite its `z-index`.
170+
* - Transforms on the parent of the preview can affect its positioning.
171+
* - The preview may be positioned under native `<dialog>` or popover elements.
172+
*/
173+
@Input({alias: 'cdkDragDisablePreviewPopover', transform: booleanAttribute})
174+
disablePreviewPopover: boolean;
175+
162176
/** Emits when the user starts dragging the item. */
163177
@Output('cdkDragStarted') readonly started: EventEmitter<CdkDragStart> =
164178
new EventEmitter<CdkDragStart>();
@@ -458,7 +472,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
458472
.withBoundaryElement(this._getBoundaryElement())
459473
.withPlaceholderTemplate(placeholder)
460474
.withPreviewTemplate(preview)
461-
.withPreviewContainer(this.previewContainer || 'global');
475+
.withPreviewContainer(this.previewContainer || 'global', this.disablePreviewPopover);
462476

463477
if (dir) {
464478
ref.withDirection(dir.value);
@@ -559,10 +573,12 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
559573
draggingDisabled,
560574
rootElementSelector,
561575
previewContainer,
576+
disablePreviewPopover,
562577
} = config;
563578

564579
this.disabled = draggingDisabled == null ? false : draggingDisabled;
565580
this.dragStartDelay = dragStartDelay || 0;
581+
this.disablePreviewPopover = disablePreviewPopover || false;
566582

567583
if (lockAxis) {
568584
this.lockAxis = lockAxis;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export class DragRef<T = any> {
119119
/** Container into which to insert the preview. */
120120
private _previewContainer: PreviewContainer | undefined;
121121

122+
/** Whether to disable the popover wrapper around the preview. */
123+
private _disablePreviewPopover: boolean;
124+
122125
/** Reference to the view of the placeholder element. */
123126
private _placeholderRef: EmbeddedViewRef<any> | null;
124127

@@ -591,9 +594,11 @@ export class DragRef<T = any> {
591594
/**
592595
* Sets the container into which to insert the preview element.
593596
* @param value Container into which to insert the preview.
597+
* @param disablePreviewPopover Whether to disable the popover wrapper around the preview.
594598
*/
595-
withPreviewContainer(value: PreviewContainer): this {
599+
withPreviewContainer(value: PreviewContainer, disablePreviewPopover = false): this {
596600
this._previewContainer = value;
601+
this._disablePreviewPopover = disablePreviewPopover;
597602
return this;
598603
}
599604

@@ -831,6 +836,7 @@ export class DragRef<T = any> {
831836
this._rootElement,
832837
this._direction,
833838
this._initialDomRect!,
839+
this._disablePreviewPopover,
834840
this._previewTemplate || null,
835841
this.previewClass || null,
836842
this._pickupPositionOnPage,

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

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

42-
/** Reference to the preview wrapper. */
43-
private _wrapper: HTMLElement;
42+
/**
43+
* Reference to the preview popover wrapper.
44+
* May not be created if `_disablePopover` is enabled.
45+
*/
46+
private _popover: HTMLElement | null;
4447

4548
constructor(
4649
private _document: Document,
4750
private _rootElement: HTMLElement,
4851
private _direction: Direction,
4952
private _initialDomRect: DOMRect,
53+
private _disablePopover: boolean,
5054
private _previewTemplate: DragPreviewTemplate | null,
5155
private _previewClass: string | string[] | null,
5256
private _pickupPositionOnPage: {
@@ -58,22 +62,34 @@ export class PreviewRef {
5862
) {}
5963

6064
attach(parent: HTMLElement): void {
61-
this._wrapper = this._createWrapper();
6265
this._preview = this._createPreview();
63-
this._wrapper.appendChild(this._preview);
64-
parent.appendChild(this._wrapper);
6566

66-
// The null check is necessary for browsers that don't support the popover API.
67-
// Note that we use a string access for compatibility with Closure.
68-
if ('showPopover' in this._wrapper) {
69-
this._wrapper['showPopover']();
67+
if (this._disablePopover) {
68+
this._styleRootElement(this._preview);
69+
parent.appendChild(this._preview);
70+
} else {
71+
this._popover = this._createWrapper();
72+
this._styleRootElement(this._popover);
73+
this._popover.appendChild(this._preview);
74+
parent.appendChild(this._popover);
75+
76+
// The null check is necessary for browsers that don't support the popover API.
77+
// Note that we use a string access for compatibility with Closure.
78+
if ('showPopover' in this._popover) {
79+
this._popover['showPopover']();
80+
}
7081
}
7182
}
7283

7384
destroy(): void {
74-
this._wrapper?.remove();
85+
if (this._popover) {
86+
this._popover.remove();
87+
} else {
88+
this._preview.remove();
89+
}
90+
7591
this._previewEmbeddedView?.destroy();
76-
this._preview = this._wrapper = this._previewEmbeddedView = null!;
92+
this._preview = this._popover = this._previewEmbeddedView = null!;
7793
}
7894

7995
setTransform(value: string): void {
@@ -103,17 +119,13 @@ export class PreviewRef {
103119
private _createWrapper(): HTMLElement {
104120
const wrapper = this._document.createElement('div');
105121
wrapper.setAttribute('popover', 'manual');
106-
wrapper.setAttribute('dir', this._direction);
107122
wrapper.classList.add('cdk-drag-preview-container');
108123

109124
extendStyles(wrapper.style, {
110125
// This is redundant, but we need it for browsers that don't support the popover API.
111-
'position': 'fixed',
112-
'top': '0',
113-
'left': '0',
126+
// The rest of the positioning styles are in `_styleRootElement`.
114127
'width': '100%',
115128
'height': '100%',
116-
'z-index': this._zIndex + '',
117129

118130
// Reset the user agent styles.
119131
'background': 'none',
@@ -190,4 +202,19 @@ export class PreviewRef {
190202

191203
return preview;
192204
}
205+
206+
private _styleRootElement(root: HTMLElement): void {
207+
root.setAttribute('dir', this._direction);
208+
209+
extendStyles(
210+
root.style,
211+
{
212+
'position': 'fixed',
213+
'top': '0',
214+
'left': '0',
215+
'z-index': this._zIndex + '',
216+
},
217+
importantProperties,
218+
);
219+
}
193220
}

tools/public_api_guard/cdk/drag-drop.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
5959
data: T;
6060
get disabled(): boolean;
6161
set disabled(value: boolean);
62+
disablePreviewPopover: boolean;
6263
_dragRef: DragRef<CdkDrag<T>>;
6364
dragStartDelay: DragStartDelay;
6465
dropContainer: CdkDropList;
@@ -76,6 +77,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
7677
// (undocumented)
7778
static ngAcceptInputType_disabled: unknown;
7879
// (undocumented)
80+
static ngAcceptInputType_disablePreviewPopover: unknown;
81+
// (undocumented)
7982
ngAfterViewInit(): void;
8083
// (undocumented)
8184
ngOnChanges(changes: SimpleChanges): void;
@@ -99,7 +102,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
99102
_setPreviewTemplate(preview: CdkDragPreview): void;
100103
readonly started: EventEmitter<CdkDragStart>;
101104
// (undocumented)
102-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>;
105+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; "disablePreviewPopover": { "alias": "cdkDragDisablePreviewPopover"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>;
103106
// (undocumented)
104107
static ɵfac: i0.ɵɵFactoryDeclaration<CdkDrag<any>, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>;
105108
}
@@ -317,6 +320,8 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
317320
// (undocumented)
318321
constrainPosition?: DragConstrainPosition;
319322
// (undocumented)
323+
disablePreviewPopover?: boolean;
324+
// (undocumented)
320325
draggingDisabled?: boolean;
321326
// (undocumented)
322327
dragStartDelay?: DragStartDelay;
@@ -451,7 +456,7 @@ export class DragRef<T = any> {
451456
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
452457
withParent(parent: DragRef<unknown> | null): this;
453458
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
454-
withPreviewContainer(value: PreviewContainer): this;
459+
withPreviewContainer(value: PreviewContainer, disablePreviewPopover?: boolean): this;
455460
withPreviewTemplate(template: DragPreviewTemplate | null): this;
456461
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
457462
}

0 commit comments

Comments
 (0)