Skip to content

Commit

Permalink
fix(cdk/drag-drop): text selection not disabled inside shadow dom on …
Browse files Browse the repository at this point in the history
…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.

(cherry picked from commit 42cb25f)
  • Loading branch information
crisbeto committed Apr 12, 2024
1 parent 312d57a commit 8c8fe2b
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 7 deletions.
40 changes: 40 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6520,6 +6520,46 @@ describe('CdkDrag', () => {
});
}));

it('should prevent selection at the shadow root level', fakeAsync(() => {
// This test is only relevant for Shadow DOM-supporting browsers.
if (!_supportsShadowDom()) {
return;
}

const fixture = createComponent(
ConnectedDropZones,
[],
undefined,
[],
ViewEncapsulation.ShadowDom,
);
fixture.detectChanges();

const shadowRoot = fixture.nativeElement.shadowRoot;
const item = fixture.componentInstance.groupedDragItems[0][1];

startDraggingViaMouse(fixture, item.element.nativeElement);
fixture.detectChanges();

const initialSelectStart = dispatchFakeEvent(
shadowRoot,
'selectstart',
);
fixture.detectChanges();
expect(initialSelectStart.defaultPrevented).toBe(true);

dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();

const afterDropSelectStart = dispatchFakeEvent(
shadowRoot,
'selectstart',
);
fixture.detectChanges();
expect(afterDropSelectStart.defaultPrevented).toBe(false);
}));

it('should not throw if its next sibling is removed while dragging', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZonesWithSingleItems);
fixture.detectChanges();
Expand Down
41 changes: 34 additions & 7 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: tr
/** Options that can be used to bind an active event listener. */
const activeEventListenerOptions = normalizePassiveListenerOptions({passive: false});

/** Event options that can be used to bind an active, capturing event. */
const activeCapturingEventOptions = normalizePassiveListenerOptions({
passive: false,
capture: true,
});

/**
* Time in milliseconds for which to ignore mouse events, after
* receiving a touch event. Used to avoid doing double work for
Expand Down Expand Up @@ -496,7 +502,7 @@ export class DragRef<T = any> {
this._destroyPreview();
this._destroyPlaceholder();
this._dragDropRegistry.removeDragItem(this);
this._removeSubscriptions();
this._removeListeners();
this.beforeStarted.complete();
this.started.complete();
this.released.complete();
Expand Down Expand Up @@ -608,10 +614,15 @@ export class DragRef<T = any> {
}

/** Unsubscribes from the global subscriptions. */
private _removeSubscriptions() {
private _removeListeners() {
this._pointerMoveSubscription.unsubscribe();
this._pointerUpSubscription.unsubscribe();
this._scrollSubscription.unsubscribe();
this._getShadowRoot()?.removeEventListener(
'selectstart',
shadowDomSelectStart,
activeCapturingEventOptions,
);
}

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

this._removeSubscriptions();
this._removeListeners();
this._dragDropRegistry.stopDragging(this);
this._toggleNativeDragInteractions();

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

this._toggleNativeDragInteractions();

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

if (shadowRoot) {
// In some browsers the global `selectstart` that we maintain in the `DragDropRegistry`
// doesn't cross the shadow boundary so we have to prevent it at the shadow root (see #28792).
this._ngZone.runOutsideAngular(() => {
shadowRoot.addEventListener(
'selectstart',
shadowDomSelectStart,
activeCapturingEventOptions,
);
});
}

if (dropContainer) {
const element = this._rootElement;
const parent = element.parentNode as HTMLElement;
const placeholder = (this._placeholder = this._createPlaceholderElement());
const anchor = (this._anchor = this._anchor || this._document.createComment(''));

// Needs to happen before the root element is moved.
const shadowRoot = this._getShadowRoot();

// Insert an anchor node so that we can restore the element's position in the DOM.
parent.insertBefore(anchor, element);

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

// Avoid multiple subscriptions and memory leaks when multi touch
// (isDragging check above isn't enough because of possible temporal and/or dimensional delays)
this._removeSubscriptions();
this._removeListeners();
this._initialDomRect = this._rootElement.getBoundingClientRect();
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
Expand Down Expand Up @@ -1617,3 +1639,8 @@ function matchElementSize(target: HTMLElement, sourceRect: DOMRect): void {
target.style.height = `${sourceRect.height}px`;
target.style.transform = getTransform(sourceRect.left, sourceRect.top);
}

/** Callback invoked for `selectstart` events inside the shadow DOM. */
function shadowDomSelectStart(event: Event) {
event.preventDefault();
}

0 comments on commit 8c8fe2b

Please sign in to comment.