Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions packages/devextreme/js/__internal/__tests__/draggable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from '@jest/globals';
import eventsEngine from '@js/common/core/events/core/events_engine';
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import Draggable from '@js/ui/draggable';

const DRAGGABLE_ELEMENT_ID = 'draggable';

interface DraggableTest extends Draggable {
getDragInProgress: () => boolean;
}

interface FiredEvent {
_cancelPreventDefault?: boolean;
isDefaultPrevented: () => boolean;
}

const fire = (element: Element, type: string, x = 0, y = 0): FiredEvent => {
// @ts-expect-error -- Event is absent from the public eventsEngine type
const event: FiredEvent = eventsEngine.Event({
type,
pageX: x,
pageY: y,
clientX: x,
clientY: y,
offset: { x, y },
pointerType: 'touch',
pointers: type === 'dxpointerup' ? [] : [{ pointerId: 1 }],
pointerId: 1,
which: 1,
});

// @ts-expect-error -- trigger is absent from the public eventsEngine type
eventsEngine.trigger(element, event);

return event;
};

const swipe = (target: Element): void => {
fire(target, 'dxpointerdown', 0, 0);
fire(target, 'dxpointermove', 0, 20);
fire(target, 'dxpointermove', 0, 40);
fire(target, 'dxpointerup', 0, 40);
};

const appendElement = (): dxElementWrapper => $('<div>')
.attr('id', DRAGGABLE_ELEMENT_ID)
.css({ width: '100px', height: '40px' })
.appendTo(document.body);

const createDraggable = (options: Record<string, unknown> = {}): DraggableTest => {
const $element = appendElement();

return new Draggable($element.get(0), { autoScroll: false, ...options }) as DraggableTest;
};

beforeEach(() => {
document.body.innerHTML = '';
});

afterEach(() => {
const instance = Draggable.getInstance($(`#${DRAGGABLE_ELEMENT_ID}`).get(0));

instance?.dispose();
});

describe('Draggable dragInProgress reset', () => {
it('should reset dragInProgress when a drag is completed', () => {
const draggable = createDraggable();

swipe(draggable.$element().get(0));

expect(draggable.getDragInProgress()).toBe(false);
});

it('should not set dragInProgress when the drag start is canceled by an invalid handle', () => {
const draggable = createDraggable({ handle: '.handle' });
const $draggableElement = draggable.$element();

$('<span>').addClass('handle').appendTo($draggableElement);

swipe($draggableElement.get(0));

expect(draggable.getDragInProgress()).toBe(false);
});

it('should not set dragInProgress when the element is disabled', () => {
const draggable = createDraggable();

draggable.$element().addClass('dx-state-disabled');

swipe(draggable.$element().get(0));

expect(draggable.getDragInProgress()).toBe(false);
});

it('should not set dragInProgress when onDragStart cancels the drag', () => {
const draggable = createDraggable({
onDragStart: (e) => { e.cancel = true; },
});

swipe(draggable.$element().get(0));

expect(draggable.getDragInProgress()).toBe(false);
});
});

