Skip to content

feat: drag and resize lifecycle callbacks - #49

Merged
gfazioli merged 1 commit into
masterfrom
feat-drag-resize-lifecycle
Sep 1, 2026
Merged

feat: drag and resize lifecycle callbacks#49
gfazioli merged 1 commit into
masterfrom
feat-drag-resize-lifecycle

Conversation

@gfazioli

@gfazioli gfazioli commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Closes #47.

Adds onDragStart, onDragEnd, onResizeStart and onResizeEnd, completing the set that Mantine core FloatingWindow exposes — start, change and end for both gestures. Window had only the two change callbacks, and those fire on every frame of a gesture, so persisting a layout meant debouncing by hand and there was no moment to pause an expensive child, show a snap guide, or record a single undo entry per gesture.

Interaction start change end
Drag onDragStart (new) onPositionChange onDragEnd (new)
Resize onResizeStart (new) onSizeChange onResizeEnd (new)

Why this is additive, checked rather than assumed

Four optional props, no default or behaviour change → minor.

onDragStart/onDragEnd are real React DOM event names, so shadowing was the thing to check. WindowProps extends BoxProps, WindowBaseProps, StylesApiPropsnot ElementProps<div> — so those names were never in the public type and no existing usage changes meaning. They are destructured out of the props spread, which is what keeps them off the root element (leaking them would have wired the native HTML5 drag events).

Two things the implementation had to get right

No cross-talk. The global mouseup/touchend listener calls handleDragEnd() and handleResizeEnd() whenever either gesture is active. A naive end callback would therefore emit an unpaired onDragEnd during a plain resize. Each hook now checks its own in-flight ref before firing.

I confirmed the test for this actually tests it: removing the guard makes it fail with Expected number of calls: 0 / Received number of calls: 1. Restored, it passes.

No start without a real gesture. onDragStart is emitted only past the existing bail-outs — a press on a resize handle, on an interactive child (input, button, a[href], …) or on a data-no-window-drag region does not open a gesture that would never close. Unmounting mid-gesture fires the matching end, so a consumer that paused something on start is never left paused.

There is no keyboard drag or resize in this component, so core's caveat about its resize callbacks not firing for keyboard resize has no equivalent here. Stated in the docs so the difference is deliberate rather than silent.

Verified with real pointer events, not only jsdom

Driven in Chrome with Input.dispatchMouseEvent:

Moment State Writes during Writes at end
Before idle 0 0
Press on header dragging 0 0
Mid-drag (8 moves) dragging 8 0
Release idle 8 1
Press on resize handle resizing 8 1 (no spurious end)
Release idle 20 2

One drag and one resize: 20 continuous calls against 2 lifecycle calls, every start paired with exactly one end.

What is in the diff

  • 4 props on Window, threaded through useMantineWindow into the drag and resize hooks, held in refs so an inline arrow from the consumer does not re-create the memoized pointer handlers
  • 6 new tests: order and exactly-once for each gesture, no cross-talk in either direction, no onDragStart on an interactive child, end fired on unmount mid-gesture, and nothing fired on a clean unmount
  • New demo Drag and Resize Lifecycle plus a docs.mdx section; the demo counts the two kinds of write side by side, which makes the point faster than prose

Test plan

  • yarn test — 160 passing
  • yarn build, yarn docgen (the 4 props appear in the Props table), yarn docs:build with docs/.next cleared
  • Real drag and resize in Chrome, numbers above

Summary by CodeRabbit

  • New Features

    • Added drag and resize lifecycle callbacks: onDragStart, onDragEnd, onResizeStart, and onResizeEnd.
    • Callbacks fire once per valid gesture and remain correctly paired when gestures are interrupted.
    • Added documentation and an interactive demo illustrating lifecycle and continuous change callbacks.
  • Bug Fixes

    • Prevented drag callbacks from firing during resizing or interactions with excluded elements.

Closes #47.

Adds onDragStart, onDragEnd, onResizeStart and onResizeEnd, completing the set
that Mantine core FloatingWindow exposes: start, change and end for both
gestures. Window had only the two change callbacks, and those fire on every
frame — so persisting a layout meant debouncing by hand, and there was no moment
to pause an expensive child, show a snap guide, or record one undo entry per
gesture.

