Skip to content

Commit 42cb25f

Browse files
authored
fix(cdk/drag-drop): text selection not disabled inside shadow dom on firefox (#28835)
Fixes that text selection wasn't being disabled when the `cdkDrag` directive is inside the shadow DOM on Firefox. The issue appears to be that the `selectstart` event wasn't crossing the shadow boundary so we have to bind it at the shadow root as well. Fixes #28792.
1 parent 1999e20 commit 42cb25f

File tree

2 files changed

+74
-7
lines changed

2 files changed

+74
-7
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6527,6 +6527,46 @@ describe('CdkDrag', () => {
65276527
});
65286528
}));
65296529

6530+
it('should prevent selection at the shadow root level', fakeAsync(() => {
6531+
// This test is only relevant for Shadow DOM-supporting browsers.
6532+
if (!_supportsShadowDom()) {
6533+
return;
6534+
}
6535+
6536+
const fixture = createComponent(
6537+
ConnectedDropZones,
6538+
[],
6539+
undefined,
6540+
[],
6541+
ViewEncapsulation.ShadowDom,
6542+
);
6543+
fixture.detectChanges();
6544+
6545+
const shadowRoot = fixture.nativeElement.shadowRoot;
6546+
const item = fixture.componentInstance.groupedDragItems[0][1];
6547+
6548+
startDraggingViaMouse(fixture, item.element.nativeElement);
6549+
fixture.detectChanges();
6550+
6551+
const initialSelectStart = dispatchFakeEvent(
6552+
shadowRoot,
6553+
'selectstart',
6554+
);
6555+
fixture.detectChanges();
6556+
expect(initialSelectStart.defaultPrevented).toBe(true);
6557+
6558+
dispatchMouseEvent(document, 'mouseup');
6559+
fixture.detectChanges();
6560+
flush();
6561+
6562+
const afterDropSelectStart = dispatchFakeEvent(
6563+
shadowRoot,
6564+
'selectstart',
6565+
);
6566+
fixture.detectChanges();
6567+
expect(afterDropSelectStart.defaultPrevented).toBe(false);
6568+
}));
6569+
65306570
it('should not throw if its next sibling is removed while dragging', fakeAsync(() => {
65316571
const fixture = createComponent(ConnectedDropZonesWithSingleItems);
65326572
fixture.detectChanges();

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: tr
5858
/** Options that can be used to bind an active event listener. */
5959
const activeEventListenerOptions = normalizePassiveListenerOptions({passive: false});
6060

61+
/** Event options that can be used to bind an active, capturing event. */
62+
const activeCapturingEventOptions = normalizePassiveListenerOptions({
63+
passive: false,
64+
capture: true,
65+
});
66+
6167
/**
6268
* Time in milliseconds for which to ignore mouse events, after
6369
* receiving a touch event. Used to avoid doing double work for
@@ -496,7 +502,7 @@ export class DragRef<T = any> {
496502
this._destroyPreview();
497503
this._destroyPlaceholder();
498504
this._dragDropRegistry.removeDragItem(this);
499-
this._removeSubscriptions();
505+
this._removeListeners();
500506
this.beforeStarted.complete();
501507
this.started.complete();
502508
this.released.complete();
@@ -608,10 +614,15 @@ export class DragRef<T = any> {
608614
}
609615

610616
/** Unsubscribes from the global subscriptions. */
611-
private _removeSubscriptions() {
617+
private _removeListeners() {
612618
this._pointerMoveSubscription.unsubscribe();
613619
this._pointerUpSubscription.unsubscribe();
614620
this._scrollSubscription.unsubscribe();
621+
this._getShadowRoot()?.removeEventListener(
622+
'selectstart',
623+
shadowDomSelectStart,
624+
activeCapturingEventOptions,
625+
);
615626
}
616627

617628
/** Destroys the preview element and its ViewRef. */
@@ -741,7 +752,7 @@ export class DragRef<T = any> {
741752
return;
742753
}
743754

744-
this._removeSubscriptions();
755+
this._removeListeners();
745756
this._dragDropRegistry.stopDragging(this);
746757
this._toggleNativeDragInteractions();
747758

@@ -792,17 +803,28 @@ export class DragRef<T = any> {
792803

793804
this._toggleNativeDragInteractions();
794805

806+
// Needs to happen before the root element is moved.
807+
const shadowRoot = this._getShadowRoot();
795808
const dropContainer = this._dropContainer;
796809

810+
if (shadowRoot) {
811+
// In some browsers the global `selectstart` that we maintain in the `DragDropRegistry`
812+
// doesn't cross the shadow boundary so we have to prevent it at the shadow root (see #28792).
813+
this._ngZone.runOutsideAngular(() => {
814+
shadowRoot.addEventListener(
815+
'selectstart',
816+
shadowDomSelectStart,
817+
activeCapturingEventOptions,
818+
);
819+
});
820+
}
821+
797822
if (dropContainer) {
798823
const element = this._rootElement;
799824
const parent = element.parentNode as HTMLElement;
800825
const placeholder = (this._placeholder = this._createPlaceholderElement());
801826
const anchor = (this._anchor = this._anchor || this._document.createComment(''));
802827

803-
// Needs to happen before the root element is moved.
804-
const shadowRoot = this._getShadowRoot();
805-
806828
// Insert an anchor node so that we can restore the element's position in the DOM.
807829
parent.insertBefore(anchor, element);
808830

@@ -888,7 +910,7 @@ export class DragRef<T = any> {
888910

889911
// Avoid multiple subscriptions and memory leaks when multi touch
890912
// (isDragging check above isn't enough because of possible temporal and/or dimensional delays)
891-
this._removeSubscriptions();
913+
this._removeListeners();
892914
this._initialDomRect = this._rootElement.getBoundingClientRect();
893915
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
894916
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
@@ -1617,3 +1639,8 @@ function matchElementSize(target: HTMLElement, sourceRect: DOMRect): void {
16171639
target.style.height = `${sourceRect.height}px`;
16181640
target.style.transform = getTransform(sourceRect.left, sourceRect.top);
16191641
}
1642+
1643+
/** Callback invoked for `selectstart` events inside the shadow DOM. */
1644+
function shadowDomSelectStart(event: Event) {
1645+
event.preventDefault();
1646+
}

0 commit comments

Comments
 (0)