Skip to content

Commit b92f97f

Browse files
authored
feat(cdk/drag-drop): allow for preview container to be customized (#21830)
Currently we always insert the drag preview at the `body`, because it allows us to avoid dealing with `overflow` and `z-index`. The problem is that it doesn't allow the preview to retain its inherited styles. These changes add a new input which allows the consumer to configure the place into which the preview will be inserted. Fixes #13288.
1 parent 5e4d5e0 commit b92f97f

File tree

8 files changed

+150
-24
lines changed

8 files changed

+150
-24
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
4343
listAutoScrollDisabled?: boolean;
4444
listOrientation?: DropListOrientation;
4545
zIndex?: number;
46+
previewContainer?: 'global' | 'parent';
4647
}

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

+50-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {of as observableOf} from 'rxjs';
2929

3030
import {DragDropModule} from '../drag-drop-module';
3131
import {CdkDragDrop, CdkDragEnter, CdkDragStart} from '../drag-events';
32-
import {Point, DragRef} from '../drag-ref';
32+
import {Point, DragRef, PreviewContainer} from '../drag-ref';
3333
import {extendStyles} from '../drag-styling';
3434
import {moveItemInArray} from '../drag-utils';
3535

@@ -1235,7 +1235,8 @@ describe('CdkDrag', () => {
12351235
constrainPosition: () => ({x: 1337, y: 42}),
12361236
previewClass: 'custom-preview-class',
12371237
boundaryElement: '.boundary',
1238-
rootElementSelector: '.root'
1238+
rootElementSelector: '.root',
1239+
previewContainer: 'parent'
12391240
};
12401241

12411242
const fixture = createComponent(PlainStandaloneDraggable, [{
@@ -1251,6 +1252,7 @@ describe('CdkDrag', () => {
12511252
expect(drag.previewClass).toBe('custom-preview-class');
12521253
expect(drag.boundaryElement).toBe('.boundary');
12531254
expect(drag.rootElementSelector).toBe('.root');
1255+
expect(drag.previewContainer).toBe('parent');
12541256
}));
12551257

12561258
it('should not throw if touches and changedTouches are empty', fakeAsync(() => {
@@ -2580,6 +2582,47 @@ describe('CdkDrag', () => {
25802582
expect(placeholder.parentNode).toBeFalsy('Expected placeholder to be removed from the DOM');
25812583
}));
25822584

2585+
it('should insert the preview into the `body` if previewContainer is set to `global`',
2586+
fakeAsync(() => {
2587+
const fixture = createComponent(DraggableInDropZone);
2588+
fixture.componentInstance.previewContainer = 'global';
2589+
fixture.detectChanges();
2590+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2591+
2592+
startDraggingViaMouse(fixture, item);
2593+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2594+
expect(preview.parentNode).toBe(document.body);
2595+
}));
2596+
2597+
it('should insert the preview into the parent node if previewContainer is set to `parent`',
2598+
fakeAsync(() => {
2599+
const fixture = createComponent(DraggableInDropZone);
2600+
fixture.componentInstance.previewContainer = 'parent';
2601+
fixture.detectChanges();
2602+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2603+
const list = fixture.nativeElement.querySelector('.drop-list');
2604+
2605+
startDraggingViaMouse(fixture, item);
2606+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2607+
expect(list).toBeTruthy();
2608+
expect(preview.parentNode).toBe(list);
2609+
}));
2610+
2611+
it('should insert the preview into a particular element, if specified', fakeAsync(() => {
2612+
const fixture = createComponent(DraggableInDropZone);
2613+
fixture.detectChanges();
2614+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2615+
const previewContainer = fixture.componentInstance.alternatePreviewContainer;
2616+
2617+
expect(previewContainer).toBeTruthy();
2618+
fixture.componentInstance.previewContainer = previewContainer;
2619+
fixture.detectChanges();
2620+
2621+
startDraggingViaMouse(fixture, item);
2622+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2623+
expect(preview.parentNode).toBe(previewContainer.nativeElement);
2624+
}));
2625+
25832626
it('should remove the id from the placeholder', fakeAsync(() => {
25842627
const fixture = createComponent(DraggableInDropZone);
25852628
fixture.detectChanges();
@@ -5789,17 +5832,21 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
57895832
[cdkDragData]="item"
57905833
[cdkDragBoundary]="boundarySelector"
57915834
[cdkDragPreviewClass]="previewClass"
5835+
[cdkDragPreviewContainer]="previewContainer"
57925836
[style.height.px]="item.height"
57935837
[style.margin-bottom.px]="item.margin"
57945838
(cdkDragStarted)="startedSpy($event)"
57955839
style="width: 100%; background: red;">{{item.value}}</div>
57965840
</div>
5841+
5842+
<div #alternatePreviewContainer></div>
57975843
`;
57985844

57995845
@Component({template: DROP_ZONE_FIXTURE_TEMPLATE})
58005846
class DraggableInDropZone implements AfterViewInit {
58015847
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
58025848
@ViewChild(CdkDropList) dropInstance: CdkDropList;
5849+
@ViewChild('alternatePreviewContainer') alternatePreviewContainer: ElementRef<HTMLElement>;
58035850
items = [
58045851
{value: 'Zero', height: ITEM_HEIGHT, margin: 0},
58055852
{value: 'One', height: ITEM_HEIGHT, margin: 0},
@@ -5814,6 +5861,7 @@ class DraggableInDropZone implements AfterViewInit {
58145861
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
58155862
});
58165863
startedSpy = jasmine.createSpy('started spy');
5864+
previewContainer: PreviewContainer = 'global';
58175865

58185866
constructor(protected _elementRef: ElementRef) {}
58195867

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

+24-4
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import {CDK_DRAG_HANDLE, CdkDragHandle} from './drag-handle';
5050
import {CDK_DRAG_PLACEHOLDER, CdkDragPlaceholder} from './drag-placeholder';
5151
import {CDK_DRAG_PREVIEW, CdkDragPreview} from './drag-preview';
5252
import {CDK_DRAG_PARENT} from '../drag-parent';
53-
import {DragRef, Point} from '../drag-ref';
53+
import {DragRef, Point, PreviewContainer} from '../drag-ref';
5454
import {CDK_DROP_LIST, CdkDropListInternal as CdkDropList} from './drop-list';
5555
import {DragDrop} from '../drag-drop';
5656
import {CDK_DRAG_CONFIG, DragDropConfig, DragStartDelay, DragAxis} from './config';
@@ -140,6 +140,21 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
140140
/** Class to be added to the preview element. */
141141
@Input('cdkDragPreviewClass') previewClass: string | string[];
142142

143+
/**
144+
* Configures the place into which the preview of the item will be inserted. Can be configured
145+
* globally through `CDK_DROP_LIST`. Possible values:
146+
* - `global` - Preview will be inserted at the bottom of the `<body>`. The advantage is that
147+
* you don't have to worry about `overflow: hidden` or `z-index`, but the item won't retain
148+
* its inherited styles.
149+
* - `parent` - Preview will be inserted into the parent of the drag item. The advantage is that
150+
* inherited styles will be preserved, but it may be clipped by `overflow: hidden` or not be
151+
* visible due to `z-index`. Furthermore, the preview is going to have an effect over selectors
152+
* like `:nth-child` and some flexbox configurations.
153+
* - `ElementRef<HTMLElement> | HTMLElement` - Preview will be inserted into a specific element.
154+
* Same advantages and disadvantages as `parent`.
155+
*/
156+
@Input('cdkDragPreviewContainer') previewContainer: PreviewContainer;
157+
143158
/** Emits when the user starts dragging the item. */
144159
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
145160

@@ -396,7 +411,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
396411
ref
397412
.withBoundaryElement(this._getBoundaryElement())
398413
.withPlaceholderTemplate(placeholder)
399-
.withPreviewTemplate(preview);
414+
.withPreviewTemplate(preview)
415+
.withPreviewContainer(this.previewContainer || 'global');
400416

401417
if (dir) {
402418
ref.withDirection(dir.value);
@@ -481,8 +497,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
481497
/** Assigns the default input values based on a provided config object. */
482498
private _assignDefaults(config: DragDropConfig) {
483499
const {
484-
lockAxis, dragStartDelay, constrainPosition, previewClass,
485-
boundaryElement, draggingDisabled, rootElementSelector
500+
lockAxis, dragStartDelay, constrainPosition, previewClass, boundaryElement, draggingDisabled,
501+
rootElementSelector, previewContainer
486502
} = config;
487503

488504
this.disabled = draggingDisabled == null ? false : draggingDisabled;
@@ -507,6 +523,10 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
507523
if (rootElementSelector) {
508524
this.rootElementSelector = rootElementSelector;
509525
}
526+
527+
if (previewContainer) {
528+
this.previewContainer = previewContainer;
529+
}
510530
}
511531

512532
static ngAcceptInputType_disabled: BooleanInput;

src/cdk/drag-drop/drag-drop.md

+13
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,19 @@ to be applied.
128128

129129
<!-- example(cdk-drag-drop-custom-preview) -->
130130

131+
### Drag preview insertion point
132+
By default, the preview of a `cdkDrag` will be inserted into the `<body>` of the page in order to
133+
avoid issues with `z-index` and `overflow: hidden`. This may not be desireable in some cases,
134+
because the preview won't retain its inherited styles. You can control where the preview is inserted
135+
using the `cdkDrawPreviewContainer` input. The possible values are:
136+
137+
| Value | Description | Advantages | Disadvantages |
138+
|-------------------|-------------------------|------------------------|---------------------------|
139+
| `global` | Default value. Preview is inserted into the `<body>` or the closest shadow root. | Preview won't be affected by `z-index` or `overflow: hidden`. It also won't affect `:nth-child` selectors and flex layouts. | Doesn't retain inherited styles.
140+
| `parent` | Preview is inserted inside the parent of the item that is being dragged. | Preview inherits the same styles as the dragged item. | Preview may be clipped by `overflow: hidden` or be placed under other elements due to `z-index`. Furthermore, it can affect `:nth-child` selectors and some flex layouts.
141+
| `ElementRef` or `HTMLElement` | Preview will be inserted into the specified element. | Preview inherits styles from the specified container element. | Preview may be clipped by `overflow: hidden` or be placed under other elements due to `z-index`. Furthermore, it can affect `:nth-child` selectors and some flex layouts.
142+
143+
131144
### Customizing the drag placeholder
132145
While a `cdkDrag` element is being dragged, the CDK will create a placeholder element that will
133146
show where it will be placed when it's dropped. By default the placeholder is a clone of the element

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

+54-15
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ export interface Point {
8383
y: number;
8484
}
8585

86+
/**
87+
* Possible places into which the preview of a drag item can be inserted.
88+
* - `global` - Preview will be inserted at the bottom of the `<body>`. The advantage is that
89+
* you don't have to worry about `overflow: hidden` or `z-index`, but the item won't retain
90+
* its inherited styles.
91+
* - `parent` - Preview will be inserted into the parent of the drag item. The advantage is that
92+
* inherited styles will be preserved, but it may be clipped by `overflow: hidden` or not be
93+
* visible due to `z-index`. Furthermore, the preview is going to have an effect over selectors
94+
* like `:nth-child` and some flexbox configurations.
95+
* - `ElementRef<HTMLElement> | HTMLElement` - Preview will be inserted into a specific element.
96+
* Same advantages and disadvantages as `parent`.
97+
*/
98+
export type PreviewContainer = 'global' | 'parent' | ElementRef<HTMLElement> | HTMLElement;
99+
86100
/**
87101
* Reference to a draggable item. Used to manipulate or dispose of the item.
88102
*/
@@ -93,6 +107,9 @@ export class DragRef<T = any> {
93107
/** Reference to the view of the preview element. */
94108
private _previewRef: EmbeddedViewRef<any> | null;
95109

110+
/** Container into which to insert the preview. */
111+
private _previewContainer: PreviewContainer | undefined;
112+
96113
/** Reference to the view of the placeholder element. */
97114
private _placeholderRef: EmbeddedViewRef<any> | null;
98115

@@ -542,6 +559,15 @@ export class DragRef<T = any> {
542559
return this;
543560
}
544561

562+
/**
563+
* Sets the container into which to insert the preview element.
564+
* @param value Container into which to insert the preview.
565+
*/
566+
withPreviewContainer(value: PreviewContainer): this {
567+
this._previewContainer = value;
568+
return this;
569+
}
570+
545571
/** Updates the item's sort order based on the last-known pointer position. */
546572
_sortFromLastPointerPosition() {
547573
const position = this._lastKnownPointerPosition;
@@ -762,7 +788,7 @@ export class DragRef<T = any> {
762788

763789
if (dropContainer) {
764790
const element = this._rootElement;
765-
const parent = element.parentNode!;
791+
const parent = element.parentNode as HTMLElement;
766792
const preview = this._preview = this._createPreviewElement();
767793
const placeholder = this._placeholder = this._createPlaceholderElement();
768794
const anchor = this._anchor = this._anchor || this._document.createComment('');
@@ -778,7 +804,7 @@ export class DragRef<T = any> {
778804
// from the DOM completely, because iOS will stop firing all subsequent events in the chain.
779805
toggleVisibility(element, false);
780806
this._document.body.appendChild(parent.replaceChild(placeholder, element));
781-
getPreviewInsertionPoint(this._document, shadowRoot).appendChild(preview);
807+
this._getPreviewInsertionPoint(parent, shadowRoot).appendChild(preview);
782808
this.started.next({source: this}); // Emit before notifying the container.
783809
dropContainer.start();
784810
this._initialContainer = dropContainer;
@@ -1361,6 +1387,32 @@ export class DragRef<T = any> {
13611387

13621388
return this._cachedShadowRoot;
13631389
}
1390+
1391+
/** Gets the element into which the drag preview should be inserted. */
1392+
private _getPreviewInsertionPoint(initialParent: HTMLElement,
1393+
shadowRoot: ShadowRoot | null): HTMLElement {
1394+
const previewContainer = this._previewContainer || 'global';
1395+
1396+
if (previewContainer === 'parent') {
1397+
return initialParent;
1398+
}
1399+
1400+
if (previewContainer === 'global') {
1401+
const documentRef = this._document;
1402+
1403+
// We can't use the body if the user is in fullscreen mode,
1404+
// because the preview will render under the fullscreen element.
1405+
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
1406+
return shadowRoot ||
1407+
documentRef.fullscreenElement ||
1408+
(documentRef as any).webkitFullscreenElement ||
1409+
(documentRef as any).mozFullScreenElement ||
1410+
(documentRef as any).msFullscreenElement ||
1411+
documentRef.body;
1412+
}
1413+
1414+
return coerceElement(previewContainer);
1415+
}
13641416
}
13651417

13661418
/**
@@ -1397,19 +1449,6 @@ function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
13971449
return event.type[0] === 't';
13981450
}
13991451

1400-
/** Gets the element into which the drag preview should be inserted. */
1401-
function getPreviewInsertionPoint(documentRef: any, shadowRoot: ShadowRoot | null): HTMLElement {
1402-
// We can't use the body if the user is in fullscreen mode,
1403-
// because the preview will render under the fullscreen element.
1404-
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
1405-
return shadowRoot ||
1406-
documentRef.fullscreenElement ||
1407-
documentRef.webkitFullscreenElement ||
1408-
documentRef.mozFullScreenElement ||
1409-
documentRef.msFullscreenElement ||
1410-
documentRef.body;
1411-
}
1412-
14131452
/**
14141453
* Gets the root HTML element of an embedded view.
14151454
* If the root is not an HTML element it gets wrapped in one.

src/cdk/drag-drop/public-api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
export {DragDrop} from './drag-drop';
10-
export {DragRef, DragRefConfig, Point} from './drag-ref';
10+
export {DragRef, DragRefConfig, Point, PreviewContainer} from './drag-ref';
1111
export {DropListRef} from './drop-list-ref';
1212
export {CDK_DRAG_PARENT} from './drag-parent';
1313

src/dev-app/drag-drop/drag-drop-demo.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
justify-content: space-between;
4444
box-sizing: border-box;
4545

46-
.cdk-drop-list-dragging &:not(.cdk-drag-placeholder) {
46+
.cdk-drop-list-dragging &:not(.cdk-drag-placeholder):not(.cdk-drag-preview) {
4747
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
4848
}
4949

tools/public_api_guard/cdk/drag-drop.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
3636
lockAxis: DragAxis;
3737
moved: Observable<CdkDragMove<T>>;
3838
previewClass: string | string[];
39+
previewContainer: PreviewContainer;
3940
released: EventEmitter<CdkDragRelease>;
4041
rootElementSelector: string;
4142
started: EventEmitter<CdkDragStart>;
@@ -54,7 +55,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
5455
ngOnDestroy(): void;
5556
reset(): void;
5657
static ngAcceptInputType_disabled: BooleanInput;
57-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": "cdkDragData"; "lockAxis": "cdkDragLockAxis"; "rootElementSelector": "cdkDragRootElement"; "boundaryElement": "cdkDragBoundary"; "dragStartDelay": "cdkDragStartDelay"; "freeDragPosition": "cdkDragFreeDragPosition"; "disabled": "cdkDragDisabled"; "constrainPosition": "cdkDragConstrainPosition"; "previewClass": "cdkDragPreviewClass"; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, ["_previewTemplate", "_placeholderTemplate", "_handles"]>;
58+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDrag<any>, "[cdkDrag]", ["cdkDrag"], { "data": "cdkDragData"; "lockAxis": "cdkDragLockAxis"; "rootElementSelector": "cdkDragRootElement"; "boundaryElement": "cdkDragBoundary"; "dragStartDelay": "cdkDragStartDelay"; "freeDragPosition": "cdkDragFreeDragPosition"; "disabled": "cdkDragDisabled"; "constrainPosition": "cdkDragConstrainPosition"; "previewClass": "cdkDragPreviewClass"; "previewContainer": "cdkDragPreviewContainer"; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, ["_previewTemplate", "_placeholderTemplate", "_handles"]>;
5859
static ɵfac: i0.ɵɵFactoryDef<CdkDrag<any>, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>;
5960
}
6061

@@ -220,6 +221,7 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
220221
listOrientation?: DropListOrientation;
221222
lockAxis?: DragAxis;
222223
previewClass?: string | string[];
224+
previewContainer?: 'global' | 'parent';
223225
rootElementSelector?: string;
224226
sortingDisabled?: boolean;
225227
zIndex?: number;
@@ -320,6 +322,7 @@ export declare class DragRef<T = any> {
320322
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
321323
withParent(parent: DragRef<unknown> | null): this;
322324
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
325+
withPreviewContainer(value: PreviewContainer): this;
323326
withPreviewTemplate(template: DragPreviewTemplate | null): this;
324327
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
325328
}
@@ -408,4 +411,6 @@ export interface Point {
408411
y: number;
409412
}
410413

414+
export declare type PreviewContainer = 'global' | 'parent' | ElementRef<HTMLElement> | HTMLElement;
415+
411416
export declare function transferArrayItem<T = any>(currentArray: T[], targetArray: T[], currentIndex: number, targetIndex: number): void;

0 commit comments

Comments
 (0)