Skip to content

Commit

Permalink
fix(cdk/drag-drop): allow for the popover wrapper to be disabled
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Apr 29, 2024
1 parent bfd6c83 commit fd11606
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 19 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 @@ -44,4 +44,5 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
listOrientation?: DropListOrientation;
zIndex?: number;
previewContainer?: 'global' | 'parent';
disablePreviewPopover?: boolean;
}
16 changes: 16 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3183,6 +3183,20 @@ describe('CdkDrag', () => {
expect(previewContainerElement.parentNode).toBe(previewContainer.nativeElement);
}));

it('should not create a popover wrapper if disablePreviewPopover is enabled', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.previewContainer = 'global';
fixture.componentInstance.disablePreviewPopover = true;
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview') as HTMLElement;
expect(document.querySelector('.cdk-drag-preview-container')).toBeFalsy();
expect(preview).toBeTruthy();
expect(preview.parentElement).toBe(document.body);
}));

it('should remove the id from the placeholder', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
Expand Down Expand Up @@ -6936,6 +6950,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
[cdkDragBoundary]="boundarySelector"
[cdkDragPreviewClass]="previewClass"
[cdkDragPreviewContainer]="previewContainer"
[cdkDragDisablePreviewPopover]="disablePreviewPopover"
[style.height.px]="item.height"
[style.margin-bottom.px]="item.margin"
(cdkDragStarted)="startedSpy($event)"
Expand Down Expand Up @@ -6965,6 +6980,7 @@ class DraggableInDropZone implements AfterViewInit {
});
startedSpy = jasmine.createSpy('started spy');
previewContainer: PreviewContainer = 'global';
disablePreviewPopover = false;

constructor(protected _elementRef: ElementRef) {}

Expand Down
18 changes: 17 additions & 1 deletion src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,20 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
*/
@Input('cdkDragPreviewContainer') previewContainer: PreviewContainer;

/**
* By default the preview element is wrapped in a native popover in order to be compatible
* with other native popovers and to avoid issues with `overflow: hidden`. In some edge cases
* this can interfere with styling (e.g. CSS selectors targeting direct descendants). Enable
* this option to remove the wrapper around the preview, but note that it can cause the following
* issues when used with `cdkDragPreviewContainer` set to `parent` or a specific DOM node:
* - The preview may be clipped by a parent with `overflow: hidden`.
* - The preview isn't guaranteed to be on top of other elements, despite its `z-index`.
* - Transforms on the parent of the preview can affect its positioning.
* - The preview may be positioned under native `<dialog>` or popover elements.
*/
@Input({alias: 'cdkDragDisablePreviewPopover', transform: booleanAttribute})
disablePreviewPopover: boolean;

/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') readonly started: EventEmitter<CdkDragStart> =
new EventEmitter<CdkDragStart>();
Expand Down Expand Up @@ -458,7 +472,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
.withBoundaryElement(this._getBoundaryElement())
.withPlaceholderTemplate(placeholder)
.withPreviewTemplate(preview)
.withPreviewContainer(this.previewContainer || 'global');
.withPreviewContainer(this.previewContainer || 'global', this.disablePreviewPopover);

if (dir) {
ref.withDirection(dir.value);
Expand Down Expand Up @@ -559,10 +573,12 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
draggingDisabled,
rootElementSelector,
previewContainer,
disablePreviewPopover,
} = config;

this.disabled = draggingDisabled == null ? false : draggingDisabled;
this.dragStartDelay = dragStartDelay || 0;
this.disablePreviewPopover = disablePreviewPopover || false;

