Skip to content

Commit 9d1a059

Browse files
committed
refactor: Tile drag operations
* Removed disableDrag property and updated tests and assumptions around the property * Added logic to restore initial state when canceling a drag operation * Refactoring inside the drag and resize controllers * Enabled and adjusted unit tests
1 parent cf53a31 commit 9d1a059

File tree

14 files changed

+690
-373
lines changed

14 files changed

+690
-373
lines changed

src/components/common/controllers/drag.spec.ts

Lines changed: 289 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import {
1313
simulateKeyboard,
1414
simulateLostPointerCapture,
1515
simulatePointerDown,
16+
simulatePointerMove,
1617
} from '../utils.spec.js';
1718
import { addDragController } from './drag.js';
1819
import { escapeKey } from './key-bindings.js';
1920

20-
describe('Drag and drop controller', () => {
21+
describe('Drag controller', () => {
2122
type DragElement = LitElement & {
2223
controller: ReturnType<typeof addDragController>;
2324
};
@@ -60,15 +61,15 @@ describe('Drag and drop controller', () => {
6061
const root = await fixture(template);
6162

6263
instance = root.querySelector(tagName._$litStatic$)!;
63-
instance.controller.setConfig({ mode: 'immediate' });
64+
instance.controller.set({ mode: 'immediate' });
6465
});
6566

6667
afterEach(() => {
6768
dragStart.resetHistory();
6869
});
6970

7071
it('should not start drag operation when disabled', async () => {
71-
instance.controller.setConfig({ enabled: false, dragStart });
72+
instance.controller.set({ enabled: false, start: dragStart });
7273

7374
simulatePointerDown(instance);
7475
await elementUpdated(instance);
@@ -77,25 +78,27 @@ describe('Drag and drop controller', () => {
7778
});
7879

7980
it('should not start a drag operation on a non-primary button interaction', async () => {
80-
instance.controller.setConfig({ dragStart });
81+
instance.controller.set({ start: dragStart });
8182

8283
simulatePointerDown(instance, { button: 1 });
8384
await elementUpdated(instance);
8485

8586
expect(dragStart.called).is.false;
8687
});
8788

88-
it('should invoke dragStart callback on drag operation', async () => {
89-
instance.controller.setConfig({ dragStart });
89+
it('should not start a drag operation when a skip callback returns true', async () => {
90+
const skip = spy(() => true);
91+
instance.controller.set({ skip, start: dragStart });
9092

9193
simulatePointerDown(instance);
9294
await elementUpdated(instance);
9395

94-
expect(dragStart.called).is.true;
96+
expect(skip.called).is.true;
97+
expect(dragStart.called).is.false;
9598
});
9699

97100
it('should apply correct internal styles on drag operation', async () => {
98-
instance.controller.setConfig({ dragStart });
101+
instance.controller.set({ start: dragStart });
99102
const styles = { touchAction: 'none', userSelect: 'none' };
100103

101104
simulatePointerDown(instance);
@@ -111,16 +114,278 @@ describe('Drag and drop controller', () => {
111114
expect(instance.attributeStyleMap.size).to.equal(0);
112115
});
113116

114-
it('should not create ghost element in immediate mode', async () => {
115-
instance.controller.setConfig({ dragStart });
117+
it('should not create a ghost element in "immediate" mode', async () => {
118+
const ghost = spy();
119+
instance.controller.set({ start: dragStart, ghost });
120+
121+
simulatePointerDown(instance);
122+
await elementUpdated(instance);
123+
124+
expect(dragStart.called).is.true;
125+
expect(ghost.called).is.false;
126+
});
127+
128+
it('should not invoke the `layer` configuration callback in "immediate" mode', async () => {
129+
const layer = spy();
130+
instance.controller.set({ start: dragStart, layer });
131+
132+
simulatePointerDown(instance);
133+
await elementUpdated(instance);
134+
135+
expect(dragStart.called).is.true;
136+
expect(layer.called).is.false;
137+
});
138+
139+
it('should invoke start callback on drag operation', async () => {
140+
instance.controller.set({ start: dragStart });
141+
142+
simulatePointerDown(instance);
143+
await elementUpdated(instance);
144+
145+
expect(dragStart.called).is.true;
146+
expect(dragStart.callCount).to.equal(1);
147+
});
148+
149+
it('should not invoke move unless a start is invoked', async () => {
150+
const dragMove = spy();
151+
instance.controller.set({ start: dragStart, move: dragMove });
152+
153+
simulatePointerMove(instance);
154+
await elementUpdated(instance);
155+
156+
expect(dragStart.called).is.false;
157+
expect(dragMove.called).is.false;
158+
});
159+
160+
it('should invoke move when moving the dragged element around the viewport', async () => {
161+
const dragMove = spy();
162+
instance.controller.set({ start: dragStart, move: dragMove });
163+
164+
simulatePointerDown(instance);
165+
simulatePointerMove(
166+
instance,
167+
{ clientX: 200, clientY: 200 },
168+
{ x: 10, y: 10 },
169+
10
170+
);
171+
await elementUpdated(instance);
172+
173+
expect(dragStart.called).is.true;
174+
expect(dragMove.called).is.true;
175+
expect(dragMove.callCount).to.equal(10);
176+
});
177+
178+
it('should invoke end when releasing the dragged element', async () => {
179+
const dragEnd = spy();
180+
instance.controller.set({ start: dragStart, end: dragEnd });
116181

117182
simulatePointerDown(instance);
183+
simulateLostPointerCapture(instance);
118184
await elementUpdated(instance);
185+
186+
expect(dragStart.callCount).to.equal(1);
187+
expect(dragEnd.callCount).to.equal(1);
119188
});
120189

121-
it('should fire dragCancel when pressing Escape during a drag operation', async () => {
190+
it('should invoke cancel when pressing Escape during a drag operation', async () => {
122191
const dragCancel = spy();
123-
instance.controller.setConfig({ dragStart, dragCancel });
192+
instance.controller.set({ start: dragStart, cancel: dragCancel });
193+
194+
simulatePointerDown(instance);
195+
await elementUpdated(instance);
196+
197+
expect(dragStart.called).is.true;
198+
199+
simulateKeyboard(instance, escapeKey);
200+
await elementUpdated(instance);
201+
202+
expect(dragCancel.called).is.true;
203+
});
204+
205+
it('should not invoke cancel when pressing Escape outside of drag operation', async () => {
206+
// Sanity check since the Escape key handler is a root level dynamic listener.
207+
const dragCancel = spy();
208+
instance.controller.set({ cancel: dragCancel });
209+
210+
simulateKeyboard(instance, escapeKey);
211+
await elementUpdated(instance);
212+
213+
expect(dragCancel.called).is.false;
214+
});
215+
});
216+
217+
describe('Deferred mode - basic element dragging', () => {
218+
const dragStart = spy();
219+
220+
function createGhost() {
221+
const clone = instance.cloneNode() as HTMLElement;
222+
clone.setAttribute('data-deferred-drag-ghost', '');
223+
224+
return clone;
225+
}
226+
227+
function getDefaultGhost() {
228+
return document.querySelector('section')?.querySelector('div')!;
229+
}
230+
231+
function getCustomGhost() {
232+
return document.querySelector<HTMLElement>('[data-deferred-drag-ghost]')!;
233+
}
234+
235+
beforeEach(async () => {
236+
const tagName = unsafeStatic(tag);
237+
238+
const template = html`
239+
<section style="width: 1000px; height: 1000px">
240+
<${tagName}></${tagName}>
241+
</section>
242+
`;
243+
244+
const root = await fixture(template);
245+
246+
instance = root.querySelector(tagName._$litStatic$)!;
247+
instance.controller.set({ mode: 'deferred' });
248+
});
249+
250+
afterEach(() => {
251+
dragStart.resetHistory();
252+
});
253+
254+
it('should not start drag operation when disabled', async () => {
255+
instance.controller.set({ enabled: false, start: dragStart });
256+
257+
simulatePointerDown(instance);
258+
await elementUpdated(instance);
259+
260+
expect(dragStart.called).is.false;
261+
});
262+
263+
it('should not start a drag operation on a non-primary button interaction', async () => {
264+
instance.controller.set({ start: dragStart });
265+
266+
simulatePointerDown(instance, { button: 1 });
267+
await elementUpdated(instance);
268+
269+
expect(dragStart.called).is.false;
270+
});
271+
272+
it('should not start a drag operation when a skip callback returns true', async () => {
273+
const skip = spy(() => true);
274+
instance.controller.set({ skip, start: dragStart });
275+
276+
simulatePointerDown(instance);
277+
await elementUpdated(instance);
278+
279+
expect(skip.called).is.true;
280+
expect(dragStart.called).is.false;
281+
});
282+
283+
it('should apply correct internal styles on drag operation', async () => {
284+
instance.controller.set({ start: dragStart });
285+
const styles = { touchAction: 'none', userSelect: 'none' };
286+
287+
simulatePointerDown(instance);
288+
await elementUpdated(instance);
289+
290+
// Default internal styles are touch-action: none & user-select: none while in drag mode.
291+
expect(instance.attributeStyleMap.size).to.equal(2);
292+
expect(compareStyles(instance, styles)).is.true;
293+
294+
simulateLostPointerCapture(instance);
295+
await elementUpdated(instance);
296+
297+
expect(instance.attributeStyleMap.size).to.equal(0);
298+
});
299+
300+
it('should create a default ghost element in "deferred" mode if no configuration is passed', async () => {
301+
simulatePointerDown(instance);
302+
await elementUpdated(instance);
303+
304+
const defaultGhost = getDefaultGhost();
305+
306+
expect(defaultGhost).to.exist;
307+
expect(defaultGhost.getBoundingClientRect()).to.eql(
308+
instance.getBoundingClientRect()
309+
);
310+
});
311+
312+
it('should create a custom ghost element in "deferred" mode when a configuration is passed', async () => {
313+
instance.controller.set({ ghost: createGhost });
314+
315+
simulatePointerDown(instance);
316+
await elementUpdated(instance);
317+
318+
const customGhost = getCustomGhost();
319+
320+
expect(customGhost).to.exist;
321+
expect(customGhost.localName).to.equal(instance.localName);
322+
});
323+
324+
it('should correctly place the ghost element in the configured layer container', async () => {
325+
const layer = spy(() => instance.parentElement!);
326+
instance.controller.set({ layer });
327+
328+
simulatePointerDown(instance);
329+
await elementUpdated(instance);
330+
331+
expect(getDefaultGhost().parentElement).to.eql(instance.parentElement);
332+
});
333+
334+
it('should invoke start callback on drag operation', async () => {
335+
instance.controller.set({ start: dragStart });
336+
337+
simulatePointerDown(instance);
338+
await elementUpdated(instance);
339+
340+
expect(dragStart.called).is.true;
341+
expect(dragStart.callCount).to.equal(1);
342+
});
343+
344+
it('should not invoke move unless a start is invoked', async () => {
345+
const dragMove = spy();
346+
instance.controller.set({ start: dragStart, move: dragMove });
347+
348+
simulatePointerMove(instance);
349+
await elementUpdated(instance);
350+
351+
expect(dragStart.called).is.false;
352+
expect(dragMove.called).is.false;
353+
});
354+
355+
it('should invoke move when moving the dragged element around the viewport', async () => {
356+
const dragMove = spy();
357+
instance.controller.set({ start: dragStart, move: dragMove });
358+
359+
simulatePointerDown(instance);
360+
simulatePointerMove(
361+
instance,
362+
{ clientX: 200, clientY: 200 },
363+
{ x: 10, y: 10 },
364+
10
365+
);
366+
await elementUpdated(instance);
367+
368+
expect(dragStart.called).is.true;
369+
expect(dragMove.called).is.true;
370+
expect(dragMove.callCount).to.equal(10);
371+
});
372+
373+
it('should invoke end when releasing the dragged element', async () => {
374+
const dragEnd = spy();
375+
instance.controller.set({ start: dragStart, end: dragEnd });
376+
377+
simulatePointerDown(instance);
378+
simulateLostPointerCapture(instance);
379+
await elementUpdated(instance);
380+
381+
expect(dragStart.callCount).to.equal(1);
382+
expect(dragEnd.callCount).to.equal(1);
383+
expect(getDefaultGhost()).is.null;
384+
});
385+
386+
it('should invoke cancel when pressing Escape during a drag operation', async () => {
387+
const dragCancel = spy(() => instance.controller.dispose());
388+
instance.controller.set({ start: dragStart, cancel: dragCancel });
124389

125390
simulatePointerDown(instance);
126391
await elementUpdated(instance);
@@ -131,6 +396,18 @@ describe('Drag and drop controller', () => {
131396
await elementUpdated(instance);
132397

133398
expect(dragCancel.called).is.true;
399+
expect(getDefaultGhost()).is.null;
400+
});
401+
402+
it('should not invoke cancel when pressing Escape outside of drag operation', async () => {
403+
// Sanity check since the Escape key handler is a root level dynamic listener.
404+
const dragCancel = spy();
405+
instance.controller.set({ cancel: dragCancel });
406+
407+
simulateKeyboard(instance, escapeKey);
408+
await elementUpdated(instance);
409+
410+
expect(dragCancel.called).is.false;
134411
});
135412
});
136413
});

0 commit comments

Comments
 (0)