Skip to content

Commit b6bd3d6

Browse files
committed
fix(drag-drop): connected drop zones not working inside shadow root
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 77b0e0e commit b6bd3d6

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';
@@ -4010,6 +4011,39 @@ describe('CdkDrag', () => {
40104011
cleanup();
40114012
}));
40124013

4014+
it('should be able to drop into a new container inside the Shadow DOM', fakeAsync(() => {
4015+
// This test is only relevant for Shadow DOM-supporting browsers.
4016+
if (!_supportsShadowDom()) {
4017+
return;
4018+
}
4019+
4020+
const fixture = createComponent(ConnectedDropZonesInsideShadowRoot);
4021+
fixture.detectChanges();
4022+
4023+
const groups = fixture.componentInstance.groupedDragItems;
4024+
const item = groups[0][1];
4025+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
4026+
4027+
dragElementViaMouse(fixture, item.element.nativeElement,
4028+
targetRect.left + 1, targetRect.top + 1);
4029+
flush();
4030+
fixture.detectChanges();
4031+
4032+
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1);
4033+
4034+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
4035+
4036+
expect(event).toEqual({
4037+
previousIndex: 1,
4038+
currentIndex: 3,
4039+
item,
4040+
container: fixture.componentInstance.dropInstances.toArray()[1],
4041+
previousContainer: fixture.componentInstance.dropInstances.first,
4042+
isPointerOverContainer: true,
4043+
distance: {x: jasmine.any(Number), y: jasmine.any(Number)}
4044+
});
4045+
}));
4046+
40134047
});
40144048

40154049
describe('with nested drags', () => {
@@ -4389,65 +4423,68 @@ class DraggableInDropZoneWithCustomPlaceholder {
43894423
renderPlaceholder = true;
43904424
}
43914425

4426+
const CONNECTED_DROP_ZONES_STYLES = [`
4427+
.cdk-drop-list {
4428+
display: block;
4429+
width: 100px;
4430+
min-height: ${ITEM_HEIGHT}px;
4431+
background: hotpink;
4432+
}
43924433
4393-
@Component({
4394-
encapsulation: ViewEncapsulation.None,
4395-
styles: [`
4396-
.cdk-drop-list {
4397-
display: block;
4398-
width: 100px;
4399-
min-height: ${ITEM_HEIGHT}px;
4400-
background: hotpink;
4401-
}
4434+
.cdk-drag {
4435+
display: block;
4436+
height: ${ITEM_HEIGHT}px;
4437+
background: red;
4438+
}
4439+
`];
44024440

4403-
.cdk-drag {
4404-
display: block;
4405-
height: ${ITEM_HEIGHT}px;
4406-
background: red;
4407-
}
4408-
`],
4409-
template: `
4441+
const CONNECTED_DROP_ZONES_TEMPLATE = `
4442+
<div
4443+
cdkDropList
4444+
#todoZone="cdkDropList"
4445+
[cdkDropListData]="todo"
4446+
[cdkDropListConnectedTo]="[doneZone]"
4447+
(cdkDropListDropped)="droppedSpy($event)"
4448+
(cdkDropListEntered)="enteredSpy($event)">
44104449
<div
4411-
cdkDropList
4412-
#todoZone="cdkDropList"
4413-
[cdkDropListData]="todo"
4414-
[cdkDropListConnectedTo]="[doneZone]"
4415-
(cdkDropListDropped)="droppedSpy($event)"
4416-
(cdkDropListEntered)="enteredSpy($event)">
4417-
<div
4418-
[cdkDragData]="item"
4419-
(cdkDragEntered)="itemEnteredSpy($event)"
4420-
*ngFor="let item of todo"
4421-
cdkDrag>{{item}}</div>
4422-
</div>
4450+
[cdkDragData]="item"
4451+
(cdkDragEntered)="itemEnteredSpy($event)"
4452+
*ngFor="let item of todo"
4453+
cdkDrag>{{item}}</div>
4454+
</div>
44234455
4456+
<div
4457+
cdkDropList
4458+
#doneZone="cdkDropList"
4459+
[cdkDropListData]="done"
4460+
[cdkDropListConnectedTo]="[todoZone]"
4461+
(cdkDropListDropped)="droppedSpy($event)"
4462+
(cdkDropListEntered)="enteredSpy($event)">
44244463
<div
4425-
cdkDropList
4426-
#doneZone="cdkDropList"
4427-
[cdkDropListData]="done"
4428-
[cdkDropListConnectedTo]="[todoZone]"
4429-
(cdkDropListDropped)="droppedSpy($event)"
4430-
(cdkDropListEntered)="enteredSpy($event)">
4431-
<div
4432-
[cdkDragData]="item"
4433-
(cdkDragEntered)="itemEnteredSpy($event)"
4434-
*ngFor="let item of done"
4435-
cdkDrag>{{item}}</div>
4436-
</div>
4464+
[cdkDragData]="item"
4465+
(cdkDragEntered)="itemEnteredSpy($event)"
4466+
*ngFor="let item of done"
4467+
cdkDrag>{{item}}</div>
4468+
</div>
44374469
4470+
<div
4471+
cdkDropList
4472+
#extraZone="cdkDropList"
4473+
[cdkDropListData]="extra"
4474+
(cdkDropListDropped)="droppedSpy($event)"
4475+
(cdkDropListEntered)="enteredSpy($event)">
44384476
<div
4439-
cdkDropList
4440-
#extraZone="cdkDropList"
4441-
[cdkDropListData]="extra"
4442-
(cdkDropListDropped)="droppedSpy($event)"
4443-
(cdkDropListEntered)="enteredSpy($event)">
4444-
<div
4445-
[cdkDragData]="item"
4446-
(cdkDragEntered)="itemEnteredSpy($event)"
4447-
*ngFor="let item of extra"
4448-
cdkDrag>{{item}}</div>
4449-
</div>
4450-
`
4477+
[cdkDragData]="item"
4478+
(cdkDragEntered)="itemEnteredSpy($event)"
4479+
*ngFor="let item of extra"
4480+
cdkDrag>{{item}}</div>
4481+
</div>
4482+
`;
4483+
4484+
@Component({
4485+
encapsulation: ViewEncapsulation.None,
4486+
styles: CONNECTED_DROP_ZONES_STYLES,
4487+
template: CONNECTED_DROP_ZONES_TEMPLATE
44514488
})
44524489
class ConnectedDropZones implements AfterViewInit {
44534490
@ViewChildren(CdkDrag) rawDragItems: QueryList<CdkDrag>;
@@ -4472,6 +4509,15 @@ class ConnectedDropZones implements AfterViewInit {
44724509
}
44734510
}
44744511

4512+
@Component({
4513+
encapsulation: ViewEncapsulation.ShadowDom,
4514+
styles: CONNECTED_DROP_ZONES_STYLES,
4515+
template: CONNECTED_DROP_ZONES_TEMPLATE
4516+
})
4517+
class ConnectedDropZonesInsideShadowRoot extends ConnectedDropZones {
4518+
}
4519+
4520+
44754521
@Component({
44764522
encapsulation: ViewEncapsulation.None,
44774523
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)