Skip to content

Commit

Permalink
feat(cdk/drag-drop): allow for preview container to be customized (#2…
Browse files Browse the repository at this point in the history
…1830)

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.
  • Loading branch information
crisbeto committed Feb 12, 2021
1 parent 5e4d5e0 commit b92f97f
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/cdk/drag-drop/directives/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
listAutoScrollDisabled?: boolean;
listOrientation?: DropListOrientation;
zIndex?: number;
previewContainer?: 'global' | 'parent';
}
52 changes: 50 additions & 2 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {of as observableOf} from 'rxjs';

import {DragDropModule} from '../drag-drop-module';
import {CdkDragDrop, CdkDragEnter, CdkDragStart} from '../drag-events';
import {Point, DragRef} from '../drag-ref';
import {Point, DragRef, PreviewContainer} from '../drag-ref';
import {extendStyles} from '../drag-styling';
import {moveItemInArray} from '../drag-utils';

Expand Down Expand Up @@ -1235,7 +1235,8 @@ describe('CdkDrag', () => {
constrainPosition: () => ({x: 1337, y: 42}),
previewClass: 'custom-preview-class',
boundaryElement: '.boundary',
rootElementSelector: '.root'
rootElementSelector: '.root',
previewContainer: 'parent'
};

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

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

it('should insert the preview into the `body` if previewContainer is set to `global`',
fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.previewContainer = 'global';
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.parentNode).toBe(document.body);
}));

it('should insert the preview into the parent node if previewContainer is set to `parent`',
fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.previewContainer = 'parent';
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const list = fixture.nativeElement.querySelector('.drop-list');

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(list).toBeTruthy();
expect(preview.parentNode).toBe(list);
}));

it('should insert the preview into a particular element, if specified', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const previewContainer = fixture.componentInstance.alternatePreviewContainer;

expect(previewContainer).toBeTruthy();
fixture.componentInstance.previewContainer = previewContainer;
fixture.detectChanges();

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.parentNode).toBe(previewContainer.nativeElement);
}));