if (lockAxis) {
this.lockAxis = lockAxis;
Expand Down
8 changes: 7 additions & 1 deletion src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export class DragRef<T = any> {
/** Container into which to insert the preview. */
private _previewContainer: PreviewContainer | undefined;

/** Whether to disable the popover wrapper around the preview. */
private _disablePreviewPopover: boolean;

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

Expand Down Expand Up @@ -591,9 +594,11 @@ export class DragRef<T = any> {
/**
* Sets the container into which to insert the preview element.
* @param value Container into which to insert the preview.
* @param disablePreviewPopover Whether to disable the popover wrapper around the preview.
*/
withPreviewContainer(value: PreviewContainer): this {
withPreviewContainer(value: PreviewContainer, disablePreviewPopover = false): this {
this._previewContainer = value;
this._disablePreviewPopover = disablePreviewPopover;
return this;
}

Expand Down Expand Up @@ -831,6 +836,7 @@ export class DragRef<T = any> {
this._rootElement,
this._direction,
this._initialDomRect!,
this._disablePreviewPopover,
this._previewTemplate || null,
this.previewClass || null,
this._pickupPositionOnPage,
Expand Down
57 changes: 42 additions & 15 deletions src/cdk/drag-drop/preview-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@ export class PreviewRef {
/** Reference to the preview element. */
private _preview: HTMLElement;

/** Reference to the preview wrapper. */
private _wrapper: HTMLElement;
/**
* Reference to the preview popover wrapper.
* May not be created if `_disablePopover` is enabled.
*/
private _popover: HTMLElement | null;

constructor(
private _document: Document,
private _rootElement: HTMLElement,
private _direction: Direction,
private _initialDomRect: DOMRect,
private _disablePopover: boolean,
private _previewTemplate: DragPreviewTemplate | null,
private _previewClass: string | string[] | null,
private _pickupPositionOnPage: {
Expand All @@ -58,21 +62,33 @@ export class PreviewRef {
) {}

attach(parent: HTMLElement): void {
this._wrapper = this._createWrapper();
this._preview = this._createPreview();
this._wrapper.appendChild(this._preview);
parent.appendChild(this._wrapper);

// The null check is necessary for browsers that don't support the popover API.
if (this._wrapper.showPopover) {
this._wrapper.showPopover();
if (this._disablePopover) {
this._styleRootElement(this._preview);
parent.appendChild(this._preview);
} else {
this._popover = this._createWrapper();
this._styleRootElement(this._popover);
this._popover.appendChild(this._preview);
parent.appendChild(this._popover);

// The null check is necessary for browsers that don't support the popover API.
if (this._popover.showPopover) {
this._popover.showPopover();
}
}
}

destroy(): void {
this._wrapper?.remove();
if (this._popover) {
this._popover.remove();
} else {
this._preview.remove();
}

this._previewEmbeddedView?.destroy();
this._preview = this._wrapper = this._previewEmbeddedView = null!;
this._preview = this._popover = this._previewEmbeddedView = null!;
}

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

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

// Reset the user agent styles.
'background': 'none',
Expand Down Expand Up @@ -189,4 +201,19 @@ export class PreviewRef {

return preview;
}

private _styleRootElement(root: HTMLElement): void {
root.setAttribute('dir', this._direction);

extendStyles(
root.style,
{
'position': 'fixed',
'top': '0',
'left': '0',
'z-index': this._zIndex + '',
},
importantProperties,
);
}
}
9 changes: 7 additions & 2 deletions tools/public_api_guard/cdk/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
data: T;
get disabled(): boolean;
set disabled(value: boolean);
disablePreviewPopover: boolean;
_dragRef: DragRef<CdkDrag<T>>;
dragStartDelay: DragStartDelay;
dropContainer: CdkDropList;
Expand All @@ -76,6 +77,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
// (undocumented)
static ngAcceptInputType_disabled: unknown;
// (undocumented)
static ngAcceptInputType_disablePreviewPopover: unknown;
// (undocumented)
ngAfterViewInit(): void;
// (undocumented)
ngOnChanges(changes: SimpleChanges): void;
Expand All @@ -99,7 +102,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
_setPreviewTemplate(preview: CdkDragPreview): void;
readonly started: EventEmitter<CdkDragStart>;
// (undocumented)
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>;
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>;
// (undocumented)
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; }]>;
}
Expand Down Expand Up @@ -317,6 +320,8 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
// (undocumented)
constrainPosition?: DragConstrainPosition;
// (undocumented)
disablePreviewPopover?: boolean;
// (undocumented)
draggingDisabled?: boolean;
// (undocumented)
dragStartDelay?: DragStartDelay;
Expand Down Expand Up @@ -451,7 +456,7 @@ export 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;
withPreviewContainer(value: PreviewContainer, disablePreviewPopover?: boolean): this;
withPreviewTemplate(template: DragPreviewTemplate | null): this;
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
}
Expand Down

0 comments on commit fd11606

Please sign in to comment.