Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drag and drop support #1139

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
81 changes: 81 additions & 0 deletions addon/addon-test-support/@ember/test-helpers/dom/drag-move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { settled } from '..';
import { registerHook, runHooks } from '../helper-hooks';
import getElement from './-get-element';
import { log } from './-logging';
import Target from './-target';
import fireEvent from './fire-event';

registerHook(
'dragMove',
'start',
(draggableElement: Target, targetElement: Target) => {
log('dragMove', draggableElement, targetElement);
}
);

/**
Drag and drop

Simulates a drag and drop movement from a draggable source element that is dropped on a drop target element

@public
@param {element} draggableStartElement - the element or selector that the drag and drop begins on
@param {element} dropTargetElement - element the element or selector that receives the drop
@returns {promise} returns event with dataTransfer object
*/
export default function dragMove(
draggableStartElement: Target,
dropTargetElement: Target
): Promise<void> {
const context: Record<string, string> = {};

if (!draggableStartElement) {
throw new Error(
`Element not found: ${draggableStartElement}. Must pass an element or selector.`
);
}
if (!dropTargetElement) {
throw new Error(
`Element not found: ${dropTargetElement}. Must pass an element or selector.`
);
}

const dragEl = getElement(draggableStartElement) as Element | HTMLElement;

const targetEl = getElement(dropTargetElement) as Element | HTMLElement;

return Promise.resolve()
.then(() =>
runHooks('dragMove', 'start', draggableStartElement, dropTargetElement)
)
.then(async () => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a naive try to get it to work as it was before the fireEvent change.

fireEvent(dragEl, 'dragstart', {
dataTransfer: {
setData(someKey: string, value: string) {
context[someKey] = value;
},
},
})
.then(() => {
fireEvent(targetEl, 'dragenter');
})
.then(() => {
fireEvent(targetEl, 'dragover');
})
.then(() => {
fireEvent(targetEl, 'drop', {
dataTransfer: {
getData(someKey: string) {
return context[someKey];
},
},
});
})
.then(() => {
return settled();
});
})
.then(() =>
runHooks('dragMove', 'end', draggableStartElement, dropTargetElement)
);
}
73 changes: 73 additions & 0 deletions addon/addon-test-support/@ember/test-helpers/dom/dragMove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { settled } from '..';
import { registerHook, runHooks } from '../helper-hooks';
import getElement from './-get-element';
import { log } from './-logging';
import Target from './-target';
import fireEvent from './fire-event';

registerHook('fillIn', 'start', (target: Target, text: string) => {
log('fillIn', target, text);
});

/**
Drag and drop

Simulates a drag and drop movement from a draggable source element that is dropped on a drop target element

@public
@param {element} draggableStartElement - the element or selector that the drag and drop begins on
@param {element} dropTargetElement - element the element or selector that receives the drop
@returns {promise} returns event with dataTransfer object
*/
export default function dragMove(
draggableStartElement: Target,
dropTargetElement: Target
): Promise<void> {
const context: Record<string, string> = {};

return Promise.resolve()
.then(() =>
runHooks('dragMove', 'start', draggableStartElement, dropTargetElement)
)
.then(() => {
if (!draggableStartElement) {
throw new Error(
`Element not found: ${draggableStartElement}. Must pass an element or selector.`
);
}
if (!dropTargetElement) {
throw new Error(
`Element not found: ${dropTargetElement}. Must pass an element or selector.`
);
}

const dragEl = getElement(draggableStartElement) as Element | HTMLElement;

const targetEl = getElement(draggableStartElement) as
| Element
| HTMLElement;

fireEvent(dragEl, 'dragstart', {
dataTransfer: {
setData(someKey: string, value: string) {
context[someKey] = value;
},
},
});

fireEvent(targetEl, 'dragover');

fireEvent(targetEl, 'drop', {
dataTransfer: {
getData(someKey: string) {
return context[someKey];
},
},
});

return settled();
})
.then(() =>
runHooks('moveDrag', 'end', draggableStartElement, dropTargetElement)
);
}
14 changes: 10 additions & 4 deletions addon/addon-test-support/@ember/test-helpers/dom/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@ export function isKeyboardEventType(

const MOUSE_EVENT_TYPES = tuple(
'click',
'mousedown',
'mouseup',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover'
'mouseover',
'mouseup'
);
export type MouseEventType = (typeof MOUSE_EVENT_TYPES)[number];

Expand Down Expand Up @@ -200,7 +207,6 @@ function buildMouseEvent(type: MouseEventType, options: any = {}) {
event = buildBasicEvent(type, options);
}
}

