diff --git a/src/components/common/controllers/drag.ts b/src/components/common/controllers/drag.ts index 9a9065c49..a9e92e4cb 100644 --- a/src/components/common/controllers/drag.ts +++ b/src/components/common/controllers/drag.ts @@ -6,7 +6,12 @@ import type { import type { Ref } from 'lit/directives/ref.js'; import { getDefaultLayer } from '../../resize-container/default-ghost.js'; -import { findElementFromEventPath, getRoot, roundByDPR } from '../util.js'; +import { + findElementFromEventPath, + getRoot, + isLTR, + roundByDPR, +} from '../util.js'; type DragCallback = (parameters: DragCallbackParameters) => unknown; type DragCancelCallback = (state: DragState) => unknown; @@ -21,6 +26,11 @@ type State = { current: DOMRect; position: { x: number; y: number }; offset: { x: number; y: number }; + pointerState: { + initial: { initialX: number; initialY: number }; + current: { currentX: number; currentY: number }; + direction: Direction; + }; }; type DragState = State & { @@ -28,6 +38,8 @@ type DragState = State & { element: Element | null; }; +type Direction = 'start' | 'end' | 'bottom' | 'top'; + type DragControllerConfiguration = { /** Whether the drag feature is enabled for the current host. */ enabled?: boolean; @@ -157,6 +169,8 @@ class DragController implements ReactiveController { } private get _stateParameters(): DragState { + this._state.pointerState.direction = this._trackPointerMovement(); + return { ...this._state, ghost: this._ghost, @@ -164,6 +178,21 @@ class DragController implements ReactiveController { }; } + private _trackPointerMovement(): Direction { + const { initialX, initialY } = this._state.pointerState.initial; + const { currentX, currentY } = this._state.pointerState.current; + const deltaX = currentX - initialX; + const deltaY = currentY - initialY; + const LTR = isLTR(this._host); + + const isHorizontalMove = Math.abs(deltaX) >= Math.abs(deltaY); + + if (isHorizontalMove) { + return (LTR ? deltaX >= 0 : deltaX <= 0) ? 'end' : 'start'; + } + return deltaY >= 0 ? 'bottom' : 'top'; + } + constructor( host: ReactiveControllerHost & LitElement, options?: DragControllerConfiguration @@ -249,7 +278,11 @@ class DragController implements ReactiveController { this._createDragGhost(); this._updatePosition(event); - const parameters = { event, state: this._stateParameters }; + const parameters = { + event, + state: this._stateParameters, + }; + if (this._options.start?.call(this._host, parameters) === false) { this.dispose(); return; @@ -267,7 +300,20 @@ class DragController implements ReactiveController { this._updatePosition(event); this._updateMatcher(event); - const parameters = { event, state: this._stateParameters }; + this._state.pointerState.initial = { + initialX: this._state.pointerState.current.currentX, + initialY: this._state.pointerState.current.currentY, + }; + this._state.pointerState.current = { + currentX: event.clientX, + currentY: event.clientY, + }; + + const parameters = { + event, + state: this._stateParameters, + }; + this._options.move?.call(this._host, parameters); this._assignPosition(this._dragItem); @@ -313,6 +359,11 @@ class DragController implements ReactiveController { current: structuredClone(rect), position, offset, + pointerState: { + initial: { initialX: clientX, initialY: clientY }, + current: { currentX: clientX, currentY: clientY }, + direction: 'end', + }, }; } diff --git a/src/components/tile-manager/tile-dnd.spec.ts b/src/components/tile-manager/tile-dnd.spec.ts index bfe365838..6057367f1 100644 --- a/src/components/tile-manager/tile-dnd.spec.ts +++ b/src/components/tile-manager/tile-dnd.spec.ts @@ -75,7 +75,7 @@ describe('Tile drag and drop', () => { tile, { clientX: opts.x, clientY: opts.y }, { x: opts.dx, y: opts.dy }, - 2 + 3 ); } @@ -292,6 +292,22 @@ describe('Tile drag and drop', () => { await elementUpdated(draggedTile); }); + it('should swap positions properly in RTL mode', async () => { + tileManager.dir = 'rtl'; + const draggedTile = getTile(0); + const dropTarget = getTile(1); + + await elementUpdated(tileManager); + + expect(draggedTile.position).to.equal(0); + expect(dropTarget.position).to.equal(1); + + await dragAndDrop(draggedTile, dropTarget); + + expect(draggedTile.position).to.equal(1); + expect(dropTarget.position).to.equal(0); + }); + it('should swap positions properly when row, column and span are specified', async () => { const draggedTile = getTile(0); const dropTarget = getTile(1); diff --git a/src/components/tile-manager/tile-ghost-util.ts b/src/components/tile-manager/tile-ghost-util.ts index a2dbab53f..f5799c9dc 100644 --- a/src/components/tile-manager/tile-ghost-util.ts +++ b/src/components/tile-manager/tile-ghost-util.ts @@ -1,3 +1,4 @@ +import { isLTR } from '../common/util.js'; import type IgcTileComponent from './tile.js'; export function createTileDragGhost(tile: IgcTileComponent): IgcTileComponent { @@ -13,6 +14,7 @@ export function createTileDragGhost(tile: IgcTileComponent): IgcTileComponent { }); Object.assign(clone.style, { + direction: isLTR(tile) ? 'ltr' : 'rtl', position: 'absolute', contain: 'strict', top: 0, diff --git a/src/components/tile-manager/tile.ts b/src/components/tile-manager/tile.ts index 58d0e1b07..715c4f1ff 100644 --- a/src/components/tile-manager/tile.ts +++ b/src/components/tile-manager/tile.ts @@ -26,8 +26,8 @@ import { asNumber, createCounter, findElementFromEventPath, - getCenterPoint, isEmpty, + isLTR, partNameMap, } from '../common/util.js'; import IgcDividerComponent from '../divider/divider.js'; @@ -135,6 +135,7 @@ export default class IgcTileComponent extends EventEmitterMixin< private _position = -1; private _resizeState = createTileResizeState(); private _dragStack = createTileDragStack(); + private _previousPointerPosition: { x: number; y: number } | null = null; private _customAdorners = new Map( Object.entries({ @@ -399,17 +400,40 @@ export default class IgcTileComponent extends EventEmitterMixin< return true; } - private _handleDragOver(parameters: DragCallbackParameters) { + private _handleDragOver(parameters: DragCallbackParameters): void { const match = parameters.state.element as IgcTileComponent; - const { clientX, clientY } = parameters.event; if (this._dragStack.peek() === match) { - const { x, y } = getCenterPoint(match); - - const shouldSwap = - this.position <= match.position - ? clientX > x || clientY > y - : clientX < x || clientY < y; + const direction = parameters.state.pointerState.direction; + const { clientX, clientY } = parameters.event; + const { left, top, width, height } = match.getBoundingClientRect(); + const relativeX = (clientX - left) / width; + const relativeY = (clientY - top) / height; + const LTR = isLTR(this); + + let shouldSwap = false; + + switch (direction) { + case 'start': + shouldSwap = + this.position > match.position && + (LTR ? relativeX <= 0.25 : relativeX >= 0.75); + break; + + case 'end': + shouldSwap = + this.position < match.position && + (LTR ? relativeX >= 0.75 : relativeX <= 0.25); + break; + + case 'top': + shouldSwap = this.position > match.position && relativeY <= 0.25; + break; + + case 'bottom': + shouldSwap = this.position < match.position && relativeY >= 0.75; + break; + } if (shouldSwap) { this._dragStack.pop();