Skip to content

Commit 7b2b8bb

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 bfa293a commit 7b2b8bb

File tree

6 files changed

+90
-19
lines changed

6 files changed

+90
-19
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
@@ -3189,6 +3189,20 @@ describe('CdkDrag', () => {
31893189
expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement);
31903190
}));
31913191

3192+
it('should not create a popover wrapper if disablePreviewPopover is enabled', fakeAsync(() => {
3193+
const fixture = createComponent(DraggableInDropZone);
3194+
fixture.componentInstance.previewContainer = 'global';
3195+
fixture.componentInstance.disablePreviewPopover = true;
3196+
fixture.detectChanges();
3197+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
3198+
3199+
startDraggingViaMouse(fixture, item);
3200+
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
3201+
expect(document.querySelector('.cdk-drag-preview-container')).toBeFalsy();
3202+
expect(preview).toBeTruthy();
3203+
expect(preview.parentElement).toBe(document.body);
3204+
}));
3205+
31923206
it('should remove the id from the placeholder', fakeAsync(() => {
31933207
const fixture = createComponent(DraggableInDropZone);
31943208
fixture.detectChanges();
@@ -6942,6 +6956,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
69426956
[cdkDragBoundary]="boundarySelector"
69436957
[cdkDragPreviewClass]="previewClass"
69446958
[cdkDragPreviewContainer]="previewContainer"
6959+
[cdkDragDisablePreviewPopover]="disablePreviewPopover"
69456960
[style.height.px]="item.height"
69466961
[style.margin-bottom.px]="item.margin"
69476962
(cdkDragStarted)="startedSpy($event)"
@@ -6971,6 +6986,7 @@ class DraggableInDropZone implements AfterViewInit {
69716986
});
69726987
startedSpy = jasmine.createSpy('started spy');
69736988
previewContainer: PreviewContainer = 'global';
6989+
disablePreviewPopover = false;
69746990

69756991
constructor(protected _elementRef: ElementRef) {}
69766992

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: 42 additions & 15 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,21 +62,33 @@ 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-
if (this._wrapper.showPopover) {
68-
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+
if (this._popover.showPopover) {
78+
this._popover.showPopover();
79+
}
6980
}
7081
}
7182

7283
destroy(): void {
73-
this._wrapper?.remove();
84+
if (this._popover) {
85+
this._popover.remove();
86+
} else {
87+
this._preview.remove();
88+
}
89+
7490
this._previewEmbeddedView?.destroy();
75-
this._preview = this._wrapper = this._previewEmbeddedView = null!;
91+
this._preview = this._popover = this._previewEmbeddedView = null!;
7692
}
7793

7894
setTransform(value: string): void {
@@ -102,17 +118,13 @@ export class PreviewRef {
102118
private _createWrapper(): HTMLElement {
103119
const wrapper = this._document.createElement('div');
104120
wrapper.setAttribute('popover', 'manual');
105-
wrapper.setAttribute('dir', this._direction);
106121
wrapper.classList.add('cdk-drag-preview-container');
107122

108123
extendStyles(wrapper.style, {
109124
// 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',
125+
// The rest of the positioning styles are in `_styleRootElement`.
113126
'width': '100%',
114127
'height': '100%',
115-
'z-index': this._zIndex + '',
116128

117129
// Reset the user agent styles.
118130
'background': 'none',
@@ -189,4 +201,19 @@ export class PreviewRef {
189201

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

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)