You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(drag-drop): cancel an in-progress drag with Escape or cancelDrag() - #17744
Adds a way to cancel an igxDrag drag that is in progress, either with the new cancelDrag() method or, opt-in, with Escape.
IgxDragDirective
New cancelDrag(): void ends the drag without dropping:
no igxDrop is dispatched; a drop area under the pointer only gets leave;
dragEnd is emitted with cancelled: true and the current pointer position, and the element returns to where it was before the drag (the ghost is removed, or with ghost = false the element gets back its full pre-drag inline transform);
any later pointer move or release is ignored until a new drag starts;
it does nothing when no drag is in progress, including while the pointer is pressed but hasn't moved beyond dragTolerance (the release still emits dragClick);
it is safe to call from the drag's own handlers:
dragStart: dragEnd/transitioned are still emitted with cancelled: true, so every dragStart gets a matching dragEnd;
ghostCreate, including while a ghostTemplate ghost is created;
dragMove;
drop-area enter/leave.
The drag stops right away, with no orphan ghost, no further movement and no further drop-area events;
pointer capture is released from whichever element holds it (the ghost, or the base element / drag handle when ghost = false).
New @Input() cancelOnEscape = false (opt-in): when true, Escape cancels the drag.
The listener is on document, because focus is on <body> during a pointer drag. It is attached only while a drag is active, in the capture phase.
Escape is consumed (preventDefault + stopPropagation) only when it actually cancels a drag, so it doesn't also close a dialog or an overlay. Outside a drag it isn't touched.
New cancelled?: boolean on IDragBaseEventArgs, set for both dragEnd and transitioned. On transitioned it reaches components that commit their changes when the movement ends, such as igx-chip / igx-chips-areamoveEnd, where it is originalEvent.cancelled.
For the dragEnd of a cancelled drag, originalEvent is the KeyboardEvent (Escape) or null (cancelDrag()).
To animate back instead, call transitionToOrigin() in the dragEnd handler (chips with animateOnRelease already do). For a cancelled drag with ghost = false, it animates back to the pre-drag transform.
Bug fixes:
transitionToOrigin() compared the start location's Y with baseOriginLeft instead of baseOriginTop;
transitionToOrigin() and transitionTo() now emit transitioned right away when the element is already at the target or there is no ghost to animate. Previously the drag state got stuck and the next drag didn't start correctly;
the directive no longer keeps a reference to a removed ghost after a drag ends, and an auto-scroll step scheduled before the drag ended is skipped.
No behavior change for existing apps.cancelOnEscape is off by default, so existing igxDrag usages, including igx-chip, behave as before and no migration is needed. Apps that turn it on and move or reorder items in a dragEnd or transitioned handler should check cancelled there and restore their state instead.
Components updated to handle a cancelled drag
Grid / Tree Grid group-by area: a cancelled group-by chip drag restores the chip order and keeps the grouping unchanged. The grid now clears the group expansion state only when a reorder is actually committed, so a cancelled drag, or a drag released without changing the order, keeps the user's expanded and collapsed groups.
Grid row drag and column moving keep their own Escape handling (cancelOnEscape = false). cancelDrag() on them now ends the grid state cleanly without dropping:
row drag emits rowDragEnd and resets rowDragging/dragRowID, including when cancelled from a rowDragStart handler;
column moving clears the column in drag;
both skip their ghost customization when no ghost was created.
Demos (src/app/drag-drop)
Playground: added a "Cancel upon Esc" switch to the "No ghost dragging" and "Ghost dragging - transition to origin" examples.
"Wishlist reorder with transitions": turns on cancelOnEscape and shows how to restore the items moved during the drag when it is cancelled.
Changes after review
cancelOnEscape is now opt-in (default false) instead of changing the behavior of every existing igxDrag.
The group-by area keeps the group expansion state when a reorder is cancelled.
cancelDrag() is safe to call from every handler that runs during a drag (see above), ignores a pending click, and reports the current pointer position.
The full pre-drag inline transform is restored on cancel, also when animating back.
transitionToOrigin()/transitionTo() finish when there is nothing to animate.
Grid row drag and column moving end cleanly on cancelDrag().
The Query Builder integration is moved to a follow-up (see below).
A ghostDestroy handler that sets cancel = true still keeps the ghost on cancel, the same as on a normal release.
Motivation / Context
Users couldn't back out of an accidental drag, and components built on igxDrag had no way to cancel one. See #17735.
Breaking change (fix or feature that would cause existing functionality to not work as expected)
Refactoring (no functional changes)
Documentation
Demos
CI/CD
Tests
Changelog
Skills/Agents
Component(s) / Area(s) Affected:
Drag and Drop (igxDrag), Chip / Chips Area, Grid and Tree Grid (group-by area, row drag, column moving), drag-drop demo
How Has This Been Tested?
Unit tests
Manual testing
Automated e2e tests
drag-drop.spec.ts:
cancelDrag() over a drop area: leave only, no drop, dragEnd/transitioned with cancelled, ghost removed, later move/release ignored;
no-op when idle or while the pointer is pressed but the drag hasn't started (dragClick still emitted); a drag after a cancelled one doesn't report cancelled;
Escape doesn't cancel by default; with cancelOnEscape = true, Escape/Esc on body cancels; with false it doesn't;
Escape is consumed only during a drag, and the listener is removed after drop, cancel and destroy;
cancelling from dragStart, ghostCreate (also during ghostTemplate creation), dragMove, enter and leave handlers: matching dragEnd/transitioned, current pointer coordinates, no orphan ghost, no further movement or drop-area events, no crash;
ghostCreate cancel combined with a dragEnd return animation, also when ghost creation itself is cancelled;
pointer capture is released from the base element when ghost = false, and no ghost reference is kept after a drag ends;
ghost = false returns to its pre-drag position and restores any inline transform (e.g. translate(...) rotate(...)), also when animating back;
transitionToOrigin()/transitionTo() finish when the element is already at the target or there is no ghost;
a container scroll step scheduled before the cancel doesn't scroll or move the element;
the baseOriginTop fix.
Grid group-by: a cancelled chip drag keeps the grouping and the expansion state; a drag released without reordering keeps the expansion state.
Tree grid: handleMoveEnd restores the chips order on cancel.
Grid row drag / column moving: cancelDrag() during the drag and from rowDragStart ends the grid state cleanly; cancelling in ghostCreate doesn't throw; a cancelled rowDragStart doesn't keep the ghost reference.
The new tests fail without their fixes. The existing column-moving and row-drag Escape specs pass.
test:lib:others (3229 passing); grid.groupby, column-moving, row-drag.directive and grid-filtering-advanced specs (269 passing); tree-grid-grouping (23 passing). lint:lib has no errors.
Test Configuration:
Angular version: 22.2.0
Browser(s): Chrome
OS: Windows 11
Screenshots / Recordings
Checklist:
All relevant tags have been applied to this PR
This PR includes unit tests covering all the new code (test guidelines)
This PR includes API docs for newly added methods/properties (api docs guidelines)
This PR includes feature/README.MD updates for the feature docs
This PR includes general feature table updates in the root README.MD
This PR includes CHANGELOG.MD updates for newly added functionality
This PR contains breaking changes
This PR includes ng update migrations for the breaking changes (migrations guidelines)
This PR includes behavioral changes and the feature specification has been updated with them
Accessibility (ARIA, keyboard navigation, focus management) has been verified
This shared event-args documentation implies that every cancelled event carries the cancellation trigger, but transitioned receives null for an immediate Escape cancel and a TransitionEvent after an animated return. Qualify the new KeyboardEvent/null behavior as applying to dragEnd, otherwise consumers may incorrectly expect the Escape event from transitioned.
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #17735
Description
Adds a way to cancel an
igxDragdrag that is in progress, either with the newcancelDrag()method or, opt-in, withEscape.IgxDragDirectivecancelDrag(): voidends the drag without dropping:igxDropis dispatched; a drop area under the pointer only getsleave;dragEndis emitted withcancelled: trueand the current pointer position, and the element returns to where it was before the drag (the ghost is removed, or withghost = falsethe element gets back its full pre-drag inline transform);dragTolerance(the release still emitsdragClick);dragStart:dragEnd/transitionedare still emitted withcancelled: true, so everydragStartgets a matchingdragEnd;ghostCreate, including while aghostTemplateghost is created;dragMove;enter/leave.The drag stops right away, with no orphan ghost, no further movement and no further drop-area events;
ghost = false).@Input() cancelOnEscape = false(opt-in): whentrue,Escapecancels the drag.document, because focus is on<body>during a pointer drag. It is attached only while a drag is active, in the capture phase.Escapeis consumed (preventDefault+stopPropagation) only when it actually cancels a drag, so it doesn't also close a dialog or an overlay. Outside a drag it isn't touched.cancelled?: booleanonIDragBaseEventArgs, set for bothdragEndandtransitioned. Ontransitionedit reaches components that commit their changes when the movement ends, such asigx-chip/igx-chips-areamoveEnd, where it isoriginalEvent.cancelled.dragEndof a cancelled drag,originalEventis theKeyboardEvent(Escape) ornull(cancelDrag()).transitionToOrigin()in thedragEndhandler (chips withanimateOnReleasealready do). For a cancelled drag withghost = false, it animates back to the pre-drag transform.transitionToOrigin()compared the start location's Y withbaseOriginLeftinstead ofbaseOriginTop;transitionToOrigin()andtransitionTo()now emittransitionedright away when the element is already at the target or there is no ghost to animate. Previously the drag state got stuck and the next drag didn't start correctly;No behavior change for existing apps.
cancelOnEscapeis off by default, so existingigxDragusages, includingigx-chip, behave as before and no migration is needed. Apps that turn it on and move or reorder items in adragEndortransitionedhandler should checkcancelledthere and restore their state instead.Components updated to handle a cancelled drag
Escapehandling (cancelOnEscape = false).cancelDrag()on them now ends the grid state cleanly without dropping:rowDragEndand resetsrowDragging/dragRowID, including when cancelled from arowDragStarthandler;Demos (
src/app/drag-drop)cancelOnEscapeand shows how to restore the items moved during the drag when it is cancelled.Changes after review
cancelOnEscapeis now opt-in (defaultfalse) instead of changing the behavior of every existingigxDrag.cancelDrag()is safe to call from every handler that runs during a drag (see above), ignores a pending click, and reports the current pointer position.transitionToOrigin()/transitionTo()finish when there is nothing to animate.cancelDrag().ghostDestroyhandler that setscancel = truestill keeps the ghost on cancel, the same as on a normal release.Motivation / Context
Users couldn't back out of an accidental drag, and components built on
igxDraghad no way to cancel one. See #17735.Follow-ups, not part of this PR:
Escape(resolves theTODO cancel mouse draginquery-builder-drag.service.ts). Deferred until this PR and the Query Builder refactor [Query Builder] Refactor, bug fixing and test coverage improved #17706 are merged. Tracked in Query Builder: cancel a mouse drag of a condition with Escape #17749.Escapewhile dragging a grid column over the group-by area groups by that column. This also happens onmasterand is tracked in Grid: pressing Escape while dragging a column over the group-by area groups by that column #17745.Type of Change (check all that apply):
Component(s) / Area(s) Affected:
Drag and Drop (
igxDrag), Chip / Chips Area, Grid and Tree Grid (group-by area, row drag, column moving), drag-drop demoHow Has This Been Tested?
Unit tests
Manual testing
Automated e2e tests
drag-drop.spec.ts:cancelDrag()over a drop area:leaveonly, no drop,dragEnd/transitionedwithcancelled, ghost removed, later move/release ignored;dragClickstill emitted); a drag after a cancelled one doesn't reportcancelled;Escapedoesn't cancel by default; withcancelOnEscape = true,Escape/Esconbodycancels; withfalseit doesn't;Escapeis consumed only during a drag, and the listener is removed after drop, cancel and destroy;dragStart,ghostCreate(also duringghostTemplatecreation),dragMove,enterandleavehandlers: matchingdragEnd/transitioned, current pointer coordinates, no orphan ghost, no further movement or drop-area events, no crash;ghostCreatecancel combined with adragEndreturn animation, also when ghost creation itself is cancelled;ghost = false, and no ghost reference is kept after a drag ends;ghost = falsereturns to its pre-drag position and restores any inline transform (e.g.translate(...) rotate(...)), also when animating back;transitionToOrigin()/transitionTo()finish when the element is already at the target or there is no ghost;baseOriginTopfix.Grid group-by: a cancelled chip drag keeps the grouping and the expansion state; a drag released without reordering keeps the expansion state.
Tree grid:
handleMoveEndrestores the chips order on cancel.Grid row drag / column moving:
cancelDrag()during the drag and fromrowDragStartends the grid state cleanly; cancelling inghostCreatedoesn't throw; a cancelledrowDragStartdoesn't keep the ghost reference.The new tests fail without their fixes. The existing column-moving and row-drag
Escapespecs pass.test:lib:others(3229 passing);grid.groupby,column-moving,row-drag.directiveandgrid-filtering-advancedspecs (269 passing);tree-grid-grouping(23 passing).lint:libhas no errors.Test Configuration:
Screenshots / Recordings
Checklist:
feature/README.MDupdates for the feature docsREADME.MDCHANGELOG.MDupdates for newly added functionalityng updatemigrations for the breaking changes (migrations guidelines)