From ca6359f7e618d91e679908c69f4c1b2e30fd7589 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:00:08 +0200 Subject: [PATCH 01/21] feat(DraggableCore): Added mobileHoldDelay --- README.md | 155 +++---- lib/DraggableCore.js | 957 ++++++++++++++++++++++++------------------- typings/index.d.ts | 126 +++--- 3 files changed, 680 insertions(+), 558 deletions(-) diff --git a/README.md b/README.md index cdffebc0..4ec529f7 100644 --- a/README.md +++ b/README.md @@ -14,36 +14,33 @@ A simple component for making elements draggable. ```js -
I can now be moved around!
+
I can now be moved around!
``` -- [Demo](http://react-grid-layout.github.io/react-draggable/example/) -- [Changelog](CHANGELOG.md) +- [Demo](http://react-grid-layout.github.io/react-draggable/example/) +- [Changelog](CHANGELOG.md) +| Version | Compatibility | +| ------- | ----------------- | +| 4.x | React 16.3+ | +| 3.x | React 15-16 | +| 2.x | React 0.14 - 15 | +| 1.x | React 0.13 - 0.14 | +| 0.x | React 0.10 - 0.13 | -|Version | Compatibility| -|------------|--------------| -|4.x | React 16.3+ | -|3.x | React 15-16 | -|2.x | React 0.14 - 15 | -|1.x | React 0.13 - 0.14 | -|0.x | React 0.10 - 0.13 | - ------- +--- #### Technical Documentation -- [Installing](#installing) -- [Exports](#exports) -- [Draggable](#draggable) -- [Draggable Usage](#draggable-usage) -- [Draggable API](#draggable-api) -- [Controlled vs. Uncontrolled](#controlled-vs-uncontrolled) -- [DraggableCore](#draggablecore) -- [DraggableCore API](#draggablecore-api) - - +- [Installing](#installing) +- [Exports](#exports) +- [Draggable](#draggable) +- [Draggable Usage](#draggable-usage) +- [Draggable API](#draggable-api) +- [Controlled vs. Uncontrolled](#controlled-vs-uncontrolled) +- [DraggableCore](#draggablecore) +- [DraggableCore API](#draggablecore-api) ### Installing @@ -65,13 +62,13 @@ Here's how to use it: ```js // ES6 -import Draggable from 'react-draggable'; // The default -import {DraggableCore} from 'react-draggable'; // -import Draggable, {DraggableCore} from 'react-draggable'; // Both at the same time +import Draggable from 'react-draggable' // The default +import { DraggableCore } from 'react-draggable' // +import Draggable, { DraggableCore } from 'react-draggable' // Both at the same time // CommonJS -let Draggable = require('react-draggable'); -let DraggableCore = Draggable.DraggableCore; +let Draggable = require('react-draggable') +let DraggableCore = Draggable.DraggableCore ``` ## `` @@ -91,39 +88,39 @@ View the [Demo](http://react-grid-layout.github.io/react-draggable/example/) and [source](/example/example.js) for more. ```js -import React from 'react'; -import ReactDOM from 'react-dom'; -import Draggable from 'react-draggable'; +import React from 'react' +import ReactDOM from 'react-dom' +import Draggable from 'react-draggable' class App extends React.Component { - - eventLogger = (e: MouseEvent, data: Object) => { - console.log('Event: ', e); - console.log('Data: ', data); - }; - - render() { - return ( - -
-
Drag from here
-
This readme is really dragging on...
-
-
- ); - } + eventLogger = (e: MouseEvent, data: Object) => { + console.log('Event: ', e) + console.log('Data: ', data) + } + + render() { + return ( + +
+
Drag from here
+
This readme is really dragging on...
+
+
+ ) + } } -ReactDOM.render(, document.body); +ReactDOM.render(, document.body) ``` ### Draggable API @@ -134,9 +131,10 @@ The `` component transparently adds draggability to its children. For the `` component to correctly attach itself to its child, the child element must provide support for the following props: -- `style` is used to give the transform css to the child. -- `className` is used to apply the proper classes to the object being dragged. -- `onMouseDown`, `onMouseUp`, `onTouchStart`, and `onTouchEnd` are used to keep track of dragging state. + +- `style` is used to give the transform css to the child. +- `className` is used to apply the proper classes to the object being dragged. +- `onMouseDown`, `onMouseUp`, `onTouchStart`, and `onTouchEnd` are used to keep track of dragging state. React.DOM elements support the above properties by default, so you may use those elements as children without any changes. If you wish to use a React component you created, you'll need to be sure to @@ -168,7 +166,7 @@ allowAnyClick: boolean, // If set to `true`, the 'touchstart' event will not be prevented, // which will allow scrolling inside containers. We recommend // using the 'handle' / 'cancel' props when possible instead of enabling this. -// +// // See https://github.com/react-grid-layout/react-draggable/issues/728 allowMobileScroll: boolean, @@ -257,7 +255,7 @@ onStop: DraggableEventHandler, // pointing to the actual child DOM node and not a custom component. // // For rich components, you need to both forward the ref *and props* to the underlying DOM -// element. Props must be forwarded so that DOM event handlers can be attached. +// element. Props must be forwarded so that DOM event handlers can be attached. // For example: // // const Component1 = React.forwardRef(function (props, ref) { @@ -288,15 +286,17 @@ positionOffset: {x: number | string, y: number | string}, // Specifies the scale of the canvas your are dragging this element on. This allows // you to, for example, get the correct drag deltas while you are zoomed in or out via // a transform or matrix in the parent of this element. -scale: number +scale: number, + +// If true, will add a delay to the start of the drag on mobile devices. +// This is useful to prevent accidental drags when a user is trying to scroll. +mobileDragDelay: number, } ``` - Note that sending `className`, `style`, or `transform` as properties will error - set them on the child element directly. - ## Controlled vs. Uncontrolled `` is a 'batteries-included' component that manages its own state. If you want to completely @@ -346,7 +346,8 @@ on itself and thus must have callbacks attached to be useful. onDrag: DraggableEventHandler, onStop: DraggableEventHandler, onMouseDown: (e: MouseEvent) => void, - scale: number + scale: number, + mobileDragDelay: number, } ``` @@ -356,24 +357,24 @@ to set actual positions on ``. Drag callbacks (`onStart`, `onDrag`, `onStop`) are called with the [same arguments as ``](#draggable-api). ----- +--- ### Contributing -- Fork the project -- Run the project in development mode: `$ npm run dev` -- Make changes. -- Add appropriate tests -- `$ npm test` -- If tests don't pass, make them pass. -- Update README with appropriate docs. -- Commit and PR +- Fork the project +- Run the project in development mode: `$ npm run dev` +- Make changes. +- Add appropriate tests +- `$ npm test` +- If tests don't pass, make them pass. +- Update README with appropriate docs. +- Commit and PR ### Release checklist -- Update CHANGELOG -- `make release-patch`, `make release-minor`, or `make-release-major` -- `make publish` +- Update CHANGELOG +- `make release-patch`, `make release-minor`, or `make-release-major` +- `make publish` ### License diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index e673e024..ebbe7b8a 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -2,64 +2,84 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; -import {matchesSelectorAndParentsTo, addEvent, removeEvent, addUserSelectStyles, getTouchIdentifier, - scheduleRemoveUserSelectStyles} from './utils/domFns'; -import {createCoreData, getControlPosition, snapToGrid} from './utils/positionFns'; -import {dontSetMe} from './utils/shims'; +import { + matchesSelectorAndParentsTo, + addEvent, + removeEvent, + addUserSelectStyles, + getTouchIdentifier, + scheduleRemoveUserSelectStyles, +} from './utils/domFns'; +import { + createCoreData, + getControlPosition, + snapToGrid, +} from './utils/positionFns'; +import { dontSetMe } from './utils/shims'; import log from './utils/log'; -import type {EventHandler, MouseTouchEvent} from './utils/types'; -import type {Element as ReactElement} from 'react'; +import type { EventHandler, MouseTouchEvent } from './utils/types'; +import type { Element as ReactElement } from 'react'; // Simple abstraction for dragging events names. const eventsFor = { - touch: { - start: 'touchstart', - move: 'touchmove', - stop: 'touchend' - }, - mouse: { - start: 'mousedown', - move: 'mousemove', - stop: 'mouseup' - } + touch: { + start: 'touchstart', + move: 'touchmove', + stop: 'touchend', + }, + mouse: { + start: 'mousedown', + move: 'mousemove', + stop: 'mouseup', + }, }; // Default to mouse events. let dragEventFor = eventsFor.mouse; export type DraggableData = { - node: HTMLElement, - x: number, y: number, - deltaX: number, deltaY: number, - lastX: number, lastY: number, + node: HTMLElement, + x: number, + y: number, + deltaX: number, + deltaY: number, + lastX: number, + lastY: number, }; -export type DraggableEventHandler = (e: MouseEvent, data: DraggableData) => void | false; +export type DraggableEventHandler = ( + e: MouseEvent, + data: DraggableData +) => void | false; -export type ControlPosition = {x: number, y: number}; -export type PositionOffsetControlPosition = {x: number|string, y: number|string}; +export type ControlPosition = { x: number, y: number }; +export type PositionOffsetControlPosition = { + x: number | string, + y: number | string, +}; export type DraggableCoreDefaultProps = { - allowAnyClick: boolean, - allowMobileScroll: boolean, - disabled: boolean, - enableUserSelectHack: boolean, - onStart: DraggableEventHandler, - onDrag: DraggableEventHandler, - onStop: DraggableEventHandler, - onMouseDown: (e: MouseEvent) => void, - scale: number, + allowAnyClick: boolean, + allowMobileScroll: boolean, + disabled: boolean, + enableUserSelectHack: boolean, + onStart: DraggableEventHandler, + onDrag: DraggableEventHandler, + onStop: DraggableEventHandler, + onMouseDown: (e: MouseEvent) => void, + scale: number, + mobileDragDelay?: number, }; export type DraggableCoreProps = { - ...DraggableCoreDefaultProps, - cancel: string, - children: ReactElement, - offsetParent: HTMLElement, - grid: [number, number], - handle: string, - nodeRef?: ?React.ElementRef, + ...DraggableCoreDefaultProps, + cancel: string, + children: ReactElement, + offsetParent: HTMLElement, + grid: [number, number], + handle: string, + nodeRef?: ?React.ElementRef, }; // @@ -70,397 +90,484 @@ export type DraggableCoreProps = { // export default class DraggableCore extends React.Component { - - static displayName: ?string = 'DraggableCore'; - - static propTypes: Object = { - /** - * `allowAnyClick` allows dragging using any mouse button. - * By default, we only accept the left button. - * - * Defaults to `false`. - */ - allowAnyClick: PropTypes.bool, - - /** - * `allowMobileScroll` turns off cancellation of the 'touchstart' event - * on mobile devices. Only enable this if you are having trouble with click - * events. Prefer using 'handle' / 'cancel' instead. - * - * Defaults to `false`. - */ - allowMobileScroll: PropTypes.bool, - - children: PropTypes.node.isRequired, - - /** - * `disabled`, if true, stops the from dragging. All handlers, - * with the exception of `onMouseDown`, will not fire. - */ - disabled: PropTypes.bool, - - /** - * By default, we add 'user-select:none' attributes to the document body - * to prevent ugly text selection during drag. If this is causing problems - * for your app, set this to `false`. - */ - enableUserSelectHack: PropTypes.bool, - - /** - * `offsetParent`, if set, uses the passed DOM node to compute drag offsets - * instead of using the parent node. - */ - offsetParent: function(props: DraggableCoreProps, propName: $Keys) { - if (props[propName] && props[propName].nodeType !== 1) { - throw new Error('Draggable\'s offsetParent must be a DOM Node.'); - } - }, - - /** - * `grid` specifies the x and y that dragging should snap to. - */ - grid: PropTypes.arrayOf(PropTypes.number), - - /** - * `handle` specifies a selector to be used as the handle that initiates drag. - * - * Example: - * - * ```jsx - * let App = React.createClass({ - * render: function () { - * return ( - * - *
- *
Click me to drag
- *
This is some other content
- *
- *
- * ); - * } - * }); - * ``` - */ - handle: PropTypes.string, - - /** - * `cancel` specifies a selector to be used to prevent drag initialization. - * - * Example: - * - * ```jsx - * let App = React.createClass({ - * render: function () { - * return( - * - *
- *
You can't drag from here
- *
Dragging here works fine
- *
- *
- * ); - * } - * }); - * ``` - */ - cancel: PropTypes.string, - - /* If running in React Strict mode, ReactDOM.findDOMNode() is deprecated. - * Unfortunately, in order for to work properly, we need raw access - * to the underlying DOM node. If you want to avoid the warning, pass a `nodeRef` - * as in this example: - * - * function MyComponent() { - * const nodeRef = React.useRef(null); - * return ( - * - *
Example Target
- *
- * ); - * } - * - * This can be used for arbitrarily nested components, so long as the ref ends up - * pointing to the actual child DOM node and not a custom component. - */ - nodeRef: PropTypes.object, - - /** - * Called when dragging starts. - * If this function returns the boolean false, dragging will be canceled. - */ - onStart: PropTypes.func, - - /** - * Called while dragging. - * If this function returns the boolean false, dragging will be canceled. - */ - onDrag: PropTypes.func, - - /** - * Called when dragging stops. - * If this function returns the boolean false, the drag will remain active. - */ - onStop: PropTypes.func, - - /** - * A workaround option which can be passed if onMouseDown needs to be accessed, - * since it'll always be blocked (as there is internal use of onMouseDown) - */ - onMouseDown: PropTypes.func, - - /** - * `scale`, if set, applies scaling while dragging an element - */ - scale: PropTypes.number, - - /** - * These properties should be defined on the child, not here. - */ - className: dontSetMe, - style: dontSetMe, - transform: dontSetMe - }; - - static defaultProps: DraggableCoreDefaultProps = { - allowAnyClick: false, // by default only accept left click - allowMobileScroll: false, - disabled: false, - enableUserSelectHack: true, - onStart: function(){}, - onDrag: function(){}, - onStop: function(){}, - onMouseDown: function(){}, - scale: 1, - }; - - dragging: boolean = false; - - // Used while dragging to determine deltas. - lastX: number = NaN; - lastY: number = NaN; - - touchIdentifier: ?number = null; - - mounted: boolean = false; - - componentDidMount() { - this.mounted = true; - // Touch handlers must be added with {passive: false} to be cancelable. - // https://developers.google.com/web/updates/2017/01/scrolling-intervention - const thisNode = this.findDOMNode(); - if (thisNode) { - addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {passive: false}); - } - } - - componentWillUnmount() { - this.mounted = false; - // Remove any leftover event handlers. Remove both touch and mouse handlers in case - // some browser quirk caused a touch event to fire during a mouse move, or vice versa. - const thisNode = this.findDOMNode(); - if (thisNode) { - const {ownerDocument} = thisNode; - removeEvent(ownerDocument, eventsFor.mouse.move, this.handleDrag); - removeEvent(ownerDocument, eventsFor.touch.move, this.handleDrag); - removeEvent(ownerDocument, eventsFor.mouse.stop, this.handleDragStop); - removeEvent(ownerDocument, eventsFor.touch.stop, this.handleDragStop); - removeEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {passive: false}); - if (this.props.enableUserSelectHack) scheduleRemoveUserSelectStyles(ownerDocument); - } - } - - // React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find - // the underlying DOM node ourselves. See the README for more information. - findDOMNode(): ?HTMLElement { - return this.props?.nodeRef ? this.props?.nodeRef?.current : ReactDOM.findDOMNode(this); - } - - handleDragStart: EventHandler = (e) => { - // Make it possible to attach event handlers on top of this one. - this.props.onMouseDown(e); - - // Only accept left-clicks. - if (!this.props.allowAnyClick && typeof e.button === 'number' && e.button !== 0) return false; - - // Get nodes. Be sure to grab relative document (could be iframed) - const thisNode = this.findDOMNode(); - if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) { - throw new Error(' not mounted on DragStart!'); + static displayName: ?string = 'DraggableCore'; + + static propTypes: Object = { + /** + * `allowAnyClick` allows dragging using any mouse button. + * By default, we only accept the left button. + * + * Defaults to `false`. + */ + allowAnyClick: PropTypes.bool, + + /** + * `allowMobileScroll` turns off cancellation of the 'touchstart' event + * on mobile devices. Only enable this if you are having trouble with click + * events. Prefer using 'handle' / 'cancel' instead. + * + * Defaults to `false`. + */ + allowMobileScroll: PropTypes.bool, + + children: PropTypes.node.isRequired, + + /** + * `disabled`, if true, stops the from dragging. All handlers, + * with the exception of `onMouseDown`, will not fire. + */ + disabled: PropTypes.bool, + + /** + * By default, we add 'user-select:none' attributes to the document body + * to prevent ugly text selection during drag. If this is causing problems + * for your app, set this to `false`. + */ + enableUserSelectHack: PropTypes.bool, + + /** + * `offsetParent`, if set, uses the passed DOM node to compute drag offsets + * instead of using the parent node. + */ + offsetParent: function ( + props: DraggableCoreProps, + propName: $Keys + ) { + if (props[propName] && props[propName].nodeType !== 1) { + throw new Error("Draggable's offsetParent must be a DOM Node."); + } + }, + + /** + * `grid` specifies the x and y that dragging should snap to. + */ + grid: PropTypes.arrayOf(PropTypes.number), + + /** + * `handle` specifies a selector to be used as the handle that initiates drag. + * + * Example: + * + * ```jsx + * let App = React.createClass({ + * render: function () { + * return ( + * + *
+ *
Click me to drag
+ *
This is some other content
+ *
+ *
+ * ); + * } + * }); + * ``` + */ + handle: PropTypes.string, + + /** + * `cancel` specifies a selector to be used to prevent drag initialization. + * + * Example: + * + * ```jsx + * let App = React.createClass({ + * render: function () { + * return( + * + *
+ *
You can't drag from here
+ *
Dragging here works fine
+ *
+ *
+ * ); + * } + * }); + * ``` + */ + cancel: PropTypes.string, + + /* If running in React Strict mode, ReactDOM.findDOMNode() is deprecated. + * Unfortunately, in order for to work properly, we need raw access + * to the underlying DOM node. If you want to avoid the warning, pass a `nodeRef` + * as in this example: + * + * function MyComponent() { + * const nodeRef = React.useRef(null); + * return ( + * + *
Example Target
+ *
+ * ); + * } + * + * This can be used for arbitrarily nested components, so long as the ref ends up + * pointing to the actual child DOM node and not a custom component. + */ + nodeRef: PropTypes.object, + + /** + * Called when dragging starts. + * If this function returns the boolean false, dragging will be canceled. + */ + onStart: PropTypes.func, + + /** + * Called while dragging. + * If this function returns the boolean false, dragging will be canceled. + */ + onDrag: PropTypes.func, + + /** + * Called when dragging stops. + * If this function returns the boolean false, the drag will remain active. + */ + onStop: PropTypes.func, + + /** + * A workaround option which can be passed if onMouseDown needs to be accessed, + * since it'll always be blocked (as there is internal use of onMouseDown) + */ + onMouseDown: PropTypes.func, + + /** + * `scale`, if set, applies scaling while dragging an element + */ + scale: PropTypes.number, + + /** + * Delay on mobile devices before the drag starts. + * This is useful to prevent accidental drags when scrolling on touch devices. + */ + mobileDragDelay: PropTypes.number, + + /** + * These properties should be defined on the child, not here. + */ + className: dontSetMe, + style: dontSetMe, + transform: dontSetMe, + }; + + static defaultProps: DraggableCoreDefaultProps = { + allowAnyClick: false, // by default only accept left click + allowMobileScroll: false, + disabled: false, + enableUserSelectHack: true, + onStart: function () {}, + onDrag: function () {}, + onStop: function () {}, + onMouseDown: function () {}, + scale: 1, + mobileDragDelay: 0, + }; + + dragging: boolean = false; + + dragTimeout: TimeoutID | null = null; + + // Used while dragging to determine deltas. + lastX: number = NaN; + lastY: number = NaN; + + touchIdentifier: ?number = null; + + mounted: boolean = false; + + componentDidMount() { + this.mounted = true; + // Touch handlers must be added with {passive: false} to be cancelable. + // https://developers.google.com/web/updates/2017/01/scrolling-intervention + const thisNode = this.findDOMNode(); + if (thisNode) { + addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, { + passive: false, + }); + } } - const {ownerDocument} = thisNode; - - // Short circuit if handle or cancel prop was provided and selector doesn't match. - if (this.props.disabled || - (!(e.target instanceof ownerDocument.defaultView.Node)) || - (this.props.handle && !matchesSelectorAndParentsTo(e.target, this.props.handle, thisNode)) || - (this.props.cancel && matchesSelectorAndParentsTo(e.target, this.props.cancel, thisNode))) { - return; - } - - // Prevent scrolling on mobile devices, like ipad/iphone. - // Important that this is after handle/cancel. - if (e.type === 'touchstart' && !this.props.allowMobileScroll) e.preventDefault(); - - // Set touch identifier in component state if this is a touch event. This allows us to - // distinguish between individual touches on multitouch screens by identifying which - // touchpoint was set to this element. - const touchIdentifier = getTouchIdentifier(e); - this.touchIdentifier = touchIdentifier; - - // Get the current drag point from the event. This is used as the offset. - const position = getControlPosition(e, touchIdentifier, this); - if (position == null) return; // not possible but satisfies flow - const {x, y} = position; - - // Create an event object with all the data parents need to make a decision here. - const coreEvent = createCoreData(this, x, y); - - log('DraggableCore: handleDragStart: %j', coreEvent); - - // Call event handler. If it returns explicit false, cancel. - log('calling', this.props.onStart); - const shouldUpdate = this.props.onStart(e, coreEvent); - if (shouldUpdate === false || this.mounted === false) return; - - // Add a style to the body to disable user-select. This prevents text from - // being selected all over the page. - if (this.props.enableUserSelectHack) addUserSelectStyles(ownerDocument); - - // Initiate dragging. Set the current x and y as offsets - // so we know how much we've moved during the drag. This allows us - // to drag elements around even if they have been moved, without issue. - this.dragging = true; - this.lastX = x; - this.lastY = y; - - // Add events to the document directly so we catch when the user's mouse/touch moves outside of - // this element. We use different events depending on whether or not we have detected that this - // is a touch-capable device. - addEvent(ownerDocument, dragEventFor.move, this.handleDrag); - addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop); - }; - - handleDrag: EventHandler = (e) => { - - // Get the current drag point from the event. This is used as the offset. - const position = getControlPosition(e, this.touchIdentifier, this); - if (position == null) return; - let {x, y} = position; - - // Snap to grid if prop has been provided - if (Array.isArray(this.props.grid)) { - let deltaX = x - this.lastX, deltaY = y - this.lastY; - [deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY); - if (!deltaX && !deltaY) return; // skip useless drag - x = this.lastX + deltaX, y = this.lastY + deltaY; - } - - const coreEvent = createCoreData(this, x, y); - - log('DraggableCore: handleDrag: %j', coreEvent); - - // Call event handler. If it returns explicit false, trigger end. - const shouldUpdate = this.props.onDrag(e, coreEvent); - if (shouldUpdate === false || this.mounted === false) { - try { - // $FlowIgnore - this.handleDragStop(new MouseEvent('mouseup')); - } catch (err) { - // Old browsers - const event = ((document.createEvent('MouseEvents'): any): MouseTouchEvent); - // I see why this insanity was deprecated - // $FlowIgnore - event.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - this.handleDragStop(event); - } - return; - } - - this.lastX = x; - this.lastY = y; - }; - handleDragStop: EventHandler = (e) => { - if (!this.dragging) return; - - const position = getControlPosition(e, this.touchIdentifier, this); - if (position == null) return; - let {x, y} = position; - - // Snap to grid if prop has been provided - if (Array.isArray(this.props.grid)) { - let deltaX = x - this.lastX || 0; - let deltaY = y - this.lastY || 0; - [deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY); - x = this.lastX + deltaX, y = this.lastY + deltaY; + componentWillUnmount() { + this.mounted = false; + // Remove any leftover event handlers. Remove both touch and mouse handlers in case + // some browser quirk caused a touch event to fire during a mouse move, or vice versa. + const thisNode = this.findDOMNode(); + if (thisNode) { + const { ownerDocument } = thisNode; + removeEvent(ownerDocument, eventsFor.mouse.move, this.handleDrag); + removeEvent(ownerDocument, eventsFor.touch.move, this.handleDrag); + removeEvent( + ownerDocument, + eventsFor.mouse.stop, + this.handleDragStop + ); + removeEvent( + ownerDocument, + eventsFor.touch.stop, + this.handleDragStop + ); + removeEvent(thisNode, eventsFor.touch.start, this.onTouchStart, { + passive: false, + }); + if (this.props.enableUserSelectHack) { + scheduleRemoveUserSelectStyles(ownerDocument); + } + } } - const coreEvent = createCoreData(this, x, y); - - // Call event handler - const shouldContinue = this.props.onStop(e, coreEvent); - if (shouldContinue === false || this.mounted === false) return false; - - const thisNode = this.findDOMNode(); - if (thisNode) { - // Remove user-select hack - if (this.props.enableUserSelectHack) scheduleRemoveUserSelectStyles(thisNode.ownerDocument); + // React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find + // the underlying DOM node ourselves. See the README for more information. + findDOMNode(): ?HTMLElement { + return this.props?.nodeRef + ? this.props?.nodeRef?.current + : ReactDOM.findDOMNode(this); } - log('DraggableCore: handleDragStop: %j', coreEvent); - - // Reset the el. - this.dragging = false; - this.lastX = NaN; - this.lastY = NaN; - - if (thisNode) { - // Remove event handlers - log('DraggableCore: Removing handlers'); - removeEvent(thisNode.ownerDocument, dragEventFor.move, this.handleDrag); - removeEvent(thisNode.ownerDocument, dragEventFor.stop, this.handleDragStop); + handleDragStart: EventHandler = (e) => { + // Make it possible to attach event handlers on top of this one. + this.props.onMouseDown(e); + + // Only accept left-clicks. + if ( + !this.props.allowAnyClick && + typeof e.button === 'number' && + e.button !== 0 + ) { + return false; + } + + // Get nodes. Be sure to grab relative document (could be iframed) + const thisNode = this.findDOMNode(); + if ( + !thisNode || + !thisNode.ownerDocument || + !thisNode.ownerDocument.body + ) { + throw new Error(' not mounted on DragStart!'); + } + const { ownerDocument } = thisNode; + + // Short circuit if handle or cancel prop was provided and selector doesn't match. + if ( + this.props.disabled || + !(e.target instanceof ownerDocument.defaultView.Node) || + (this.props.handle && + !matchesSelectorAndParentsTo( + e.target, + this.props.handle, + thisNode + )) || + (this.props.cancel && + matchesSelectorAndParentsTo( + e.target, + this.props.cancel, + thisNode + )) + ) { + return; + } + + // Prevent scrolling on mobile devices, like iPad/iPhone + if (e.type === 'touchstart') e.preventDefault(); + + // Set touch identifier + const touchIdentifier = getTouchIdentifier(e); + this.touchIdentifier = touchIdentifier; + + // Get the current drag point + const position = getControlPosition(e, touchIdentifier, this); + if (position == null) return; + const { x, y } = position; + + // Create an event object + const coreEvent = createCoreData(this, x, y); + log('DraggableCore: handleDragStart: %j', coreEvent); + + // Function to start dragging + const startDragging = () => { + log('calling', this.props.onStart); + const shouldUpdate = this.props.onStart(e, coreEvent); + if (shouldUpdate === false || this.mounted === false) return; + + // Prevent text selection while dragging + if (this.props.enableUserSelectHack) { + addUserSelectStyles(ownerDocument); + } + + this.dragging = true; + this.lastX = x; + this.lastY = y; + + // Add global move/stop listeners + addEvent(ownerDocument, dragEventFor.move, this.handleDrag); + addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop); + }; + + // If mobileDragDelay prop exists and it's a touch — delay start + if ( + this.props.mobileDragDelay && + this.props.mobileDragDelay > 0 && + e.type === 'touchstart' + ) { + clearTimeout(this.dragTimeout); + if (typeof e.persist === 'function') e.persist(); + this.dragTimeout = setTimeout( + startDragging, + this.props.mobileDragDelay + ); + } else { + // Mouse or no delay — start immediately + startDragging(); + } + }; + + handleDrag: EventHandler = (e) => { + // Get the current drag point from the event. This is used as the offset. + const position = getControlPosition(e, this.touchIdentifier, this); + if (position == null) return; + let { x, y } = position; + + // Snap to grid if prop has been provided + if (Array.isArray(this.props.grid)) { + let deltaX = x - this.lastX, + deltaY = y - this.lastY; + [deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY); + if (!deltaX && !deltaY) return; // skip useless drag + (x = this.lastX + deltaX), (y = this.lastY + deltaY); + } + + const coreEvent = createCoreData(this, x, y); + + log('DraggableCore: handleDrag: %j', coreEvent); + + // Call event handler. If it returns explicit false, trigger end. + const shouldUpdate = this.props.onDrag(e, coreEvent); + if (shouldUpdate === false || this.mounted === false) { + try { + // $FlowIgnore + this.handleDragStop(new MouseEvent('mouseup')); + } catch (err) { + // Old browsers + const event = ((document.createEvent( + 'MouseEvents' + ): any): MouseTouchEvent); + // I see why this insanity was deprecated + // $FlowIgnore + event.initMouseEvent( + 'mouseup', + true, + true, + window, + 0, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ); + this.handleDragStop(event); + } + return; + } + + this.lastX = x; + this.lastY = y; + }; + + handleDragStop: EventHandler = (e) => { + if (!this.dragging) return; + if (this.dragTimeout) { + clearTimeout(this.dragTimeout); + this.dragTimeout = null; + } + const position = getControlPosition(e, this.touchIdentifier, this); + if (position == null) return; + let { x, y } = position; + + // Snap to grid if prop has been provided + if (Array.isArray(this.props.grid)) { + let deltaX = x - this.lastX || 0; + let deltaY = y - this.lastY || 0; + [deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY); + (x = this.lastX + deltaX), (y = this.lastY + deltaY); + } + const coreEvent = createCoreData(this, x, y); + + // Call event handler + const shouldContinue = this.props.onStop(e, coreEvent); + if (shouldContinue === false || this.mounted === false) return false; + const thisNode = this.findDOMNode(); + if (thisNode) { + // Remove user-select hack + if (this.props.enableUserSelectHack) { + scheduleRemoveUserSelectStyles(thisNode.ownerDocument); + } + } + log('DraggableCore: handleDragStop: %j', coreEvent); + + // Reset the el. + this.dragging = false; + this.lastX = NaN; + this.lastY = NaN; + if (thisNode) { + // Remove event handlers + log('DraggableCore: Removing handlers'); + removeEvent( + thisNode.ownerDocument, + dragEventFor.move, + this.handleDrag + ); + removeEvent( + thisNode.ownerDocument, + dragEventFor.stop, + this.handleDragStop + ); + } + }; + + onMouseDown: EventHandler = (e) => { + dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse + + return this.handleDragStart(e); + }; + + onMouseUp: EventHandler = (e) => { + dragEventFor = eventsFor.mouse; + + return this.handleDragStop(e); + }; + + // Same as onMouseDown (start drag), but now consider this a touch device. + onTouchStart: EventHandler = (e) => { + // We're on a touch device now, so change the event handlers + dragEventFor = eventsFor.touch; + + return this.handleDragStart(e); + }; + + onTouchEnd: EventHandler = (e) => { + // We're on a touch device now, so change the event handlers + dragEventFor = eventsFor.touch; + + return this.handleDragStop(e); + }; + + render(): React.Element { + // Reuse the child provided + // This makes it flexible to use whatever element is wanted (div, ul, etc) + return React.cloneElement(React.Children.only(this.props.children), { + // Note: mouseMove handler is attached to document so it will still function + // when the user drags quickly and leaves the bounds of the element. + onMouseDown: this.onMouseDown, + onMouseUp: this.onMouseUp, + // onTouchStart is added on `componentDidMount` so they can be added with + // {passive: false}, which allows it to cancel. See + // https://developers.google.com/web/updates/2017/01/scrolling-intervention + onTouchEnd: this.onTouchEnd, + }); } - }; - - onMouseDown: EventHandler = (e) => { - dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse - - return this.handleDragStart(e); - }; - - onMouseUp: EventHandler = (e) => { - dragEventFor = eventsFor.mouse; - - return this.handleDragStop(e); - }; - - // Same as onMouseDown (start drag), but now consider this a touch device. - onTouchStart: EventHandler = (e) => { - // We're on a touch device now, so change the event handlers - dragEventFor = eventsFor.touch; - - return this.handleDragStart(e); - }; - - onTouchEnd: EventHandler = (e) => { - // We're on a touch device now, so change the event handlers - dragEventFor = eventsFor.touch; - - return this.handleDragStop(e); - }; - - render(): React.Element { - // Reuse the child provided - // This makes it flexible to use whatever element is wanted (div, ul, etc) - return React.cloneElement(React.Children.only(this.props.children), { - // Note: mouseMove handler is attached to document so it will still function - // when the user drags quickly and leaves the bounds of the element. - onMouseDown: this.onMouseDown, - onMouseUp: this.onMouseUp, - // onTouchStart is added on `componentDidMount` so they can be added with - // {passive: false}, which allows it to cancel. See - // https://developers.google.com/web/updates/2017/01/scrolling-intervention - onTouchEnd: this.onTouchEnd - }); - } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 14d29b8c..19cabfa8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,68 +1,82 @@ declare module 'react-draggable' { - import * as React from 'react'; + import * as React from 'react' - export interface DraggableBounds { - left?: number - right?: number - top?: number - bottom?: number - } + export interface DraggableBounds { + left?: number + right?: number + top?: number + bottom?: number + } - export interface DraggableProps extends DraggableCoreProps { - axis: 'both' | 'x' | 'y' | 'none', - bounds: DraggableBounds | string | false , - defaultClassName: string, - defaultClassNameDragging: string, - defaultClassNameDragged: string, - defaultPosition: ControlPosition, - positionOffset: PositionOffsetControlPosition, - position: ControlPosition - } + export interface DraggableProps extends DraggableCoreProps { + axis: 'both' | 'x' | 'y' | 'none' + bounds: DraggableBounds | string | false + defaultClassName: string + defaultClassNameDragging: string + defaultClassNameDragged: string + defaultPosition: ControlPosition + positionOffset: PositionOffsetControlPosition + position: ControlPosition + } - export type DraggableEvent = React.MouseEvent - | React.TouchEvent - | MouseEvent - | TouchEvent + export type DraggableEvent = + | React.MouseEvent + | React.TouchEvent + | MouseEvent + | TouchEvent - export type DraggableEventHandler = ( - e: DraggableEvent, - data: DraggableData - ) => void | false; + export type DraggableEventHandler = ( + e: DraggableEvent, + data: DraggableData + ) => void | false - export interface DraggableData { - node: HTMLElement, - x: number, y: number, - deltaX: number, deltaY: number, - lastX: number, lastY: number - } + export interface DraggableData { + node: HTMLElement + x: number + y: number + deltaX: number + deltaY: number + lastX: number + lastY: number + } - export type ControlPosition = {x: number, y: number}; + export type ControlPosition = { x: number; y: number } - export type PositionOffsetControlPosition = {x: number|string, y: number|string}; + export type PositionOffsetControlPosition = { + x: number | string + y: number | string + } - export interface DraggableCoreProps { - allowAnyClick: boolean, - allowMobileScroll: boolean, - cancel: string, - children?: React.ReactNode, - disabled: boolean, - enableUserSelectHack: boolean, - offsetParent: HTMLElement, - grid: [number, number], - handle: string, - nodeRef?: React.RefObject, - onStart: DraggableEventHandler, - onDrag: DraggableEventHandler, - onStop: DraggableEventHandler, - onMouseDown: (e: MouseEvent) => void, - scale: number - } + export interface DraggableCoreProps { + allowAnyClick: boolean + allowMobileScroll: boolean + cancel: string + children?: React.ReactNode + disabled: boolean + enableUserSelectHack: boolean + offsetParent: HTMLElement + grid: [number, number] + handle: string + nodeRef?: React.RefObject + onStart: DraggableEventHandler + onDrag: DraggableEventHandler + onStop: DraggableEventHandler + onMouseDown: (e: MouseEvent) => void + scale: number + mobileDragDelay?: number + } - export default class Draggable extends React.Component, {}> { - static defaultProps : DraggableProps; - } + export default class Draggable extends React.Component< + Partial, + {} + > { + static defaultProps: DraggableProps + } - export class DraggableCore extends React.Component, {}> { - static defaultProps : DraggableCoreProps; - } + export class DraggableCore extends React.Component< + Partial, + {} + > { + static defaultProps: DraggableCoreProps + } } From badd6d9f41e6e8c755bc598da64b35818e68ac0d Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:12:37 +0200 Subject: [PATCH 02/21] feat(pkg): Added prepare for github link install --- package.json | 211 ++++++++++++++++++++++++++------------------------- 1 file changed, 106 insertions(+), 105 deletions(-) diff --git a/package.json b/package.json index 9dc93af1..e87fc54f 100644 --- a/package.json +++ b/package.json @@ -1,106 +1,107 @@ { - "name": "react-draggable", - "version": "4.5.0", - "description": "React draggable component", - "main": "build/cjs/cjs.js", - "unpkg": "build/web/react-draggable.min.js", - "scripts": { - "test": "make test", - "test-phantom": "make test-phantom", - "test-debug": "karma start --browsers=Chrome --single-run=false --auto-watch=true", - "test-firefox": "karma start --browsers=Firefox --single-run=false --auto-watch=true", - "test-ie": "karma start --browsers=IE --single-run=false --auto-watch=true", - "dev": "make dev", - "build": "make clean build", - "lint": "make lint", - "flow": "flow" - }, - "files": [ - "/build", - "/typings", - "/web/react-draggable.min.js", - "/web/react-draggable.min.js.map" - ], - "typings": "./typings/index.d.ts", - "types": "./typings/index.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/react-grid-layout/react-draggable.git" - }, - "keywords": [ - "react", - "draggable", - "react-component" - ], - "author": "Matt Zabriskie", - "contributors": [ - "Samuel Reed (http://strml.net/)" - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/react-grid-layout/react-draggable/issues" - }, - "homepage": "https://github.com/react-grid-layout/react-draggable", - "devDependencies": { - "@types/react": "^18.2.23", - "@types/react-dom": "^18.2.8", - "@babel/cli": "^7.27.2", - "@babel/core": "^7.27.4", - "@babel/eslint-parser": "^7.27.5", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-flow-comments": "^7.27.3", - "@babel/preset-env": "^7.27.2", - "@babel/preset-flow": "^7.27.1", - "@babel/preset-react": "^7.27.1", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.29.0", - "@types/node": "^24.0.4", - "assert": "^2.1.0", - "babel-loader": "^10.0.0", - "babel-plugin-transform-inline-environment-variables": "^0.4.4", - "eslint": "^9.29.0", - "eslint-plugin-react": "^7.37.5", - "flow-bin": "^0.217.0", - "globals": "^16.2.0", - "jasmine-core": "^5.8.0", - "karma": "^6.4.4", - "karma-chrome-launcher": "^3.2.0", - "karma-cli": "2.0.0", - "karma-firefox-launcher": "^2.1.3", - "karma-ie-launcher": "^1.0.0", - "karma-jasmine": "^5.1.0", - "karma-phantomjs-launcher": "^1.0.4", - "karma-phantomjs-shim": "^1.5.0", - "karma-webpack": "^5.0.1", - "lodash": "^4.17.4", - "phantomjs-prebuilt": "^2.1.16", - "pre-commit": "^1.2.2", - "process": "^0.11.10", - "puppeteer": "^24.10.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-frame-component": "^5.2.7", - "react-test-renderer": "^16.13.1", - "semver": "^7.7.2", - "static-server": "^3.0.0", - "typescript": "^5.8.3", - "webpack": "^5.99.9", - "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.2" - }, - "resolutions": { - "minimist": "^1.2.5" - }, - "precommit": [ - "lint", - "test" - ], - "dependencies": { - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } -} \ No newline at end of file + "name": "react-draggable", + "version": "4.5.0", + "description": "React draggable component", + "main": "build/cjs/cjs.js", + "unpkg": "build/web/react-draggable.min.js", + "scripts": { + "test": "make test", + "test-phantom": "make test-phantom", + "test-debug": "karma start --browsers=Chrome --single-run=false --auto-watch=true", + "test-firefox": "karma start --browsers=Firefox --single-run=false --auto-watch=true", + "test-ie": "karma start --browsers=IE --single-run=false --auto-watch=true", + "dev": "make dev", + "build": "make clean build", + "lint": "make lint", + "flow": "flow", + "prepare": "make clean build" + }, + "files": [ + "/build", + "/typings", + "/web/react-draggable.min.js", + "/web/react-draggable.min.js.map" + ], + "typings": "./typings/index.d.ts", + "types": "./typings/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/react-grid-layout/react-draggable.git" + }, + "keywords": [ + "react", + "draggable", + "react-component" + ], + "author": "Matt Zabriskie", + "contributors": [ + "Samuel Reed (http://strml.net/)" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/react-grid-layout/react-draggable/issues" + }, + "homepage": "https://github.com/react-grid-layout/react-draggable", + "devDependencies": { + "@types/react": "^18.2.23", + "@types/react-dom": "^18.2.8", + "@babel/cli": "^7.27.2", + "@babel/core": "^7.27.4", + "@babel/eslint-parser": "^7.27.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-flow-comments": "^7.27.3", + "@babel/preset-env": "^7.27.2", + "@babel/preset-flow": "^7.27.1", + "@babel/preset-react": "^7.27.1", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.29.0", + "@types/node": "^24.0.4", + "assert": "^2.1.0", + "babel-loader": "^10.0.0", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", + "eslint": "^9.29.0", + "eslint-plugin-react": "^7.37.5", + "flow-bin": "^0.217.0", + "globals": "^16.2.0", + "jasmine-core": "^5.8.0", + "karma": "^6.4.4", + "karma-chrome-launcher": "^3.2.0", + "karma-cli": "2.0.0", + "karma-firefox-launcher": "^2.1.3", + "karma-ie-launcher": "^1.0.0", + "karma-jasmine": "^5.1.0", + "karma-phantomjs-launcher": "^1.0.4", + "karma-phantomjs-shim": "^1.5.0", + "karma-webpack": "^5.0.1", + "lodash": "^4.17.4", + "phantomjs-prebuilt": "^2.1.16", + "pre-commit": "^1.2.2", + "process": "^0.11.10", + "puppeteer": "^24.10.2", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-frame-component": "^5.2.7", + "react-test-renderer": "^16.13.1", + "semver": "^7.7.2", + "static-server": "^3.0.0", + "typescript": "^5.8.3", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1", + "webpack-dev-server": "^5.2.2" + }, + "resolutions": { + "minimist": "^1.2.5" + }, + "precommit": [ + "lint", + "test" + ], + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } +} From edb1db73c908a67b60087e07aaa2cbc81ed13944 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:26:55 +0200 Subject: [PATCH 03/21] reverted last commit --- package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e87fc54f..bb5e6f97 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "react-draggable", + "name": "@zsobecki_futurum/react-draggable", "version": "4.5.0", - "description": "React draggable component", + "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", "scripts": { @@ -13,8 +13,7 @@ "dev": "make dev", "build": "make clean build", "lint": "make lint", - "flow": "flow", - "prepare": "make clean build" + "flow": "flow" }, "files": [ "/build", From bcab60c0fcff6231a41c37d3c199e5dc5c28369a Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:27:46 +0200 Subject: [PATCH 04/21] chore(pkg): Bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb5e6f97..f843ae70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.0", + "version": "4.5.1", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From ab5df9117f135636ac4fdab371051c1acc8ab261 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:32:50 +0200 Subject: [PATCH 05/21] chore(pkg): Changed package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f843ae70..e4a1037d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@zsobecki_futurum/react-draggable", + "name": "@zsobecki-futurum/react-draggable", "version": "4.5.1", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", From 24b2a28d2e57f32810310f6f839cb0d289f837b0 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:35:22 +0200 Subject: [PATCH 06/21] chore(pkg): Another package update --- package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e4a1037d..6c5970a6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "types": "./typings/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/react-grid-layout/react-draggable.git" + "url": "https://github.com/zsobecki-futurum/react-draggable.git" }, "keywords": [ "react", @@ -34,13 +34,14 @@ ], "author": "Matt Zabriskie", "contributors": [ - "Samuel Reed (http://strml.net/)" + "Samuel Reed (http://strml.net/)", + "Zbyszko Sobecki " ], "license": "MIT", "bugs": { - "url": "https://github.com/react-grid-layout/react-draggable/issues" + "url": "https://github.com/zsobecki-futurum/react-draggable/issues" }, - "homepage": "https://github.com/react-grid-layout/react-draggable", + "homepage": "https://github.com/zsobecki-futurum/react-draggable", "devDependencies": { "@types/react": "^18.2.23", "@types/react-dom": "^18.2.8", From 2054c70a85494903eba05babbd01201ce2e96928 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:36:51 +0200 Subject: [PATCH 07/21] fix(pkg): Fixed package errors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c5970a6..c1a92183 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "types": "./typings/index.d.ts", "repository": { "type": "git", - "url": "https://github.com/zsobecki-futurum/react-draggable.git" + "url": "git+https://github.com/zsobecki-futurum/react-draggable.git" }, "keywords": [ "react", From 72e189b975e12d32ae173a6394d3b02f517060be Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:38:54 +0200 Subject: [PATCH 08/21] chore(pkg): Changed package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c1a92183..a4db1d8d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@zsobecki-futurum/react-draggable", + "name": "@zsobecki_futurum/react-draggable", "version": "4.5.1", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", From a5e4719585d40a5bbd14846cc249f7a640eff9ba Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:39:38 +0200 Subject: [PATCH 09/21] chore(pkg): Bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4db1d8d..eecb0507 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.1", + "version": "4.5.1b", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 2e212d178c0b5d3406c0bd64958b60e6af2449b1 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 14:40:27 +0200 Subject: [PATCH 10/21] chore(pkg): Bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eecb0507..1aaaaafd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.1b", + "version": "4.5.2", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 5b68117dcbaf67b3647b7ff4bc2fd2327405fe05 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 16:03:49 +0200 Subject: [PATCH 11/21] feat(mobileDragDelay): Added cancel when user moves during timeframe --- lib/DraggableCore.js | 45 ++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index ebbe7b8a..6eb384be 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -69,7 +69,7 @@ export type DraggableCoreDefaultProps = { onStop: DraggableEventHandler, onMouseDown: (e: MouseEvent) => void, scale: number, - mobileDragDelay?: number, + mobileDragDelay: number, }; export type DraggableCoreProps = { @@ -259,7 +259,7 @@ export default class DraggableCore extends React.Component { onStop: function () {}, onMouseDown: function () {}, scale: 1, - mobileDragDelay: 0, + mobileDragDelay: 250, // 250ms delay on mobile devices }; dragging: boolean = false; @@ -402,18 +402,39 @@ export default class DraggableCore extends React.Component { addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop); }; - // If mobileDragDelay prop exists and it's a touch — delay start - if ( - this.props.mobileDragDelay && - this.props.mobileDragDelay > 0 && - e.type === 'touchstart' - ) { + // If it's a touch — delay start + if (this.props.mobileDragDelay > 0 && e.type === 'touchstart') { clearTimeout(this.dragTimeout); if (typeof e.persist === 'function') e.persist(); - this.dragTimeout = setTimeout( - startDragging, - this.props.mobileDragDelay - ); + + const initialX = x; + const initialY = y; + + this.dragTimeout = setTimeout(() => { + const currentPosition = getControlPosition( + e, + touchIdentifier, + this + ); + if (currentPosition) { + const { x: currentX, y: currentY } = currentPosition; + const deltaX = Math.abs(currentX - initialX); + const deltaY = Math.abs(currentY - initialY); + + // Cancel drag if movement exceeds threshold + const movementThreshold = 10; + if ( + deltaX > movementThreshold || + deltaY > movementThreshold + ) { + log( + 'DraggableCore: Drag canceled due to movement during delay' + ); + return; + } + } + startDragging(); + }, this.props.mobileDragDelay); } else { // Mouse or no delay — start immediately startDragging(); From 4f399a99a3a81bbcba3fdd44434c57183b9c3ef7 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 16:04:10 +0200 Subject: [PATCH 12/21] chore(pkg): Bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1aaaaafd..4bb57c64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.2", + "version": "4.5.3", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From f0fd7567b043d76e97b9847dbd3868d3fcf5fed7 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 16:29:53 +0200 Subject: [PATCH 13/21] feat(handleMobileDrag): Added cancel on user move --- .babelrc.js | 20 +- eslint.config.mjs | 38 +-- lib/Draggable.js | 740 +++++++++++++++++++++++-------------------- lib/DraggableCore.js | 12 +- package.json | 2 +- 5 files changed, 428 insertions(+), 384 deletions(-) diff --git a/.babelrc.js b/.babelrc.js index 1d936d79..41f61a1d 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,19 +1,19 @@ 'use strict'; module.exports = { - "presets": [ + 'presets': [ [ - "@babel/preset-env", + '@babel/preset-env', { - targets: "> 0.25%, not dead" + targets: '> 0.25%, not dead' }, ], - "@babel/react", - "@babel/preset-flow" + '@babel/react', + '@babel/preset-flow' ], - "plugins": [ - "@babel/plugin-transform-flow-comments", - "@babel/plugin-transform-class-properties", - "transform-inline-environment-variables" + 'plugins': [ + '@babel/plugin-transform-flow-comments', + '@babel/plugin-transform-class-properties', + 'transform-inline-environment-variables' ] -} +}; diff --git a/eslint.config.mjs b/eslint.config.mjs index 11cf50d9..584c54c7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,11 +1,11 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import react from "eslint-plugin-react"; -import globals from "globals"; -import babelParser from "@babel/eslint-parser"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import react from 'eslint-plugin-react'; +import globals from 'globals'; +import babelParser from '@babel/eslint-parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -15,8 +15,8 @@ const compat = new FlatCompat({ allConfig: js.configs.all }); -export default defineConfig([globalIgnores(["build/**/*.js"]), { - extends: compat.extends("eslint:recommended"), +export default defineConfig([globalIgnores(['build/**/*.js']), { + extends: compat.extends('eslint:recommended'), plugins: { react, @@ -39,20 +39,20 @@ export default defineConfig([globalIgnores(["build/**/*.js"]), { rules: { strict: 0, - quotes: [1, "single"], - curly: [1, "multi-line"], + quotes: [1, 'single'], + curly: [1, 'multi-line'], camelcase: 0, - "comma-dangle": 0, - "no-console": 2, - "no-use-before-define": [1, "nofunc"], - "no-underscore-dangle": 0, + 'comma-dangle': 0, + 'no-console': 2, + 'no-use-before-define': [1, 'nofunc'], + 'no-underscore-dangle': 0, - "no-unused-vars": [1, { + 'no-unused-vars': [1, { ignoreRestSiblings: true, }], - "new-cap": 0, - "prefer-const": 1, + 'new-cap': 0, + 'prefer-const': 1, semi: 1, }, }]); \ No newline at end of file diff --git a/lib/Draggable.js b/lib/Draggable.js index 1ce95ee1..094cce8c 100644 --- a/lib/Draggable.js +++ b/lib/Draggable.js @@ -3,40 +3,52 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import { clsx } from 'clsx'; -import {createCSSTransform, createSVGTransform} from './utils/domFns'; -import {canDragX, canDragY, createDraggableData, getBoundPosition} from './utils/positionFns'; -import {dontSetMe} from './utils/shims'; +import { createCSSTransform, createSVGTransform } from './utils/domFns'; +import { + canDragX, + canDragY, + createDraggableData, + getBoundPosition, +} from './utils/positionFns'; +import { dontSetMe } from './utils/shims'; import DraggableCore from './DraggableCore'; -import type {ControlPosition, PositionOffsetControlPosition, DraggableCoreProps, DraggableCoreDefaultProps} from './DraggableCore'; +import type { + ControlPosition, + PositionOffsetControlPosition, + DraggableCoreProps, + DraggableCoreDefaultProps, +} from './DraggableCore'; import log from './utils/log'; -import type {Bounds, DraggableEventHandler} from './utils/types'; -import type {Element as ReactElement} from 'react'; +import type { Bounds, DraggableEventHandler } from './utils/types'; +import type { Element as ReactElement } from 'react'; type DraggableState = { - dragging: boolean, - dragged: boolean, - x: number, y: number, - slackX: number, slackY: number, - isElementSVG: boolean, - prevPropsPosition: ?ControlPosition, + dragging: boolean, + dragged: boolean, + x: number, + y: number, + slackX: number, + slackY: number, + isElementSVG: boolean, + prevPropsPosition: ?ControlPosition, }; export type DraggableDefaultProps = { - ...DraggableCoreDefaultProps, - axis: 'both' | 'x' | 'y' | 'none', - bounds: Bounds | string | false, - defaultClassName: string, - defaultClassNameDragging: string, - defaultClassNameDragged: string, - defaultPosition: ControlPosition, - scale: number, + ...DraggableCoreDefaultProps, + axis: 'both' | 'x' | 'y' | 'none', + bounds: Bounds | string | false, + defaultClassName: string, + defaultClassNameDragging: string, + defaultClassNameDragged: string, + defaultPosition: ControlPosition, + scale: number, }; export type DraggableProps = { - ...DraggableCoreProps, - ...DraggableDefaultProps, - positionOffset: PositionOffsetControlPosition, - position: ControlPosition, + ...DraggableCoreProps, + ...DraggableDefaultProps, + positionOffset: PositionOffsetControlPosition, + position: ControlPosition, }; // @@ -44,358 +56,384 @@ export type DraggableProps = { // class Draggable extends React.Component { + static displayName: ?string = 'Draggable'; + + static propTypes: DraggableProps = { + // Accepts all props accepts. + ...DraggableCore.propTypes, + + /** + * `axis` determines which axis the draggable can move. + * + * Note that all callbacks will still return data as normal. This only + * controls flushing to the DOM. + * + * 'both' allows movement horizontally and vertically. + * 'x' limits movement to horizontal axis. + * 'y' limits movement to vertical axis. + * 'none' limits all movement. + * + * Defaults to 'both'. + */ + axis: PropTypes.oneOf(['both', 'x', 'y', 'none']), + + /** + * `bounds` determines the range of movement available to the element. + * Available values are: + * + * 'parent' restricts movement within the Draggable's parent node. + * + * Alternatively, pass an object with the following properties, all of which are optional: + * + * {left: LEFT_BOUND, right: RIGHT_BOUND, bottom: BOTTOM_BOUND, top: TOP_BOUND} + * + * All values are in px. + * + * Example: + * + * ```jsx + * let App = React.createClass({ + * render: function () { + * return ( + * + *
Content
+ *
+ * ); + * } + * }); + * ``` + */ + bounds: PropTypes.oneOfType([ + PropTypes.shape({ + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + bottom: PropTypes.number, + }), + PropTypes.string, + PropTypes.oneOf([false]), + ]), + + defaultClassName: PropTypes.string, + defaultClassNameDragging: PropTypes.string, + defaultClassNameDragged: PropTypes.string, + + /** + * `defaultPosition` specifies the x and y that the dragged item should start at + * + * Example: + * + * ```jsx + * let App = React.createClass({ + * render: function () { + * return ( + * + *
I start with transformX: 25px and transformY: 25px;
+ *
+ * ); + * } + * }); + * ``` + */ + defaultPosition: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + positionOffset: PropTypes.shape({ + x: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + }), + + /** + * `position`, if present, defines the current position of the element. + * + * This is similar to how form elements in React work - if no `position` is supplied, the component + * is uncontrolled. + * + * Example: + * + * ```jsx + * let App = React.createClass({ + * render: function () { + * return ( + * + *
I start with transformX: 25px and transformY: 25px;
+ *
+ * ); + * } + * }); + * ``` + */ + position: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + + /** + * These properties should be defined on the child, not here. + */ + className: dontSetMe, + style: dontSetMe, + transform: dontSetMe, + }; + + static defaultProps: DraggableDefaultProps = { + ...DraggableCore.defaultProps, + axis: 'both', + bounds: false, + defaultClassName: 'react-draggable', + defaultClassNameDragging: 'react-draggable-dragging', + defaultClassNameDragged: 'react-draggable-dragged', + defaultPosition: { x: 0, y: 0 }, + scale: 1, + }; - static displayName: ?string = 'Draggable'; - - static propTypes: DraggableProps = { - // Accepts all props accepts. - ...DraggableCore.propTypes, - - /** - * `axis` determines which axis the draggable can move. - * - * Note that all callbacks will still return data as normal. This only - * controls flushing to the DOM. - * - * 'both' allows movement horizontally and vertically. - * 'x' limits movement to horizontal axis. - * 'y' limits movement to vertical axis. - * 'none' limits all movement. - * - * Defaults to 'both'. - */ - axis: PropTypes.oneOf(['both', 'x', 'y', 'none']), - - /** - * `bounds` determines the range of movement available to the element. - * Available values are: - * - * 'parent' restricts movement within the Draggable's parent node. - * - * Alternatively, pass an object with the following properties, all of which are optional: - * - * {left: LEFT_BOUND, right: RIGHT_BOUND, bottom: BOTTOM_BOUND, top: TOP_BOUND} - * - * All values are in px. - * - * Example: - * - * ```jsx - * let App = React.createClass({ - * render: function () { - * return ( - * - *
Content
- *
- * ); - * } - * }); - * ``` - */ - bounds: PropTypes.oneOfType([ - PropTypes.shape({ - left: PropTypes.number, - right: PropTypes.number, - top: PropTypes.number, - bottom: PropTypes.number - }), - PropTypes.string, - PropTypes.oneOf([false]) - ]), - - defaultClassName: PropTypes.string, - defaultClassNameDragging: PropTypes.string, - defaultClassNameDragged: PropTypes.string, - - /** - * `defaultPosition` specifies the x and y that the dragged item should start at - * - * Example: - * - * ```jsx - * let App = React.createClass({ - * render: function () { - * return ( - * - *
I start with transformX: 25px and transformY: 25px;
- *
- * ); - * } - * }); - * ``` - */ - defaultPosition: PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number - }), - positionOffset: PropTypes.shape({ - x: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) - }), - - /** - * `position`, if present, defines the current position of the element. - * - * This is similar to how form elements in React work - if no `position` is supplied, the component - * is uncontrolled. - * - * Example: - * - * ```jsx - * let App = React.createClass({ - * render: function () { - * return ( - * - *
I start with transformX: 25px and transformY: 25px;
- *
- * ); - * } - * }); - * ``` - */ - position: PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number - }), - - /** - * These properties should be defined on the child, not here. - */ - className: dontSetMe, - style: dontSetMe, - transform: dontSetMe - }; - - static defaultProps: DraggableDefaultProps = { - ...DraggableCore.defaultProps, - axis: 'both', - bounds: false, - defaultClassName: 'react-draggable', - defaultClassNameDragging: 'react-draggable-dragging', - defaultClassNameDragged: 'react-draggable-dragged', - defaultPosition: {x: 0, y: 0}, - scale: 1 - }; - - // React 16.3+ - // Arity (props, state) - static getDerivedStateFromProps({position}: DraggableProps, {prevPropsPosition}: DraggableState): ?Partial { - // Set x/y if a new position is provided in props that is different than the previous. - if ( - position && - (!prevPropsPosition || - position.x !== prevPropsPosition.x || position.y !== prevPropsPosition.y - ) - ) { - log('Draggable: getDerivedStateFromProps %j', {position, prevPropsPosition}); - return { - x: position.x, - y: position.y, - prevPropsPosition: {...position} - }; + // React 16.3+ + // Arity (props, state) + static getDerivedStateFromProps( + { position }: DraggableProps, + { prevPropsPosition }: DraggableState + ): ?Partial { + // Set x/y if a new position is provided in props that is different than the previous. + if ( + position && + (!prevPropsPosition || + position.x !== prevPropsPosition.x || + position.y !== prevPropsPosition.y) + ) { + log('Draggable: getDerivedStateFromProps %j', { + position, + prevPropsPosition, + }); + return { + x: position.x, + y: position.y, + prevPropsPosition: { ...position }, + }; + } + return null; } - return null; - } - constructor(props: DraggableProps) { - super(props); + constructor(props: DraggableProps) { + super(props); - this.state = { - // Whether or not we are currently dragging. - dragging: false, + this.state = { + // Whether or not we are currently dragging. + dragging: false, - // Whether or not we have been dragged before. - dragged: false, + // Whether or not we have been dragged before. + dragged: false, - // Current transform x and y. - x: props.position ? props.position.x : props.defaultPosition.x, - y: props.position ? props.position.y : props.defaultPosition.y, + // Current transform x and y. + x: props.position ? props.position.x : props.defaultPosition.x, + y: props.position ? props.position.y : props.defaultPosition.y, - prevPropsPosition: {...props.position}, + prevPropsPosition: { ...props.position }, - // Used for compensating for out-of-bounds drags - slackX: 0, slackY: 0, + // Used for compensating for out-of-bounds drags + slackX: 0, + slackY: 0, - // Can only determine if SVG after mounting - isElementSVG: false - }; + // Can only determine if SVG after mounting + isElementSVG: false, + }; - if (props.position && !(props.onDrag || props.onStop)) { - // eslint-disable-next-line no-console - console.warn('A `position` was applied to this , without drag handlers. This will make this ' + - 'component effectively undraggable. Please attach `onDrag` or `onStop` handlers so you can adjust the ' + - '`position` of this element.'); + if (props.position && !(props.onDrag || props.onStop)) { + // eslint-disable-next-line no-console + console.warn( + 'A `position` was applied to this , without drag handlers. This will make this ' + + 'component effectively undraggable. Please attach `onDrag` or `onStop` handlers so you can adjust the ' + + '`position` of this element.' + ); + } } - } - componentDidMount() { - // Check to see if the element passed is an instanceof SVGElement - if(typeof window.SVGElement !== 'undefined' && this.findDOMNode() instanceof window.SVGElement) { - this.setState({isElementSVG: true}); + componentDidMount() { + // Check to see if the element passed is an instanceof SVGElement + if ( + typeof window.SVGElement !== 'undefined' && + this.findDOMNode() instanceof window.SVGElement + ) { + this.setState({ isElementSVG: true }); + } } - } - componentWillUnmount() { - if (this.state.dragging) { - this.setState({dragging: false}); // prevents invariant if unmounted while dragging + componentWillUnmount() { + if (this.state.dragging) { + this.setState({ dragging: false }); // prevents invariant if unmounted while dragging + } } - } - - // React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find - // the underlying DOM node ourselves. See the README for more information. - findDOMNode(): ?HTMLElement { - return this.props?.nodeRef?.current ?? ReactDOM.findDOMNode(this); - } - - onDragStart: DraggableEventHandler = (e, coreData) => { - log('Draggable: onDragStart: %j', coreData); - - // Short-circuit if user's callback killed it. - const shouldStart = this.props.onStart(e, createDraggableData(this, coreData)); - // Kills start event on core as well, so move handlers are never bound. - if (shouldStart === false) return false; - - this.setState({dragging: true, dragged: true}); - }; - - onDrag: DraggableEventHandler = (e, coreData) => { - if (!this.state.dragging) return false; - log('Draggable: onDrag: %j', coreData); - const uiData = createDraggableData(this, coreData); - - const newState = { - x: uiData.x, - y: uiData.y, - slackX: 0, - slackY: 0, - }; - - // Keep within bounds. - if (this.props.bounds) { - // Save original x and y. - const {x, y} = newState; - - // Add slack to the values used to calculate bound position. This will ensure that if - // we start removing slack, the element won't react to it right away until it's been - // completely removed. - newState.x += this.state.slackX; - newState.y += this.state.slackY; - - // Get bound position. This will ceil/floor the x and y within the boundaries. - const [newStateX, newStateY] = getBoundPosition(this, newState.x, newState.y); - newState.x = newStateX; - newState.y = newStateY; - - // Recalculate slack by noting how much was shaved by the boundPosition handler. - newState.slackX = this.state.slackX + (x - newState.x); - newState.slackY = this.state.slackY + (y - newState.y); - - // Update the event we fire to reflect what really happened after bounds took effect. - uiData.x = newState.x; - uiData.y = newState.y; - uiData.deltaX = newState.x - this.state.x; - uiData.deltaY = newState.y - this.state.y; + // React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find + // the underlying DOM node ourselves. See the README for more information. + findDOMNode(): ?HTMLElement { + return this.props?.nodeRef?.current ?? ReactDOM.findDOMNode(this); } - // Short-circuit if user's callback killed it. - const shouldUpdate = this.props.onDrag(e, uiData); - if (shouldUpdate === false) return false; - - this.setState(newState); - }; - - onDragStop: DraggableEventHandler = (e, coreData) => { - if (!this.state.dragging) return false; + onDragStart: DraggableEventHandler = (e, coreData) => { + log('Draggable: onDragStart: %j', coreData); - // Short-circuit if user's callback killed it. - const shouldContinue = this.props.onStop(e, createDraggableData(this, coreData)); - if (shouldContinue === false) return false; + // Short-circuit if user's callback killed it. + const shouldStart = this.props.onStart( + e, + createDraggableData(this, coreData) + ); + // Kills start event on core as well, so move handlers are never bound. + if (shouldStart === false) return false; - log('Draggable: onDragStop: %j', coreData); - - const newState: Partial = { - dragging: false, - slackX: 0, - slackY: 0 + this.setState({ dragging: true, dragged: true }); }; - // If this is a controlled component, the result of this operation will be to - // revert back to the old position. We expect a handler on `onDragStop`, at the least. - const controlled = Boolean(this.props.position); - if (controlled) { - const {x, y} = this.props.position; - newState.x = x; - newState.y = y; - } + onDrag: DraggableEventHandler = (e, coreData) => { + if (!this.state.dragging) return false; + log('Draggable: onDrag: %j', coreData); + + const uiData = createDraggableData(this, coreData); + + const newState = { + x: uiData.x, + y: uiData.y, + slackX: 0, + slackY: 0, + }; + + // Keep within bounds. + if (this.props.bounds) { + // Save original x and y. + const { x, y } = newState; + + // Add slack to the values used to calculate bound position. This will ensure that if + // we start removing slack, the element won't react to it right away until it's been + // completely removed. + newState.x += this.state.slackX; + newState.y += this.state.slackY; + + // Get bound position. This will ceil/floor the x and y within the boundaries. + const [newStateX, newStateY] = getBoundPosition( + this, + newState.x, + newState.y + ); + newState.x = newStateX; + newState.y = newStateY; + + // Recalculate slack by noting how much was shaved by the boundPosition handler. + newState.slackX = this.state.slackX + (x - newState.x); + newState.slackY = this.state.slackY + (y - newState.y); + + // Update the event we fire to reflect what really happened after bounds took effect. + uiData.x = newState.x; + uiData.y = newState.y; + uiData.deltaX = newState.x - this.state.x; + uiData.deltaY = newState.y - this.state.y; + } + + // Short-circuit if user's callback killed it. + const shouldUpdate = this.props.onDrag(e, uiData); + if (shouldUpdate === false) return false; + + this.setState(newState); + }; - this.setState(newState); - }; - - render(): ReactElement { - const { - axis, - bounds, - children, - defaultPosition, - defaultClassName, - defaultClassNameDragging, - defaultClassNameDragged, - position, - positionOffset, - scale, - ...draggableCoreProps - } = this.props; - - let style = {}; - let svgTransform = null; - - // If this is controlled, we don't want to move it - unless it's dragging. - const controlled = Boolean(position); - const draggable = !controlled || this.state.dragging; - - const validPosition = position || defaultPosition; - const transformOpts = { - // Set left if horizontal drag is enabled - x: canDragX(this) && draggable ? - this.state.x : - validPosition.x, - - // Set top if vertical drag is enabled - y: canDragY(this) && draggable ? - this.state.y : - validPosition.y + onDragStop: DraggableEventHandler = (e, coreData) => { + if (!this.state.dragging) return false; + + // Short-circuit if user's callback killed it. + const shouldContinue = this.props.onStop( + e, + createDraggableData(this, coreData) + ); + if (shouldContinue === false) return false; + + log('Draggable: onDragStop: %j', coreData); + + const newState: Partial = { + dragging: false, + slackX: 0, + slackY: 0, + }; + + // If this is a controlled component, the result of this operation will be to + // revert back to the old position. We expect a handler on `onDragStop`, at the least. + const controlled = Boolean(this.props.position); + if (controlled) { + const { x, y } = this.props.position; + newState.x = x; + newState.y = y; + } + + this.setState(newState); }; - // If this element was SVG, we use the `transform` attribute. - if (this.state.isElementSVG) { - svgTransform = createSVGTransform(transformOpts, positionOffset); - } else { - // Add a CSS transform to move the element around. This allows us to move the element around - // without worrying about whether or not it is relatively or absolutely positioned. - // If the item you are dragging already has a transform set, wrap it in a so - // has a clean slate. - style = createCSSTransform(transformOpts, positionOffset); + render(): ReactElement { + const { + axis, + bounds, + children, + defaultPosition, + defaultClassName, + defaultClassNameDragging, + defaultClassNameDragged, + position, + positionOffset, + scale, + ...draggableCoreProps + } = this.props; + + let style = {}; + let svgTransform = null; + + // If this is controlled, we don't want to move it - unless it's dragging. + const controlled = Boolean(position); + const draggable = !controlled || this.state.dragging; + + const validPosition = position || defaultPosition; + const transformOpts = { + // Set left if horizontal drag is enabled + x: canDragX(this) && draggable ? this.state.x : validPosition.x, + + // Set top if vertical drag is enabled + y: canDragY(this) && draggable ? this.state.y : validPosition.y, + }; + + // If this element was SVG, we use the `transform` attribute. + if (this.state.isElementSVG) { + svgTransform = createSVGTransform(transformOpts, positionOffset); + } else { + // Add a CSS transform to move the element around. This allows us to move the element around + // without worrying about whether or not it is relatively or absolutely positioned. + // If the item you are dragging already has a transform set, wrap it in a so + // has a clean slate. + style = createCSSTransform(transformOpts, positionOffset); + } + + // Mark with class while dragging + const className = clsx( + children.props.className || '', + defaultClassName, + { + [defaultClassNameDragging]: this.state.dragging, + [defaultClassNameDragged]: this.state.dragged, + } + ); + + // Reuse the child provided + // This makes it flexible to use whatever element is wanted (div, ul, etc) + return ( + + {React.cloneElement(React.Children.only(children), { + className: className, + style: { ...children.props.style, ...style }, + transform: svgTransform, + })} + + ); } - - // Mark with class while dragging - const className = clsx((children.props.className || ''), defaultClassName, { - [defaultClassNameDragging]: this.state.dragging, - [defaultClassNameDragged]: this.state.dragged - }); - - // Reuse the child provided - // This makes it flexible to use whatever element is wanted (div, ul, etc) - return ( - - {React.cloneElement(React.Children.only(children), { - className: className, - style: {...children.props.style, ...style}, - transform: svgTransform - })} - - ); - } } -export {Draggable as default, DraggableCore}; +export { Draggable as default, DraggableCore }; diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 6eb384be..ae69e47e 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -134,7 +134,7 @@ export default class DraggableCore extends React.Component { propName: $Keys ) { if (props[propName] && props[propName].nodeType !== 1) { - throw new Error("Draggable's offsetParent must be a DOM Node."); + throw new Error('Draggable\'s offsetParent must be a DOM Node.'); } }, @@ -367,7 +367,9 @@ export default class DraggableCore extends React.Component { } // Prevent scrolling on mobile devices, like iPad/iPhone - if (e.type === 'touchstart') e.preventDefault(); + if (e.type === 'touchstart' && !this.props.allowMobileScroll) { + e.preventDefault(); + } // Set touch identifier const touchIdentifier = getTouchIdentifier(e); @@ -403,7 +405,11 @@ export default class DraggableCore extends React.Component { }; // If it's a touch — delay start - if (this.props.mobileDragDelay > 0 && e.type === 'touchstart') { + if ( + this.props.allowMobileScroll && + this.props.mobileDragDelay > 0 && + e.type === 'touchstart' + ) { clearTimeout(this.dragTimeout); if (typeof e.persist === 'function') e.persist(); diff --git a/package.json b/package.json index 4bb57c64..34e040a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.3", + "version": "4.5.4", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 2cd53015193ae603c7c5a6743012a14a072fa4c9 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 17:38:39 +0200 Subject: [PATCH 14/21] fix(handleMobileDrag): Fixed cancel on user move --- lib/DraggableCore.js | 30 +++++++++++++++++------------- package.json | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index ae69e47e..0f05aec2 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -134,7 +134,7 @@ export default class DraggableCore extends React.Component { propName: $Keys ) { if (props[propName] && props[propName].nodeType !== 1) { - throw new Error('Draggable\'s offsetParent must be a DOM Node.'); + throw new Error("Draggable's offsetParent must be a DOM Node."); } }, @@ -378,13 +378,11 @@ export default class DraggableCore extends React.Component { // Get the current drag point const position = getControlPosition(e, touchIdentifier, this); if (position == null) return; - const { x, y } = position; + const { x: initialX, y: initialY } = position; - // Create an event object - const coreEvent = createCoreData(this, x, y); + const coreEvent = createCoreData(this, initialX, initialY); log('DraggableCore: handleDragStart: %j', coreEvent); - // Function to start dragging const startDragging = () => { log('calling', this.props.onStart); const shouldUpdate = this.props.onStart(e, coreEvent); @@ -396,8 +394,8 @@ export default class DraggableCore extends React.Component { } this.dragging = true; - this.lastX = x; - this.lastY = y; + this.lastX = initialX; // Update lastX and lastY only after drag starts + this.lastY = initialY; // Add global move/stop listeners addEvent(ownerDocument, dragEventFor.move, this.handleDrag); @@ -413,10 +411,17 @@ export default class DraggableCore extends React.Component { clearTimeout(this.dragTimeout); if (typeof e.persist === 'function') e.persist(); - const initialX = x; - const initialY = y; + const onTouchEnd = () => { + log('DraggableCore: touchend detected, canceling drag timeout'); + clearTimeout(this.dragTimeout); + removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); + }; + + addEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); this.dragTimeout = setTimeout(() => { + removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); + const currentPosition = getControlPosition( e, touchIdentifier, @@ -427,12 +432,11 @@ export default class DraggableCore extends React.Component { const deltaX = Math.abs(currentX - initialX); const deltaY = Math.abs(currentY - initialY); + const delta = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + // Cancel drag if movement exceeds threshold const movementThreshold = 10; - if ( - deltaX > movementThreshold || - deltaY > movementThreshold - ) { + if (delta > movementThreshold) { log( 'DraggableCore: Drag canceled due to movement during delay' ); diff --git a/package.json b/package.json index 34e040a7..f56e3793 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.4", + "version": "4.5.5", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 6c3f21de5929331f7825a8b9c09bad10c6e319ef Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 18:57:47 +0200 Subject: [PATCH 15/21] fix(handleMobileDrag): Fixed cancel on user move 2 --- lib/DraggableCore.js | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 0f05aec2..cced55ac 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -436,14 +436,15 @@ export default class DraggableCore extends React.Component { // Cancel drag if movement exceeds threshold const movementThreshold = 10; - if (delta > movementThreshold) { + if (delta < movementThreshold) { + startDragging(); + } else { log( 'DraggableCore: Drag canceled due to movement during delay' ); return; } } - startDragging(); }, this.props.mobileDragDelay); } else { // Mouse or no delay — start immediately diff --git a/package.json b/package.json index f56e3793..42608c00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.5", + "version": "4.5.6", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From efa7f8781dd78dde60a9e3da01f7693293cef85d Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 19:06:10 +0200 Subject: [PATCH 16/21] fix(handleMobileDrag): Fixed cancel on user move 3 --- README.md | 32 ++++++++++++++++---------------- lib/DraggableCore.js | 4 ++-- package.json | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4ec529f7..67af2cc0 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ Here's how to use it: ```js // ES6 -import Draggable from 'react-draggable' // The default -import { DraggableCore } from 'react-draggable' // -import Draggable, { DraggableCore } from 'react-draggable' // Both at the same time +import Draggable from 'react-draggable'; // The default +import { DraggableCore } from 'react-draggable'; // +import Draggable, { DraggableCore } from 'react-draggable'; // Both at the same time // CommonJS -let Draggable = require('react-draggable') -let DraggableCore = Draggable.DraggableCore +let Draggable = require('react-draggable'); +let DraggableCore = Draggable.DraggableCore; ``` ## `` @@ -88,15 +88,15 @@ View the [Demo](http://react-grid-layout.github.io/react-draggable/example/) and [source](/example/example.js) for more. ```js -import React from 'react' -import ReactDOM from 'react-dom' -import Draggable from 'react-draggable' +import React from 'react'; +import ReactDOM from 'react-dom'; +import Draggable from 'react-draggable'; class App extends React.Component { eventLogger = (e: MouseEvent, data: Object) => { - console.log('Event: ', e) - console.log('Data: ', data) - } + console.log('Event: ', e); + console.log('Data: ', data); + }; render() { return ( @@ -116,11 +116,11 @@ class App extends React.Component {
This readme is really dragging on...
- ) + ); } } -ReactDOM.render(, document.body) +ReactDOM.render(, document.body); ``` ### Draggable API @@ -288,9 +288,9 @@ positionOffset: {x: number | string, y: number | string}, // a transform or matrix in the parent of this element. scale: number, -// If true, will add a delay to the start of the drag on mobile devices. -// This is useful to prevent accidental drags when a user is trying to scroll. -mobileDragDelay: number, +// The delay is in milliseconds and defaults to 0. If you set this to a value greater than 0, +// the drag will not start until the specified delay has passed after the initial touchstart event. +mobileDragDelay: number | undefined } ``` diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index cced55ac..01673dc7 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -69,7 +69,6 @@ export type DraggableCoreDefaultProps = { onStop: DraggableEventHandler, onMouseDown: (e: MouseEvent) => void, scale: number, - mobileDragDelay: number, }; export type DraggableCoreProps = { @@ -80,6 +79,7 @@ export type DraggableCoreProps = { grid: [number, number], handle: string, nodeRef?: ?React.ElementRef, + mobileDragDelay?: number, }; // @@ -259,7 +259,6 @@ export default class DraggableCore extends React.Component { onStop: function () {}, onMouseDown: function () {}, scale: 1, - mobileDragDelay: 250, // 250ms delay on mobile devices }; dragging: boolean = false; @@ -405,6 +404,7 @@ export default class DraggableCore extends React.Component { // If it's a touch — delay start if ( this.props.allowMobileScroll && + this.props.mobileDragDelay && this.props.mobileDragDelay > 0 && e.type === 'touchstart' ) { diff --git a/package.json b/package.json index 42608c00..d211c8f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.6", + "version": "4.5.7", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 60092459f005f9ded67da2e255ea0411f3d7ae53 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 19:25:55 +0200 Subject: [PATCH 17/21] test --- lib/DraggableCore.js | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 01673dc7..410c6d2c 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -420,6 +420,7 @@ export default class DraggableCore extends React.Component { addEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); this.dragTimeout = setTimeout(() => { + console.log('Timeout end'); removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); const currentPosition = getControlPosition( @@ -436,6 +437,7 @@ export default class DraggableCore extends React.Component { // Cancel drag if movement exceeds threshold const movementThreshold = 10; + console.log('Delta:', delta); if (delta < movementThreshold) { startDragging(); } else { diff --git a/package.json b/package.json index d211c8f5..da23a6c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.7", + "version": "4.5.7-test", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 1ed7bb91de8b4d7dac10cc77517c99a53f8f7c82 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 20:11:44 +0200 Subject: [PATCH 18/21] test 2 --- lib/DraggableCore.js | 58 +++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 410c6d2c..58979027 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -69,6 +69,7 @@ export type DraggableCoreDefaultProps = { onStop: DraggableEventHandler, onMouseDown: (e: MouseEvent) => void, scale: number, + mobileDragDelay: number, }; export type DraggableCoreProps = { @@ -259,6 +260,7 @@ export default class DraggableCore extends React.Component { onStop: function () {}, onMouseDown: function () {}, scale: 1, + mobileDragDelay: 250, // 250ms delay on mobile devices }; dragging: boolean = false; @@ -411,20 +413,22 @@ export default class DraggableCore extends React.Component { clearTimeout(this.dragTimeout); if (typeof e.persist === 'function') e.persist(); + // Prevent context menu on long touch + const onContextMenu = (contextEvent: MouseTouchEvent) => { + contextEvent.preventDefault(); + }; + addEvent(ownerDocument, 'contextmenu', onContextMenu); + const onTouchEnd = () => { log('DraggableCore: touchend detected, canceling drag timeout'); clearTimeout(this.dragTimeout); removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); + removeEvent(ownerDocument, 'contextmenu', onContextMenu); }; - addEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); - - this.dragTimeout = setTimeout(() => { - console.log('Timeout end'); - removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); - + const onTouchMove = (moveEvent: MouseTouchEvent) => { const currentPosition = getControlPosition( - e, + moveEvent, touchIdentifier, this ); @@ -432,21 +436,42 @@ export default class DraggableCore extends React.Component { const { x: currentX, y: currentY } = currentPosition; const deltaX = Math.abs(currentX - initialX); const deltaY = Math.abs(currentY - initialY); - const delta = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - // Cancel drag if movement exceeds threshold const movementThreshold = 10; - console.log('Delta:', delta); - if (delta < movementThreshold) { - startDragging(); - } else { + if (delta > movementThreshold) { log( 'DraggableCore: Drag canceled due to movement during delay' ); - return; + clearTimeout(this.dragTimeout); + removeEvent( + ownerDocument, + eventsFor.touch.stop, + onTouchEnd + ); + removeEvent( + ownerDocument, + eventsFor.touch.move, + onTouchMove + ); + removeEvent( + ownerDocument, + 'contextmenu', + onContextMenu + ); } } + }; + + addEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); + addEvent(ownerDocument, eventsFor.touch.move, onTouchMove); + + this.dragTimeout = setTimeout(() => { + log('DraggableCore: Timeout end, starting drag'); + removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); + removeEvent(ownerDocument, eventsFor.touch.move, onTouchMove); + removeEvent(ownerDocument, 'contextmenu', onContextMenu); + startDragging(); }, this.props.mobileDragDelay); } else { // Mouse or no delay — start immediately @@ -455,6 +480,9 @@ export default class DraggableCore extends React.Component { }; handleDrag: EventHandler = (e) => { + // Ensure drag has started + if (!this.dragging) return; + // Get the current drag point from the event. This is used as the offset. const position = getControlPosition(e, this.touchIdentifier, this); if (position == null) return; @@ -513,7 +541,9 @@ export default class DraggableCore extends React.Component { }; handleDragStop: EventHandler = (e) => { + // Ensure drag has started if (!this.dragging) return; + if (this.dragTimeout) { clearTimeout(this.dragTimeout); this.dragTimeout = null; diff --git a/package.json b/package.json index da23a6c8..6da8d55a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.7-test", + "version": "4.5.8", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From b8dc99890f15d24ce8304631027e8e8d43861fbe Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 20:17:54 +0200 Subject: [PATCH 19/21] removed context menu on touch --- lib/DraggableCore.js | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 58979027..451220f7 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -284,6 +284,8 @@ export default class DraggableCore extends React.Component { addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, { passive: false, }); + // Disable context menu for touch elements + addEvent(thisNode, 'contextmenu', this.onContextMenu); } } @@ -309,6 +311,8 @@ export default class DraggableCore extends React.Component { removeEvent(thisNode, eventsFor.touch.start, this.onTouchStart, { passive: false, }); + // Remove context menu event listener + removeEvent(thisNode, 'contextmenu', this.onContextMenu); if (this.props.enableUserSelectHack) { scheduleRemoveUserSelectStyles(ownerDocument); } @@ -620,6 +624,12 @@ export default class DraggableCore extends React.Component { return this.handleDragStop(e); }; + onContextMenu: EventHandler = (e) => { + // Prevent context menu for touch elements + e.preventDefault(); + return false; + }; + render(): React.Element { // Reuse the child provided // This makes it flexible to use whatever element is wanted (div, ul, etc) diff --git a/package.json b/package.json index 6da8d55a..e3e28296 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.5.8", + "version": "4.6.0", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From 52fdade9d845cfd07876a636b451074af24e906c Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Thu, 14 Aug 2025 20:33:11 +0200 Subject: [PATCH 20/21] removed context menu on touch 3 --- lib/DraggableCore.js | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 451220f7..13e7f2b3 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -398,6 +398,11 @@ export default class DraggableCore extends React.Component { addUserSelectStyles(ownerDocument); } + // Prevent scrolling during drag + addEvent(ownerDocument, 'touchmove', this.preventScroll, { + passive: false, + }); + this.dragging = true; this.lastX = initialX; // Update lastX and lastY only after drag starts this.lastY = initialY; @@ -594,6 +599,13 @@ export default class DraggableCore extends React.Component { dragEventFor.stop, this.handleDragStop ); + // Remove scroll prevention + removeEvent( + thisNode.ownerDocument, + 'touchmove', + this.preventScroll, + { passive: false } + ); } }; @@ -630,6 +642,13 @@ export default class DraggableCore extends React.Component { return false; }; + preventScroll: EventHandler = (e) => { + // Prevent scrolling during drag + if (this.dragging) { + e.preventDefault(); + } + }; + render(): React.Element { // Reuse the child provided // This makes it flexible to use whatever element is wanted (div, ul, etc) diff --git a/package.json b/package.json index e3e28296..09295d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.6.0", + "version": "4.6.1", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js", From d44534eafe4d403fc3ac278a8af5c0ce84ae47b7 Mon Sep 17 00:00:00 2001 From: Zbyszko Sobecki Date: Tue, 19 Aug 2025 12:05:31 +0200 Subject: [PATCH 21/21] fix: Update version to 4.6.2 and refactor context menu handling in DraggableCore --- .gitignore | 1 + lib/DraggableCore.js | 12 ++++-------- package.json | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 466f3266..6f419fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.iml node_modules/ build/ +.vscode/ \ No newline at end of file diff --git a/lib/DraggableCore.js b/lib/DraggableCore.js index 13e7f2b3..a0ec4cbc 100644 --- a/lib/DraggableCore.js +++ b/lib/DraggableCore.js @@ -422,17 +422,13 @@ export default class DraggableCore extends React.Component { clearTimeout(this.dragTimeout); if (typeof e.persist === 'function') e.persist(); - // Prevent context menu on long touch - const onContextMenu = (contextEvent: MouseTouchEvent) => { - contextEvent.preventDefault(); - }; - addEvent(ownerDocument, 'contextmenu', onContextMenu); + addEvent(ownerDocument, 'contextmenu', this.onContextMenu); const onTouchEnd = () => { log('DraggableCore: touchend detected, canceling drag timeout'); clearTimeout(this.dragTimeout); removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); - removeEvent(ownerDocument, 'contextmenu', onContextMenu); + removeEvent(ownerDocument, 'contextmenu', this.onContextMenu); }; const onTouchMove = (moveEvent: MouseTouchEvent) => { @@ -466,7 +462,7 @@ export default class DraggableCore extends React.Component { removeEvent( ownerDocument, 'contextmenu', - onContextMenu + this.onContextMenu ); } } @@ -479,7 +475,7 @@ export default class DraggableCore extends React.Component { log('DraggableCore: Timeout end, starting drag'); removeEvent(ownerDocument, eventsFor.touch.stop, onTouchEnd); removeEvent(ownerDocument, eventsFor.touch.move, onTouchMove); - removeEvent(ownerDocument, 'contextmenu', onContextMenu); + removeEvent(ownerDocument, 'contextmenu', this.onContextMenu); startDragging(); }, this.props.mobileDragDelay); } else { diff --git a/package.json b/package.json index 09295d40..9ddc9103 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zsobecki_futurum/react-draggable", - "version": "4.6.1", + "version": "4.6.2", "description": "React draggable component with better support for touch events", "main": "build/cjs/cjs.js", "unpkg": "build/web/react-draggable.min.js",