it('should remove the id from the placeholder', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
Expand Down Expand Up @@ -5789,17 +5832,21 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
[cdkDragData]="item"
[cdkDragBoundary]="boundarySelector"
[cdkDragPreviewClass]="previewClass"
[cdkDragPreviewContainer]="previewContainer"
[style.height.px]="item.height"
[style.margin-bottom.px]="item.margin"
(cdkDragStarted)="startedSpy($event)"
style="width: 100%; background: red;">{{item.value}}</div>
</div>
<div #alternatePreviewContainer></div>
`;

@Component({template: DROP_ZONE_FIXTURE_TEMPLATE})
class DraggableInDropZone implements AfterViewInit {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
@ViewChild(CdkDropList) dropInstance: CdkDropList;
@ViewChild('alternatePreviewContainer') alternatePreviewContainer: ElementRef<HTMLElement>;
items = [
{value: 'Zero', height: ITEM_HEIGHT, margin: 0},
{value: 'One', height: ITEM_HEIGHT, margin: 0},
Expand All @@ -5814,6 +5861,7 @@ class DraggableInDropZone implements AfterViewInit {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
});
startedSpy = jasmine.createSpy('started spy');
previewContainer: PreviewContainer = 'global';

constructor(protected _elementRef: ElementRef) {}

Expand Down
28 changes: 24 additions & 4 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {CDK_DRAG_HANDLE, CdkDragHandle} from './drag-handle';
import {CDK_DRAG_PLACEHOLDER, CdkDragPlaceholder} from './drag-placeholder';
import {CDK_DRAG_PREVIEW, CdkDragPreview} from './drag-preview';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, Point} from '../drag-ref';
import {DragRef, Point, PreviewContainer} from '../drag-ref';
import {CDK_DROP_LIST, CdkDropListInternal as CdkDropList} from './drop-list';
import {DragDrop} from '../drag-drop';
import {CDK_DRAG_CONFIG, DragDropConfig, DragStartDelay, DragAxis} from './config';
Expand Down Expand Up @@ -140,6 +140,21 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
/** Class to be added to the preview element. */
@Input('cdkDragPreviewClass') previewClass: string | string[];

/**
* Configures the place into which the preview of the item will be inserted. Can be configured
* globally through `CDK_DROP_LIST`. Possible values:
* - `global` - Preview will be inserted at the bottom of the `<body>`. The advantage is that
* you don't have to worry about `overflow: hidden` or `z-index`, but the item won't retain
* its inherited styles.
* - `parent` - Preview will be inserted into the parent of the drag item. The advantage is that
* inherited styles will be preserved, but it may be clipped by `overflow: hidden` or not be
* visible due to `z-index`. Furthermore, the preview is going to have an effect over selectors
* like `:nth-child` and some flexbox configurations.
* - `ElementRef<HTMLElement> | HTMLElement` - Preview will be inserted into a specific element.
* Same advantages and disadvantages as `parent`.
*/
@Input('cdkDragPreviewContainer') previewContainer: PreviewContainer;

/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();

Expand Down Expand Up @@ -396,7 +411,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
ref
.withBoundaryElement(this._getBoundaryElement())
.withPlaceholderTemplate(placeholder)
.withPreviewTemplate(preview);
.withPreviewTemplate(preview)
.withPreviewContainer(this.previewContainer || 'global');

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

this.disabled = draggingDisabled == null ? false : draggingDisabled;
Expand All @@ -507,6 +523,10 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
if (rootElementSelector) {
this.rootElementSelector = rootElementSelector;
}

if (previewContainer) {
this.previewContainer = previewContainer;
}
}

static ngAcceptInputType_disabled: BooleanInput;
Expand Down
13 changes: 13 additions & 0 deletions src/cdk/drag-drop/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ to be applied.

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

### Drag preview insertion point
By default, the preview of a `cdkDrag` will be inserted into the `<body>` of the page in order to
avoid issues with `z-index` and `overflow: hidden`. This may not be desireable in some cases,
because the preview won't retain its inherited styles. You can control where the preview is inserted
using the `cdkDrawPreviewContainer` input. The possible values are:

| Value | Description | Advantages | Disadvantages |
|-------------------|-------------------------|------------------------|---------------------------|
| `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.
| `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.
| `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.


### Customizing the drag placeholder
While a `cdkDrag` element is being dragged, the CDK will create a placeholder element that will
show where it will be placed when it's dropped. By default the placeholder is a clone of the element
Expand Down
69 changes: 54 additions & 15 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ export interface Point {
y: number;
}

/**
* Possible places into which the preview of a drag item can be inserted.
* - `global` - Preview will be inserted at the bottom of the `<body>`. The advantage is that
* you don't have to worry about `overflow: hidden` or `z-index`, but the item won't retain
* its inherited styles.
* - `parent` - Preview will be inserted into the parent of the drag item. The advantage is that
* inherited styles will be preserved, but it may be clipped by `overflow: hidden` or not be
* visible due to `z-index`. Furthermore, the preview is going to have an effect over selectors
* like `:nth-child` and some flexbox configurations.
* - `ElementRef<HTMLElement> | HTMLElement` - Preview will be inserted into a specific element.
* Same advantages and disadvantages as `parent`.
*/
export type PreviewContainer = 'global' | 'parent' | ElementRef<HTMLElement> | HTMLElement;

/**
* Reference to a draggable item. Used to manipulate or dispose of the item.
*/
Expand All @@ -93,6 +107,9 @@ export class DragRef<T = any> {
/** Reference to the view of the preview element. */
private _previewRef: EmbeddedViewRef<any> | null;

/** Container into which to insert the preview. */
private _previewContainer: PreviewContainer | undefined;

/** Reference to the view of the placeholder element. */
private _placeholderRef: EmbeddedViewRef<any> | null;

Expand Down Expand Up @@ -542,6 +559,15 @@ export class DragRef<T = any> {
return this;
}

/**
* Sets the container into which to insert the preview element.
* @param value Container into which to insert the preview.
*/
withPreviewContainer(value: PreviewContainer): this {
this._previewContainer = value;
return this;
}

/** Updates the item's sort order based on the last-known pointer position. */
_sortFromLastPointerPosition() {
const position = this._lastKnownPointerPosition;
Expand Down Expand Up @@ -762,7 +788,7 @@ export class DragRef<T = any> {

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

return this._cachedShadowRoot;
}

/** Gets the element into which the drag preview should be inserted. */
private _getPreviewInsertionPoint(initialParent: HTMLElement,
shadowRoot: ShadowRoot | null): HTMLElement {
const previewContainer = this._previewContainer || 'global';

if (previewContainer === 'parent') {
return initialParent;
}

if (previewContainer === 'global') {
const documentRef = this._document;

// We can't use the body if the user is in fullscreen mode,
// because the preview will render under the fullscreen element.
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
return shadowRoot ||
documentRef.fullscreenElement ||
(documentRef as any).webkitFullscreenElement ||
(documentRef as any).mozFullScreenElement ||
(documentRef as any).msFullscreenElement ||
documentRef.body;
}

return coerceElement(previewContainer);
}
}

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

/** Gets the element into which the drag preview should be inserted. */
function getPreviewInsertionPoint(documentRef: any, shadowRoot: ShadowRoot | null): HTMLElement {
// We can't use the body if the user is in fullscreen mode,
// because the preview will render under the fullscreen element.
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
return shadowRoot ||
documentRef.fullscreenElement ||
documentRef.webkitFullscreenElement ||
documentRef.mozFullScreenElement ||
documentRef.msFullscreenElement ||
documentRef.body;
}

/**
* Gets the root HTML element of an embedded view.
* If the root is not an HTML element it gets wrapped in one.
Expand Down
2 changes: 1 addition & 1 deletion src/cdk/drag-drop/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

export {DragDrop} from './drag-drop';
export {DragRef, DragRefConfig, Point} from './drag-ref';
export {DragRef, DragRefConfig, Point, PreviewContainer} from './drag-ref';
export {DropListRef} from './drop-list-ref';
export {CDK_DRAG_PARENT} from './drag-parent';

Expand Down
2 changes: 1 addition & 1 deletion src/dev-app/drag-drop/drag-drop-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
justify-content: space-between;
box-sizing: border-box;

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

Expand Down
7 changes: 6 additions & 1 deletion tools/public_api_guard/cdk/drag-drop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
lockAxis: DragAxis;
moved: Observable<CdkDragMove<T>>;
previewClass: string | string[];
previewContainer: PreviewContainer;
released: EventEmitter<CdkDragRelease>;
rootElementSelector: string;
started: EventEmitter<CdkDragStart>;
Expand All @@ -54,7 +55,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
ngOnDestroy(): void;
reset(): void;
static ngAcceptInputType_disabled: BooleanInput;
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"]>;
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"]>;
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; }]>;
}

Expand Down Expand Up @@ -220,6 +221,7 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
listOrientation?: DropListOrientation;
lockAxis?: DragAxis;
previewClass?: string | string[];
previewContainer?: 'global' | 'parent';
rootElementSelector?: string;
sortingDisabled?: boolean;
zIndex?: number;
Expand Down Expand Up @@ -320,6 +322,7 @@ export declare class DragRef<T = any> {
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
withParent(parent: DragRef<unknown> | null): this;
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
withPreviewContainer(value: PreviewContainer): this;
withPreviewTemplate(template: DragPreviewTemplate | null): this;
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
}
Expand Down Expand Up @@ -408,4 +411,6 @@ export interface Point {
y: number;
}

export declare type PreviewContainer = 'global' | 'parent' | ElementRef<HTMLElement> | HTMLElement;

export declare function transferArrayItem<T = any>(currentArray: T[], targetArray: T[], currentIndex: number, targetIndex: number): void;

0 comments on commit b92f97f

Please sign in to comment.