return event;
}

Expand Down
2 changes: 2 additions & 0 deletions addon/addon-test-support/@ember/test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export { default as find } from './dom/find';
export { default as findAll } from './dom/find-all';
export { default as typeIn } from './dom/type-in';
export { default as scrollTo } from './dom/scroll-to';
export { default as dragMove } from './dom/drag-move';

export type { default as Target } from './dom/-target';

// Declaration-merge for our internal purposes.
Expand Down
117 changes: 117 additions & 0 deletions addon/tests/integration/dom/drag-move-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { module, test } from 'qunit';
import { hbs } from 'ember-cli-htmlbars';
import {
dragMove,
find,
render,
setupContext,
setupRenderingContext,
teardownContext,
} from '@ember/test-helpers';
import hasEmberVersion from '@ember/test-helpers/has-ember-version';

module('DOM Helper: moveDrag', function (hooks) {
if (!hasEmberVersion(2, 4)) {
return;
}

hooks.beforeEach(async function () {
await setupContext(this);
await setupRenderingContext(this);
});

hooks.afterEach(async function () {
await teardownContext(this);
});

test('Drag and drop triggers native events', async function (assert) {
this.set('dragstartTriggered', false);
this.set('dragoverTriggered', false);
this.set('dragenterTriggered', false);
this.set('dropTriggered', false);

this.set('handleDragStart', () => {
this.set('dragStarted', true);
});
this.set('handleDragOver', (event) => {
// Per docs, you should prevent default on over and enter in order
// for drop zone to be a valid target
event.preventDefault();
this.set('dragoverTriggered', true);
});
this.set('handleDragEnter', (event) => {
event.preventDefault();
this.set('dragenterTriggered', true);
});
this.set('handleDrop', () => {
console.log('dropped');
this.set('dropTriggered', true);
});

await render(hbs`
<div draggable='true' class='source'>Test draggable text</div>
<div class='target'>Drag text here</div>
`);

/* The below addEventListeners are used because Ember 3.8 did not have the `on` helper, and
* this test errors in ember-test-helpers github ci.
*
* This is how the dom rendering above would loook like with modern ember templates:
*
* <div draggable='true' class='source' {{on 'dragstart' this.handleDragStart}}>Test draggable text</div>
* <div class='target'
* {{on "dragover" this.handleDragOver}}
* {{on "dragenter" this.handleDragEnter}}
* {{on "drop" this.handleDrop}}
* >Drag text here</div>
*/
const sourceEl = find('.source');
sourceEl.addEventListener('dragstart', this.handleDragStart);
const targetEl = find('.target');
targetEl.addEventListener('drop', this.handleDrop);
targetEl.addEventListener('dragover', this.handleDragOver);
targetEl.addEventListener('dragenter', this.handleDragEnter);

await dragMove('.source', '.target');

assert.true(this.get('dragStarted'));
assert.true(this.get('dragenterTriggered'));
assert.true(this.get('dragoverTriggered'));
assert.true(this.get('dropTriggered'));
});

test('dataTransfer mock transfers data like html api', async function (assert) {
const testString = 'Test string!';

this.set('handleDragStart', (event) => {
event.dataTransfer.setData('test', testString);
});
this.set('handleDragOver', (event) => {
event.preventDefault();
});
this.set('handleDragEnter', (event) => {
event.preventDefault();
});
this.set('handleDrop', (event) => {
const payload = event.dataTransfer.getData('test');

this.set('deliveredPayload', payload);
});

await render(hbs`
<div draggable='true' class='source'>Test draggable text</div>
<div class='target'>Drag text here</div>
`);

const sourceEl = find('.source');
sourceEl.addEventListener('dragstart', this.handleDragStart);
const targetEl = find('.target');
targetEl.addEventListener('drop', this.handleDrop);
targetEl.addEventListener('dragover', this.handleDragOver);
targetEl.addEventListener('dragenter', this.handleDragEnter);

await dragMove('.source', '.target');

assert.equal(testString, this.get('deliveredPayload'));
});
});