Additive: four optional props, no default or behaviour change. WindowProps
extends BoxProps and WindowBaseProps but NOT ElementProps<'div'>, so the DOM
event names onDragStart/onDragEnd were never part of the public type and nothing
is being shadowed. Both are destructured out of the props spread, which is what
keeps them off the root element.

Two things the implementation had to get right:

- The global mouseup/touchend listener calls handleDragEnd() AND
  handleResizeEnd() whenever either gesture is active, so a naive end callback
  would emit an unpaired onDragEnd during a plain resize. Each hook now checks
  its own in-flight ref before firing. A regression test covers it, and removing
  the guard makes that test fail with "Expected 0, Received 1".
- onDragStart is emitted only past the existing bail-outs: a press on a resize
  handle, on an interactive child, or on a data-no-window-drag region does not
  open a gesture that would never close.

Unmounting mid-gesture fires the matching end, so a consumer that paused
something on start is never left paused.

There is no keyboard drag or resize in this component, so core's caveat about
its resize callbacks not firing for keyboard resize has no equivalent here.

Verified with real pointer events in Chrome, not only jsdom: across one drag and
one resize the continuous callbacks fired 20 times and the lifecycle ones twice,
with state going idle → dragging → idle → resizing → idle and no unpaired end.
@gfazioli
gfazioli merged commit 7214e45 into master Sep 1, 2026
1 of 2 checks passed
@gfazioli
gfazioli deleted the feat-drag-resize-lifecycle branch September 1, 2026 10:34
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 5473979a-18c9-437a-8508-e9eda5b9707d

📥 Commits

Reviewing files that changed from the base of the PR and between a485899 and 178ed68.

📒 Files selected for processing (8)
  • docs/demos/Window.demo.dragResizeLifecycle.tsx
  • docs/demos/index.ts
  • docs/docs.mdx
  • package/src/Window.test.tsx
  • package/src/Window.tsx
  • package/src/hooks/use-mantine-window.ts
  • package/src/hooks/use-window-drag.ts
  • package/src/hooks/use-window-resize.ts

📝 Walkthrough

Walkthrough

The Window component now supports drag and resize lifecycle callbacks. Hooks emit paired callbacks for active gestures, including interrupted drags during unmount. Tests, documentation, and a lifecycle demo cover the new behavior.

Changes

Gesture lifecycle callbacks

Layer / File(s) Summary
Callback contract and wiring
package/src/Window.tsx, package/src/hooks/use-mantine-window.ts, package/src/hooks/use-window-drag.ts, package/src/hooks/use-window-resize.ts
Adds four optional lifecycle callbacks and forwards them from Window to the drag and resize hooks.
Gesture emission and cleanup
package/src/hooks/use-window-drag.ts, package/src/hooks/use-window-resize.ts, package/src/hooks/use-mantine-window.ts
Emits start callbacks only for valid gestures, emits end callbacks only for active gestures, and ends active gestures during unmount cleanup.
Validation and documentation
package/src/Window.test.tsx, docs/docs.mdx, docs/demos/Window.demo.dragResizeLifecycle.tsx, docs/demos/index.ts
Tests callback ordering, gesture isolation, interactive-child suppression, and unmount behavior. Adds documentation and a demo comparing continuous and lifecycle callbacks.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Window
  participant useMantineWindow
  participant useWindowDrag
  participant useWindowResize
  participant Consumer
  Window->>useMantineWindow: Pass lifecycle callback props
  useMantineWindow->>useWindowDrag: Configure drag callbacks
  useMantineWindow->>useWindowResize: Configure resize callbacks
  useWindowDrag->>Consumer: Emit onDragStart and onDragEnd
  useWindowResize->>Consumer: Emit onResizeStart and onResizeEnd
  useMantineWindow->>useWindowDrag: End active drag on unmount
  useMantineWindow->>useWindowResize: End active resize on unmount
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-drag-resize-lifecycle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add drag/resize lifecycle callbacks (onDragStart, onDragEnd, onResizeStart, onResizeEnd) to match core FloatingWindow 9.6

1 participant