From fd116060c465dcd28b9394c6b9683c6481fdda13 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 29 Apr 2024 09:34:17 +0200 Subject: [PATCH] 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. --- src/cdk/drag-drop/directives/config.ts | 1 + src/cdk/drag-drop/directives/drag.spec.ts | 16 +++++++ src/cdk/drag-drop/directives/drag.ts | 18 ++++++- src/cdk/drag-drop/drag-ref.ts | 8 +++- src/cdk/drag-drop/preview-ref.ts | 57 +++++++++++++++++------ tools/public_api_guard/cdk/drag-drop.md | 9 +++- 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/cdk/drag-drop/directives/config.ts b/src/cdk/drag-drop/directives/config.ts index 06a05c9c72cf..ef477310fe50 100644 --- a/src/cdk/drag-drop/directives/config.ts +++ b/src/cdk/drag-drop/directives/config.ts @@ -44,4 +44,5 @@ export interface DragDropConfig extends Partial { listOrientation?: DropListOrientation; zIndex?: number; previewContainer?: 'global' | 'parent'; + disablePreviewPopover?: boolean; } diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index efd93105af46..53dbae875b59 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -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(); @@ -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)" @@ -6965,6 +6980,7 @@ class DraggableInDropZone implements AfterViewInit { }); startedSpy = jasmine.createSpy('started spy'); previewContainer: PreviewContainer = 'global'; + disablePreviewPopover = false; constructor(protected _elementRef: ElementRef) {} diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 8d0f5c120113..26d0f05cc936 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -159,6 +159,20 @@ export class CdkDrag 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 `` or popover elements. + */ + @Input({alias: 'cdkDragDisablePreviewPopover', transform: booleanAttribute}) + disablePreviewPopover: boolean; + /** Emits when the user starts dragging the item. */ @Output('cdkDragStarted') readonly started: EventEmitter = new EventEmitter(); @@ -458,7 +472,7 @@ export class CdkDrag 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); @@ -559,10 +573,12 @@ export class CdkDrag 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; diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 860f52993983..1e421fe69287 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -119,6 +119,9 @@ export class DragRef { /** 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 | null; @@ -591,9 +594,11 @@ export class DragRef { /** * 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; } @@ -831,6 +836,7 @@ export class DragRef { this._rootElement, this._direction, this._initialDomRect!, + this._disablePreviewPopover, this._previewTemplate || null, this.previewClass || null, this._pickupPositionOnPage, diff --git a/src/cdk/drag-drop/preview-ref.ts b/src/cdk/drag-drop/preview-ref.ts index db1ee8f940bf..e3381ded42e4 100644 --- a/src/cdk/drag-drop/preview-ref.ts +++ b/src/cdk/drag-drop/preview-ref.ts @@ -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: { @@ -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 { @@ -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', @@ -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, + ); + } } diff --git a/tools/public_api_guard/cdk/drag-drop.md b/tools/public_api_guard/cdk/drag-drop.md index 4882c460318c..6eedc77082b1 100644 --- a/tools/public_api_guard/cdk/drag-drop.md +++ b/tools/public_api_guard/cdk/drag-drop.md @@ -59,6 +59,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { data: T; get disabled(): boolean; set disabled(value: boolean); + disablePreviewPopover: boolean; _dragRef: DragRef>; dragStartDelay: DragStartDelay; dropContainer: CdkDropList; @@ -76,6 +77,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) + static ngAcceptInputType_disablePreviewPopover: unknown; + // (undocumented) ngAfterViewInit(): void; // (undocumented) ngOnChanges(changes: SimpleChanges): void; @@ -99,7 +102,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { _setPreviewTemplate(preview: CdkDragPreview): void; readonly started: EventEmitter; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[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]", ["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, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>; } @@ -317,6 +320,8 @@ export interface DragDropConfig extends Partial { // (undocumented) constrainPosition?: DragConstrainPosition; // (undocumented) + disablePreviewPopover?: boolean; + // (undocumented) draggingDisabled?: boolean; // (undocumented) dragStartDelay?: DragStartDelay; @@ -451,7 +456,7 @@ export class DragRef { withHandles(handles: (HTMLElement | ElementRef)[]): this; withParent(parent: DragRef | 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): this; }