describe('Draggable preventDefault on a drag move', () => {
it('should not prevent default on a drag move when no drag is in progress', () => {
const draggable = createDraggable();
const event = fire(draggable.$element().get(0), 'dxdrag');

expect(event._cancelPreventDefault).toBe(true);
expect(event.isDefaultPrevented()).toBe(false);
});

it('should prevent default on a drag move when a drag is in progress', () => {
const draggable = createDraggable();
const element = draggable.$element().get(0);

fire(element, 'dxpointerdown', 0, 0);
const event = fire(element, 'dxpointermove', 0, 20);

expect(event._cancelPreventDefault).not.toBe(true);
expect(event.isDefaultPrevented()).toBe(true);
});
});
60 changes: 47 additions & 13 deletions packages/devextreme/js/__internal/m_draggable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable max-classes-per-file */
import positionUtils from '@js/common/core/animation/position';
import { locate, move } from '@js/common/core/animation/translator';
import type { Cancelable } from '@js/common/core/events';
import eventsEngine from '@js/common/core/events/core/events_engine';
import {
end as dragEventEnd,
Expand Down Expand Up @@ -33,6 +34,7 @@ import { quadToObject } from '@js/core/utils/string';
import { isFunction, isNumeric, isObject } from '@js/core/utils/type';
import { value as viewPort } from '@js/core/utils/view_port';
import { getWindow } from '@js/core/utils/window';
import type { PointerInteractionEvent } from '@js/events/events.types';
import type { Properties } from '@js/ui/draggable';
import DOMComponent from '@ts/core/widget/dom_component';

Expand Down Expand Up @@ -73,6 +75,16 @@ interface Offset {
top: number;
}

interface DragEventOffset {
x: number;
y: number;
}

type DragEvent = Cancelable & PointerInteractionEvent & {
_cancelPreventDefault?: boolean;
offset?: DragEventOffset;
};

class ScrollHelper {
private _preventScroll: boolean;

Expand Down Expand Up @@ -225,9 +237,10 @@ class ScrollHelper {
that._$scrollableAtPointer[that._scrollValue](nextScrollPosition);
}

const dragMoveArgs = that._component._dragMoveArgs;
if (dragMoveArgs) {
that._component._dragMoveHandler(dragMoveArgs);
const dragMoveEvent = that._component._dragMoveEvent;

if (dragMoveEvent) {
that._component._dragMoveHandler(dragMoveEvent);
}
}
}
Expand Down Expand Up @@ -285,6 +298,8 @@ class Draggable extends DOMComponent<Draggable, Properties> {

dragInProgress?: boolean;

_dragMoveEvent?: DragEvent;

_scrollAnimator!: ScrollAnimator;

_initialLocate?: { left: number; top: number };
Expand Down Expand Up @@ -636,7 +651,6 @@ class Draggable extends DOMComponent<Draggable, Properties> {

_dragStartHandler(e) {
const $element = this._getDraggableElement(e);
this.dragInProgress = true;

if (!this._isValidElement(e, $element)) {
e.cancel = true;
Expand All @@ -654,6 +668,7 @@ class Draggable extends DOMComponent<Draggable, Properties> {
return;
}

this.dragInProgress = true;
this.option('itemData', dragStartArgs.itemData);
this._setSourceDraggable();

Expand Down Expand Up @@ -789,17 +804,16 @@ class Draggable extends DOMComponent<Draggable, Properties> {
return this.option('clone') || this.option('dragTemplate');
}

_dragMoveHandler(e) {
// @ts-expect-error ts-error
this._dragMoveArgs = e;
private _dragMoveHandler(e: DragEvent): void {
this._allowNativeScrollingWhenNotDragging(e);
this._dragMoveEvent = e;

Comment thread
Alyar666 marked this conversation as resolved.
if (!this._$dragElement) {
e.cancel = true;
return;
}

const offset = this._getDraggableElementOffset(e.offset.x, e.offset.y);

this._move(offset);
this._moveDragElement(e);
this._updateScrollable(e);

const eventArgs = this._getEventArgs(e);
Expand All @@ -809,11 +823,24 @@ class Draggable extends DOMComponent<Draggable, Properties> {
return;
}

const targetDraggable = this._getTargetDraggable();
targetDraggable.dragMove(e, scrollBy);
this._getTargetDraggable().dragMove(e, scrollBy);
}

// Without an active drag the gesture emitter must not call preventDefault on the
// move event, otherwise native scrolling is blocked on touch devices (T1329643).
private _allowNativeScrollingWhenNotDragging(e: DragEvent): void {
if (!this.dragInProgress) {
e._cancelPreventDefault = true;
}
}

private _moveDragElement(e: DragEvent): void {
const offset = this._getDraggableElementOffset(e.offset?.x ?? 0, e.offset?.y ?? 0);

this._move(offset);
}

_updateScrollable(e) {
private _updateScrollable(e: DragEvent): void {
const that = this;

if (that.option('autoScroll')) {
Expand Down Expand Up @@ -1164,6 +1191,13 @@ class Draggable extends DOMComponent<Draggable, Properties> {

this._getAction('onDragLeave')(args);
}

/// #DEBUG
// Test-only accessor, removed from production builds.
getDragInProgress(): boolean {
return !!this.dragInProgress;
}
/// #ENDDEBUG
}

registerComponent(DRAGGABLE, Draggable);
Expand Down
Loading