Skip to content

feat(drag-drop): cancel an in-progress drag with Escape or cancelDrag() - #17744

Open
ivanvpetrov wants to merge 16 commits into
masterfrom
ipetrov/drag-cancel-upon-escape
Open

ivanvpetrov wants to merge 16 commits into
masterfrom
ipetrov/drag-cancel-upon-escape

Conversation

@ivanvpetrov

@ivanvpetrov ivanvpetrov commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #17735

Description

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-area moveEnd, 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.

Follow-ups, not part of this PR:

Type of Change (check all that apply):

  • Bug fix
  • New functionality
  • 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

image

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 comment was marked as outdated.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

@ivanvpetrov
ivanvpetrov requested a balanced review from Copilot October 8, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes re-entrant drag lifecycle, pointer capture, animation, and multiple grid integrations that warrant final human validation.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

@ivanvpetrov
ivanvpetrov marked this pull request as ready for review October 8, 2026 13:49
@ivanvpetrov
ivanvpetrov requested a review from skrustev October 8, 2026 13:50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drag and Drop: allow cancelling an in-progress drag (Escape key / public API)

2 participants