Skip to content

Commit 6009211

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): connected drop zones not working inside shadow root (#16899)
Fixes not being able to drop into a connected drop list when using `ShadowDom` view encapsulation. The issue comes from the fact that we use `elementFromPoint` to figure out whether the user's pointer is over a drop list. When the element is inside a shadow root, calling `elementFromPoint` on the `document` will return the shadow root. These changes fix the issue by calling `elementFromPoint` from the shadow root instead. Fixes #16898.
1 parent 3ee3ecb commit 6009211

File tree

3 files changed

+119
-57
lines changed

3 files changed

+119
-57
lines changed

src/cdk/drag-drop/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ ng_test_library(
3535
deps = [
3636
":drag-drop",
3737
"//src/cdk/bidi",
38+
"//src/cdk/platform",
3839
"//src/cdk/scrolling",
3940
"//src/cdk/testing",
4041
"@npm//@angular/common",

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

Lines changed: 98 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
2525
import {DOCUMENT} from '@angular/common';
2626
import {ViewportRuler} from '@angular/cdk/scrolling';
27+
import {_supportsShadowDom} from '@angular/cdk/platform';
2728
import {of as observableOf} from 'rxjs';
2829

2930
import {DragDropModule} from '../drag-drop-module';
@@ -4101,6 +4102,39 @@ describe('CdkDrag', () => {
41014102
cleanup();
41024103
}));
41034104

4105+
it('should be able to drop into a new container inside the Shadow DOM', fakeAsync(() => {
4106+
// This test is only relevant for Shadow DOM-supporting browsers.
4107+
if (!_supportsShadowDom()) {
4108+
return;
4109+
}
4110+
4111+
const fixture = createComponent(ConnectedDropZonesInsideShadowRoot);
4112+
fixture.detectChanges();
4113+
4114+
const groups = fixture.componentInstance.groupedDragItems;
4115+
const item = groups[0][1];
4116+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
4117+
4118+
dragElementViaMouse(fixture, item.element.nativeElement,
4119+
targetRect.left + 1, targetRect.top + 1);
4120+
flush();
4121+
fixture.detectChanges();
4122+
4123+
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1);
4124+
4125+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
4126+
4127+
expect(event).toEqual({
4128+
previousIndex: 1,
4129+
currentIndex: 3,
4130+
item,
4131+
container: fixture.componentInstance.dropInstances.toArray()[1],
4132+
previousContainer: fixture.componentInstance.dropInstances.first,
4133+
isPointerOverContainer: true,
4134+
distance: {x: jasmine.any(Number), y: jasmine.any(Number)}
4135+
});
4136+
}));
4137+
41044138
});
41054139

