fix: does not trigger drag:start sensor event when moved during delay

This commit is contained in:
JuFeng Zhang 2020-09-09 22:33:35 +08:00
parent 03012df4ce
commit a85f13400a
12 changed files with 144 additions and 30 deletions

10
index.d.ts vendored
View File

@ -86,6 +86,12 @@ declare module '@shopify/draggable' {
? SnapOutEvent ? SnapOutEvent
: AbstractEvent; : AbstractEvent;
interface DelayOptions {
mouse?: number;
drag?: number;
touch?: number;
}
/** /**
* DragEvent * DragEvent
*/ */
@ -167,7 +173,7 @@ declare module '@shopify/draggable' {
draggable?: string; draggable?: string;
distance?: number; distance?: number;
handle?: string | NodeList | HTMLElement[] | HTMLElement | ((currentElement: HTMLElement) => HTMLElement); handle?: string | NodeList | HTMLElement[] | HTMLElement | ((currentElement: HTMLElement) => HTMLElement);
delay?: number; delay?: number | DelayOptions;
plugins?: Array<typeof AbstractPlugin>; plugins?: Array<typeof AbstractPlugin>;
sensors?: Sensor[]; sensors?: Sensor[];
classes?: { [key in DraggableClassNames]: string }; classes?: { [key in DraggableClassNames]: string };
@ -315,7 +321,7 @@ declare module '@shopify/draggable' {
export class DragPressureSensorEvent extends SensorEvent { } export class DragPressureSensorEvent extends SensorEvent { }
export interface SensorOptions { export interface SensorOptions {
delay?: number; delay?: number | DelayOptions;
} }
export class Sensor { export class Sensor {

View File

@ -1,13 +1,16 @@
import {DRAG_DELAY, defaultTouchEventOptions, defaultMouseEventOptions} from './constants'; import {DRAG_DELAY, defaultTouchEventOptions, defaultMouseEventOptions} from './constants';
import {triggerEvent} from './event'; import {triggerEvent} from './event';
export function waitForDragDelay(dragDelay = DRAG_DELAY) { export function waitForDragDelay({dragDelay = DRAG_DELAY, restoreDateMock = true} = {}) {
const next = Date.now() + dragDelay + 1; const next = Date.now() + dragDelay + 1;
const dateMock = jest.spyOn(Date, 'now').mockImplementation(() => { const dateMock = jest.spyOn(Date, 'now').mockImplementation(() => {
return next; return next;
}); });
jest.runTimersToTime(dragDelay + 1); jest.runTimersToTime(dragDelay + 1);
dateMock.mockRestore(); if (restoreDateMock) {
dateMock.mockRestore();
}
return dateMock;
} }
export function clickMouse(element, options = {}) { export function clickMouse(element, options = {}) {

View File

@ -48,7 +48,7 @@ const defaultClasses = {
export const defaultOptions = { export const defaultOptions = {
draggable: '.draggable-source', draggable: '.draggable-source',
handle: null, handle: null,
delay: 100, delay: {},
distance: 0, distance: 0,
placedTimeout: 800, placedTimeout: 800,
plugins: [], plugins: [],

View File

@ -76,9 +76,19 @@ look for an element with `.draggable-source` class. Default: `.draggable-source`
Specify a css selector for a handle element if you don't want to allow drag action Specify a css selector for a handle element if you don't want to allow drag action
on the entire element. Default: `null` on the entire element. Default: `null`
**`delay {Number}`** **`delay {Number|Object}`**
If you want to delay a drag start you can specify delay in milliseconds. This can be useful If you want to delay a drag start you can specify delay in milliseconds. This can be useful
for draggable elements within scrollable containers. Default: `100` for draggable elements within scrollable containers. To allow touch scrolling, we set 100ms delay for TouchSensor by default. Default:
```js
{
mouse: 0,
drag: 0,
touch: 100,
}
```
You can set the same delay for all sensors by setting a number, or set an object to set the delay for each sensor separately.
**`distance {Number}`** **`distance {Number}`**
The distance you want the pointer to have moved before drag starts. This can be useful The distance you want the pointer to have moved before drag starts. This can be useful

View File

@ -211,7 +211,7 @@ export default class DragSensor extends Sensor {
this.mouseDownTimeout = setTimeout(() => { this.mouseDownTimeout = setTimeout(() => {
target.draggable = true; target.draggable = true;
this.draggableElement = target; this.draggableElement = target;
}, this.options.delay); }, this.delay.drag);
} }
/** /**

View File

@ -83,7 +83,7 @@ export default class MouseSensor extends Sensor {
return; return;
} }
const {delay = 0} = this.options; const {delay} = this;
const {pageX, pageY} = event; const {pageX, pageY} = event;
Object.assign(this, {pageX, pageY}); Object.assign(this, {pageX, pageY});
@ -97,7 +97,7 @@ export default class MouseSensor extends Sensor {
this.mouseDownTimeout = window.setTimeout(() => { this.mouseDownTimeout = window.setTimeout(() => {
this[onDistanceChange]({pageX: this.pageX, pageY: this.pageY}); this[onDistanceChange]({pageX: this.pageX, pageY: this.pageY});
}, delay); }, delay.mouse);
} }
/** /**
@ -134,8 +134,8 @@ export default class MouseSensor extends Sensor {
*/ */
[onDistanceChange](event) { [onDistanceChange](event) {
const {pageX, pageY} = event; const {pageX, pageY} = event;
const {delay, distance} = this.options; const {distance} = this.options;
const {startEvent} = this; const {startEvent, delay} = this;
Object.assign(this, {pageX, pageY}); Object.assign(this, {pageX, pageY});
@ -146,8 +146,12 @@ export default class MouseSensor extends Sensor {
const timeElapsed = Date.now() - this.onMouseDownAt; const timeElapsed = Date.now() - this.onMouseDownAt;
const distanceTravelled = euclideanDistance(startEvent.pageX, startEvent.pageY, pageX, pageY) || 0; const distanceTravelled = euclideanDistance(startEvent.pageX, startEvent.pageY, pageX, pageY) || 0;
if (timeElapsed >= delay && distanceTravelled >= distance) { clearTimeout(this.mouseDownTimeout);
window.clearTimeout(this.mouseDownTimeout);
if (timeElapsed < delay.mouse) {
// moved during delay
document.removeEventListener('mousemove', this[onDistanceChange]);
} else if (distanceTravelled >= distance) {
document.removeEventListener('mousemove', this[onDistanceChange]); document.removeEventListener('mousemove', this[onDistanceChange]);
this[startDrag](); this[startDrag]();
} }

View File

@ -251,14 +251,17 @@ describe('MouseSensor', () => {
expect(dragFlow).not.toHaveTriggeredSensorEvent('drag:start'); expect(dragFlow).not.toHaveTriggeredSensorEvent('drag:start');
}); });
it('only triggers `drag:start` sensor event once when delay ends after distance is met', () => { it('does not trigger `drag:start` sensor event when moved during delay', () => {
function dragFlow() { function dragFlow() {
clickMouse(draggableElement); clickMouse(draggableElement);
moveMouse(draggableElement, {pageY: 1, pageX: 0}); moveMouse(draggableElement, {pageY: 1, pageX: 0});
const dateMock = waitForDragDelay({restoreDateMock: false});
moveMouse(draggableElement, {pageY: 2, pageX: 0});
waitForDragDelay(); waitForDragDelay();
releaseMouse(document.body); releaseMouse(document.body);
dateMock.mockRestore();
} }
expect(dragFlow).toHaveTriggeredSensorEvent('drag:start', 1); expect(dragFlow).not.toHaveTriggeredSensorEvent('drag:start', 1);
}); });
it('only triggers `drag:start` sensor event once when distance and delay are met at the same time', () => { it('only triggers `drag:start` sensor event once when distance and delay are met at the same time', () => {

View File

@ -1,3 +1,9 @@
const defaultDealy = {
mouse: 0,
drag: 0,
touch: 100,
};
/** /**
* Base sensor class. Extend from this class to create a new or custom sensor * Base sensor class. Extend from this class to create a new or custom sensor
* @class Sensor * @class Sensor
@ -45,6 +51,13 @@ export default class Sensor {
* @type {Event} * @type {Event}
*/ */
this.startEvent = null; this.startEvent = null;
/**
* The delay of each sensor
* @property delay
* @type {Object}
*/
this.delay = calcDelay(options.delay);
} }
/** /**
@ -96,3 +109,37 @@ export default class Sensor {
return sensorEvent; return sensorEvent;
} }
} }
/**
* Calculate the delay of each sensor through the delay in the options
* @param {undefined|Number|Object} optionsDelay - the delay in the options
* @return {Object}
*/
function calcDelay(optionsDelay) {
const delay = {};
if (optionsDelay === undefined) {
return {...defaultDealy};
}
if (typeof optionsDelay === 'number') {
for (const key in defaultDealy) {
if (defaultDealy.hasOwnProperty(key)) {
delay[key] = optionsDelay;
}
}
return delay;
}
for (const key in defaultDealy) {
if (defaultDealy.hasOwnProperty(key)) {
if (optionsDelay[key] === undefined) {
delay[key] = defaultDealy[key];
} else {
delay[key] = optionsDelay[key];
}
}
}
return delay;
}

View File

@ -17,6 +17,38 @@ describe('Sensor', () => {
expect(sensor.containers).toEqual(expectedContainers); expect(sensor.containers).toEqual(expectedContainers);
expect(sensor.options).toEqual(expectedOptions); expect(sensor.options).toEqual(expectedOptions);
}); });
describe('should initialize with correct delay', () => {
it('unset', () => {
const sensor = new Sensor(undefined, {});
expect(sensor.delay).toEqual({
mouse: 0,
drag: 0,
touch: 100,
});
});
it('number', () => {
const sensor = new Sensor(undefined, {delay: 42});
expect(sensor.delay).toEqual({
mouse: 42,
drag: 42,
touch: 42,
});
});
it('object', () => {
const sensor = new Sensor(undefined, {delay: {mouse: 42, drag: 142}});
expect(sensor.delay).toEqual({
mouse: 42,
drag: 142,
touch: 100,
});
});
});
}); });
describe('#attach', () => { describe('#attach', () => {

View File

@ -111,7 +111,8 @@ export default class TouchSensor extends Sensor {
if (!container) { if (!container) {
return; return;
} }
const {distance = 0, delay = 0} = this.options; const {distance = 0} = this.options;
const {delay} = this;
const {pageX, pageY} = touchCoords(event); const {pageX, pageY} = touchCoords(event);
Object.assign(this, {pageX, pageY}); Object.assign(this, {pageX, pageY});
@ -130,7 +131,7 @@ export default class TouchSensor extends Sensor {
this.tapTimeout = window.setTimeout(() => { this.tapTimeout = window.setTimeout(() => {
this[onDistanceChange]({touches: [{pageX: this.pageX, pageY: this.pageY}]}); this[onDistanceChange]({touches: [{pageX: this.pageX, pageY: this.pageY}]});
}, delay); }, delay.touch);
} }
/** /**
@ -166,16 +167,21 @@ export default class TouchSensor extends Sensor {
* @param {Event} event - Touch move event * @param {Event} event - Touch move event
*/ */
[onDistanceChange](event) { [onDistanceChange](event) {
const {delay, distance} = this.options; const {distance} = this.options;
const {startEvent} = this; const {startEvent, delay} = this;
const start = touchCoords(startEvent); const start = touchCoords(startEvent);
const current = touchCoords(event); const current = touchCoords(event);
const timeElapsed = Date.now() - this.onTouchStartAt; const timeElapsed = Date.now() - this.onTouchStartAt;
const distanceTravelled = euclideanDistance(start.pageX, start.pageY, current.pageX, current.pageY); const distanceTravelled = euclideanDistance(start.pageX, start.pageY, current.pageX, current.pageY);
Object.assign(this, current); Object.assign(this, current);
if (timeElapsed >= delay && distanceTravelled >= distance) {
window.clearTimeout(this.tapTimeout); clearTimeout(this.tapTimeout);
if (timeElapsed < delay.touch) {
// moved during delay
document.removeEventListener('touchmove', this[onDistanceChange]);
} else if (distanceTravelled >= distance) {
document.removeEventListener('touchmove', this[onDistanceChange]); document.removeEventListener('touchmove', this[onDistanceChange]);
this[startDrag](); this[startDrag]();
} }

View File

@ -240,15 +240,17 @@ describe('TouchSensor', () => {
expect(dragFlow).not.toHaveTriggeredSensorEvent('drag:start'); expect(dragFlow).not.toHaveTriggeredSensorEvent('drag:start');
}); });
it('only triggers `drag:start` sensor event once when delay ends after distance is met', () => { it('does not trigger `drag:start` sensor event when moved during delay', () => {
function dragFlow() { function dragFlow() {
touchStart(draggableElement); touchStart(draggableElement);
touchMove(draggableElement, {touches: [{pageX: 1, pageY: 0}]}); touchMove(draggableElement, {touches: [{pageX: 1, pageY: 0}]});
waitForDragDelay(); const dateMock = waitForDragDelay({restoreDateMock: false});
touchMove(draggableElement, {touches: [{pageX: 2, pageY: 0}]});
touchRelease(draggableElement); touchRelease(draggableElement);
dateMock.mockRestore();
} }
expect(dragFlow).toHaveTriggeredSensorEvent('drag:start', 1); expect(dragFlow).not.toHaveTriggeredSensorEvent('drag:start');
}); });
it('only triggers `drag:start` sensor event once when delay ends at the same time distance is met', () => { it('only triggers `drag:start` sensor event once when delay ends at the same time distance is met', () => {
@ -258,8 +260,8 @@ describe('TouchSensor', () => {
const dateMock = jest.spyOn(Date, 'now').mockImplementation(() => { const dateMock = jest.spyOn(Date, 'now').mockImplementation(() => {
return next; return next;
}); });
jest.runTimersToTime(DRAG_DELAY);
touchMove(draggableElement, {touches: [{pageX: 1, pageY: 0}]}); touchMove(draggableElement, {touches: [{pageX: 1, pageY: 0}]});
jest.runTimersToTime(DRAG_DELAY);
touchRelease(draggableElement); touchRelease(draggableElement);
dateMock.mockRestore(); dateMock.mockRestore();
} }

View File

@ -352,7 +352,8 @@ describe('Draggable', () => {
waitForDragDelay(); waitForDragDelay();
moveMouse(dynamicContainer); moveMouse(dynamicContainer);
expect(dragOverContainerHandler).not.toHaveBeenCalled(); // will be called once after delay
expect(dragOverContainerHandler).toHaveBeenCalledTimes(1);
releaseMouse(newInstance.source); releaseMouse(newInstance.source);
@ -363,7 +364,7 @@ describe('Draggable', () => {
clickMouse(draggableElement); clickMouse(draggableElement);
waitForDragDelay(); waitForDragDelay();
moveMouse(dynamicContainer); moveMouse(dynamicContainer);
expect(dragOverContainerHandler).toHaveBeenCalled(); expect(dragOverContainerHandler).toHaveBeenCalledTimes(3);
releaseMouse(newInstance.source); releaseMouse(newInstance.source);
}); });
@ -386,7 +387,7 @@ describe('Draggable', () => {
waitForDragDelay(); waitForDragDelay();
moveMouse(dynamicContainer); moveMouse(dynamicContainer);
expect(dragOverContainerHandler).toHaveBeenCalled(); expect(dragOverContainerHandler).toHaveBeenCalledTimes(2);
releaseMouse(newInstance.source); releaseMouse(newInstance.source);
@ -401,7 +402,7 @@ describe('Draggable', () => {
waitForDragDelay(); waitForDragDelay();
moveMouse(dynamicContainer); moveMouse(dynamicContainer);
expect(dragOverContainerHandler).not.toHaveBeenCalled(); expect(dragOverContainerHandler).toHaveBeenCalledTimes(1);
releaseMouse(newInstance.source); releaseMouse(newInstance.source);
}); });