From b98cbdcc7c5ff75a278e4a29d7d02f4f27978443 Mon Sep 17 00:00:00 2001 From: Maria Tsvyatkova Date: Mon, 6 Jan 2025 10:38:11 +0200 Subject: [PATCH] fix(tm): swap positions only once on drag over --- src/components/common/context.ts | 1 + src/components/common/utils.spec.ts | 3 +- src/components/tile-manager/tile-dnd.spec.ts | 36 ++++++++++++++- src/components/tile-manager/tile-manager.ts | 8 +++- src/components/tile-manager/tile.ts | 46 ++++++++++++++++++-- 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/components/common/context.ts b/src/components/common/context.ts index e45b3523c..7c9876090 100644 --- a/src/components/common/context.ts +++ b/src/components/common/context.ts @@ -6,6 +6,7 @@ import type IgcTileComponent from '../tile-manager/tile.js'; export type TileManagerContext = { instance: IgcTileManagerComponent; draggedItem: IgcTileComponent | null; + lastSwapTile: IgcTileComponent | null; }; export type TileContext = { diff --git a/src/components/common/utils.spec.ts b/src/components/common/utils.spec.ts index ce20ef0f9..82abf1d77 100644 --- a/src/components/common/utils.spec.ts +++ b/src/components/common/utils.spec.ts @@ -347,11 +347,12 @@ export function simulateDragStart(node: Element) { ); } -export function simulateDragOver(node: Element) { +export function simulateDragOver(node: Element, options?: PointerEventInit) { node.dispatchEvent( new DragEvent('dragover', { bubbles: true, composed: true, + ...options, }) ); } diff --git a/src/components/tile-manager/tile-dnd.spec.ts b/src/components/tile-manager/tile-dnd.spec.ts index f831557f7..ede28f93b 100644 --- a/src/components/tile-manager/tile-dnd.spec.ts +++ b/src/components/tile-manager/tile-dnd.spec.ts @@ -1,5 +1,5 @@ import { elementUpdated, expect, fixture, html } from '@open-wc/testing'; -import { spy } from 'sinon'; +import { spy, stub } from 'sinon'; import { range } from 'lit/directives/range.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; @@ -9,7 +9,10 @@ import { simulateDragOver, simulateDragStart, simulateDrop, + simulatePointerDown, + simulatePointerMove, } from '../common/utils.spec.js'; +import * as PositionUtils from './position.js'; import IgcTileManagerComponent from './tile-manager.js'; import type IgcTileComponent from './tile.js'; @@ -18,6 +21,8 @@ describe('Tile drag and drop', () => { defineComponents(IgcTileManagerComponent); }); + const getBoundingRect = (el: Element) => el.getBoundingClientRect(); + let tileManager: IgcTileManagerComponent; function getTiles() { @@ -180,5 +185,34 @@ describe('Tile drag and drop', () => { expect(tileManager.tiles[0].id).to.equal('tile0'); expect(tileManager.tiles[4].id).to.equal('tile4'); }); + + it('should swap positions only once while dragging smaller tile over bigger tile in slide mode', async () => { + tileManager.columnCount = 5; + const draggedTile = first(tileManager.tiles); + const dropTarget = tileManager.tiles[1]; + + draggedTile.rowSpan = 1; + draggedTile.colSpan = 1; + + dropTarget.rowSpan = 3; + dropTarget.colSpan = 3; + await elementUpdated(tileManager); + + const dropTargetRect = dropTarget.getBoundingClientRect(); + + simulateDragStart(draggedTile); + simulateDragOver(dropTarget); + await elementUpdated(tileManager); + + // Simulate second dragover event (inside dropTarget bounds) + simulateDragOver(dropTarget, { + clientX: dropTargetRect.left + dropTargetRect.width / 2, + clientY: dropTargetRect.top + dropTargetRect.height / 3, + }); + await elementUpdated(tileManager); + + expect(draggedTile.position).to.equal(1); + expect(dropTarget.position).to.equal(0); + }); }); }); diff --git a/src/components/tile-manager/tile-manager.ts b/src/components/tile-manager/tile-manager.ts index fd5acbadd..5e4337725 100644 --- a/src/components/tile-manager/tile-manager.ts +++ b/src/components/tile-manager/tile-manager.ts @@ -59,6 +59,7 @@ export default class IgcTileManagerComponent extends EventEmitterMixin< private _minColWidth?: string; private _minRowHeight?: string; private _draggedItem: IgcTileComponent | null = null; + private _lastSwapTile: IgcTileComponent | null = null; private _serializer = createSerializer(this); private _tilesState = createTilesState(this); @@ -68,12 +69,17 @@ export default class IgcTileManagerComponent extends EventEmitterMixin< initialValue: { instance: this, draggedItem: this._draggedItem, + lastSwapTile: this._lastSwapTile, }, }); private _setManagerContext() { this._context.setValue( - { instance: this, draggedItem: this._draggedItem }, + { + instance: this, + draggedItem: this._draggedItem, + lastSwapTile: this._lastSwapTile, + }, true ); } diff --git a/src/components/tile-manager/tile.ts b/src/components/tile-manager/tile.ts index 2bf0e9c28..cb3f8818a 100644 --- a/src/components/tile-manager/tile.ts +++ b/src/components/tile-manager/tile.ts @@ -351,7 +351,7 @@ export default class IgcTileComponent extends EventEmitterMixin< this._hasDragOver = true; } - private handleDragOver() { + private handleDragOver(event: DragEvent) { if (!this._draggedItem) { return; } @@ -363,8 +363,21 @@ export default class IgcTileComponent extends EventEmitterMixin< visibility: 'visible', }); } + if (this._managerContext) { + this._managerContext.lastSwapTile = null; + } } else if (this._isSlideMode) { - swapTiles(this, this._draggedItem!); + if ( + this._managerContext && + (!this._managerContext.lastSwapTile || + this.hasPointerLeftLastSwapTile( + event, + this._managerContext.lastSwapTile + )) + ) { + this._managerContext.lastSwapTile = this; + swapTiles(this, this._draggedItem!); + } } } @@ -372,7 +385,7 @@ export default class IgcTileComponent extends EventEmitterMixin< this._dragCounter--; // The drag leave is fired on entering a child element - // so we need to check if the dragged item is actually leaving the tile + // so we check if the dragged item is actually leaving the tile if (this._dragCounter === 0) { this._hasDragOver = false; } @@ -395,6 +408,31 @@ export default class IgcTileComponent extends EventEmitterMixin< this._tileContent.style.visibility = 'visible'; } + private hasPointerLeftLastSwapTile( + event: DragEvent, + lastSwapTile: IgcTileComponent | null + ) { + if (!lastSwapTile) return false; + + // Check if the pointer is outside the boundaries of the last swapped tile + + const rect = lastSwapTile.getBoundingClientRect(); + const pointerX = event.clientX; + const pointerY = event.clientY; + + const outsideBoundaries = + pointerX < rect.left || + pointerX > rect.right || + pointerY < rect.top || + pointerY > rect.bottom; + + if (outsideBoundaries && this._managerContext) { + this._managerContext.lastSwapTile = null; + } + + return outsideBoundaries; + } + private cacheStyles() { //use util const computedStyle = getComputedStyle(this); @@ -530,7 +568,7 @@ export default class IgcTileComponent extends EventEmitterMixin< protected renderContent() { const parts = partNameMap({ base: true, - 'drag-over': this._hasDragOver, + 'drag-over': this._hasDragOver && !this._isSlideMode, fullscreen: this.fullscreen, draggable: !this.disableDrag, dragging: this._isDragging,