41064140
describe('with nested drags', () => {
@@ -4480,65 +4514,68 @@ class DraggableInDropZoneWithCustomPlaceholder {
44804514
renderPlaceholder = true;
44814515
}
44824516

4517+
const CONNECTED_DROP_ZONES_STYLES = [`
4518+
.cdk-drop-list {
4519+
display: block;
4520+
width: 100px;
4521+
min-height: ${ITEM_HEIGHT}px;
4522+
background: hotpink;
4523+
}
44834524
4484-
@Component({
4485-
encapsulation: ViewEncapsulation.None,
4486-
styles: [`
4487-
.cdk-drop-list {
4488-
display: block;
4489-
width: 100px;
4490-
min-height: ${ITEM_HEIGHT}px;
4491-
background: hotpink;
4492-
}
4525+
.cdk-drag {
4526+
display: block;
4527+
height: ${ITEM_HEIGHT}px;
4528+
background: red;
4529+
}
4530+
`];
44934531

4494-
.cdk-drag {
4495-
display: block;
4496-
height: ${ITEM_HEIGHT}px;
4497-
background: red;
4498-
}
4499-
`],
4500-
template: `
4532+
const CONNECTED_DROP_ZONES_TEMPLATE = `
4533+
<div
4534+
cdkDropList
4535+
#todoZone="cdkDropList"
4536+
[cdkDropListData]="todo"
4537+
[cdkDropListConnectedTo]="[doneZone]"
4538+
(cdkDropListDropped)="droppedSpy($event)"
4539+
(cdkDropListEntered)="enteredSpy($event)">
45014540
<div
4502-
cdkDropList
4503-
#todoZone="cdkDropList"
4504-
[cdkDropListData]="todo"
4505-
[cdkDropListConnectedTo]="[doneZone]"
4506-
(cdkDropListDropped)="droppedSpy($event)"
4507-
(cdkDropListEntered)="enteredSpy($event)">
4508-
<div
4509-
[cdkDragData]="item"
4510-
(cdkDragEntered)="itemEnteredSpy($event)"
4511-
*ngFor="let item of todo"
4512-
cdkDrag>{{item}}</div>
4513-
</div>
4541+
[cdkDragData]="item"
4542+
(cdkDragEntered)="itemEnteredSpy($event)"
4543+
*ngFor="let item of todo"
4544+
cdkDrag>{{item}}</div>
4545+
</div>
45144546
4547+
<div
4548+
cdkDropList
4549+
#doneZone="cdkDropList"
4550+
[cdkDropListData]="done"
4551+
[cdkDropListConnectedTo]="[todoZone]"
4552+
(cdkDropListDropped)="droppedSpy($event)"
4553+
(cdkDropListEntered)="enteredSpy($event)">
45154554
<div
4516-
cdkDropList
4517-
#doneZone="cdkDropList"
4518-
[cdkDropListData]="done"
4519-
[cdkDropListConnectedTo]="[todoZone]"
4520-
(cdkDropListDropped)="droppedSpy($event)"
4521-
(cdkDropListEntered)="enteredSpy($event)">
4522-
<div
4523-
[cdkDragData]="item"
4524-
(cdkDragEntered)="itemEnteredSpy($event)"
4525-
*ngFor="let item of done"
4526-
cdkDrag>{{item}}</div>
4527-
</div>
4555+
[cdkDragData]="item"
4556+
(cdkDragEntered)="itemEnteredSpy($event)"
4557+
*ngFor="let item of done"
4558+
cdkDrag>{{item}}</div>
4559+
</div>
45284560
4561+
<div
4562+
cdkDropList
4563+
#extraZone="cdkDropList"
4564+
[cdkDropListData]="extra"
4565+
(cdkDropListDropped)="droppedSpy($event)"
4566+
(cdkDropListEntered)="enteredSpy($event)">
45294567
<div
4530-
cdkDropList
4531-
#extraZone="cdkDropList"
4532-
[cdkDropListData]="extra"
4533-
(cdkDropListDropped)="droppedSpy($event)"
4534-
(cdkDropListEntered)="enteredSpy($event)">
4535-
<div
4536-
[cdkDragData]="item"
4537-
(cdkDragEntered)="itemEnteredSpy($event)"
4538-
*ngFor="let item of extra"
4539-
cdkDrag>{{item}}</div>
4540-
</div>
4541-
`
4568+
[cdkDragData]="item"
4569+
(cdkDragEntered)="itemEnteredSpy($event)"
4570+
*ngFor="let item of extra"
4571+
cdkDrag>{{item}}</div>
4572+
</div>
4573+
`;
4574+
4575+
@Component({
4576+
encapsulation: ViewEncapsulation.None,
4577+
styles: CONNECTED_DROP_ZONES_STYLES,
4578+
template: CONNECTED_DROP_ZONES_TEMPLATE
45424579
})
45434580
class ConnectedDropZones implements AfterViewInit {
45444581
@ViewChildren(CdkDrag) rawDragItems: QueryList<CdkDrag>;
@@ -4563,6 +4600,15 @@ class ConnectedDropZones implements AfterViewInit {
45634600
}
45644601
}
45654602

4603+
@Component({
4604+
encapsulation: ViewEncapsulation.ShadowDom,
4605+
styles: CONNECTED_DROP_ZONES_STYLES,
4606+
template: CONNECTED_DROP_ZONES_TEMPLATE
4607+
})
4608+
class ConnectedDropZonesInsideShadowRoot extends ConnectedDropZones {
4609+
}
4610+
4611+
45664612
@Component({
45674613
encapsulation: ViewEncapsulation.None,
45684614
styles: [`

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ElementRef, NgZone} from '@angular/core';
1010
import {Direction} from '@angular/cdk/bidi';
1111
import {coerceElement} from '@angular/cdk/coercion';
1212
import {ViewportRuler} from '@angular/cdk/scrolling';
13+
import {_supportsShadowDom} from '@angular/cdk/platform';
1314
import {Subject, Subscription, interval, animationFrameScheduler} from 'rxjs';
1415
import {takeUntil} from 'rxjs/operators';
1516
import {moveItemInArray} from './drag-utils';
@@ -74,8 +75,6 @@ export interface DropListRefInternal extends DropListRef {}
7475
* @docs-private
7576
*/
7677
export class DropListRef<T = any> {
77-
private _document: Document;
78-
7978
/** Element that the drop list is attached to. */
8079
element: HTMLElement | ElementRef<HTMLElement>;
8180

@@ -201,6 +200,9 @@ export class DropListRef<T = any> {
201200
/** Used to signal to the current auto-scroll sequence when to stop. */
202201
private _stopScrollTimers = new Subject<void>();
203202

203+
/** Shadow root of the current element. Necessary for `elementFromPoint` to resolve correctly. */
204+
private _shadowRoot: DocumentOrShadowRoot;
205+
204206
constructor(
205207
element: ElementRef<HTMLElement> | HTMLElement,
206208
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
@@ -211,9 +213,9 @@ export class DropListRef<T = any> {
211213
*/
212214
private _ngZone?: NgZone,
213215
private _viewportRuler?: ViewportRuler) {
216+
const nativeNode = this.element = coerceElement(element);
217+
this._shadowRoot = getShadowRoot(nativeNode) || _document;
214218
_dragDropRegistry.registerDropContainer(this);
215-
this._document = _document;
216-
this.element = element instanceof ElementRef ? element.nativeElement : element;
217219
}
218220

219221
/** Removes the drop list functionality from the DOM element. */
@@ -815,7 +817,7 @@ export class DropListRef<T = any> {
815817
return false;
816818
}
817819

818-
const elementFromPoint = this._document.elementFromPoint(x, y) as HTMLElement | null;
820+
const elementFromPoint = this._shadowRoot.elementFromPoint(x, y) as HTMLElement | null;
819821

820822
// If there's no element at the pointer position, then
821823
// the client rect is probably scrolled out of the view.
@@ -1049,3 +1051,16 @@ function getElementScrollDirections(element: HTMLElement, clientRect: ClientRect
10491051

10501052
return [verticalScrollDirection, horizontalScrollDirection];
10511053
}
1054+
1055+
/** Gets the shadow root of an element, if any. */
1056+
function getShadowRoot(element: HTMLElement): DocumentOrShadowRoot | null {
1057+
if (_supportsShadowDom()) {
1058+
const rootNode = element.getRootNode ? element.getRootNode() : null;
1059+
1060+
if (rootNode instanceof ShadowRoot) {
1061+
return rootNode;
1062+
}
1063+
}
1064+
1065+
return null;
1066+
}

0 commit comments

Comments
 (0)