Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions .babelrc.js

This file was deleted.

11 changes: 0 additions & 11 deletions .flowconfig

This file was deleted.

6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ jobs:
- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Flow check
run: yarn flow
- name: TypeScript check (source)
run: yarn tsc --noEmit

- name: TypeScript check
- name: TypeScript check (public typings)
run: yarn tsc -p typings
20 changes: 13 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,30 @@
# Make it parallel
MAKEFLAGS += j4
export BIN := $(shell yarn bin)
.PHONY: test dev lint build build-cjs build-esm build-web clean install link publish
.PHONY: test dev lint build build-lib build-web clean install link publish
.DEFAULT_GOAL := build

clean:
rm -rf build
mkdir -p build

lint:
@$(BIN)/flow
@$(BIN)/eslint lib/* lib/utils/*
@$(BIN)/eslint lib
@$(BIN)/tsc --noEmit
@$(BIN)/tsc -p typings
Comment on lines 14 to 17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make lint deterministic against generated declarations.

Line 17 now depends on generated build/cjs/cjs.d.ts (via typings/tsconfig.json), but lint does not ensure that artifact exists. This can fail on clean environments and pass only on machines with stale build output.

🔧 Proposed fix
-lint:
+lint: build-lib
 	@$(BIN)/eslint lib
 	@$(BIN)/tsc --noEmit
 	@$(BIN)/tsc -p typings
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lint:
@$(BIN)/flow
@$(BIN)/eslint lib/* lib/utils/*
@$(BIN)/eslint lib
@$(BIN)/tsc --noEmit
@$(BIN)/tsc -p typings
lint: build-lib
@$(BIN)/eslint lib
@$(BIN)/tsc --noEmit
@$(BIN)/tsc -p typings
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 14 - 17, The lint target currently runs "tsc -p
typings" but that tsconfig depends on a generated declaration
(build/cjs/cjs.d.ts), causing non-deterministic failures; update the Makefile's
lint target (the lint rule that runs "$(BIN)/eslint lib", "$(BIN)/tsc --noEmit",
"$(BIN)/tsc -p typings") to first produce the required declarations (either by
invoking the build step that creates build/cjs/cjs.d.ts or by running tsc with
emitDeclarationOnly against the appropriate tsconfig) so that the subsequent
"$(BIN)/tsc -p typings" always has the generated artifact available. Ensure the
change references the same lint target and the "$(BIN)/tsc -p typings"
invocation so reviewers can locate and verify the fix.


build: clean build-cjs build-esm build-web
# tsup emits cjs + esm + dts into build/cjs (and rewrites build/cjs/cjs.js to the
# legacy module.exports === Draggable shape). webpack emits the UMD global bundle.
# Both depend on `clean` so the dir is reset first even under parallel make (-j).
# The recipe runs after both prerequisites complete and verifies the published
# CJS/UMD contracts (see scripts/verify-build.cjs).
build: build-lib build-web
@node scripts/verify-build.cjs

build-cjs: $(BIN)
$(BIN)/babel --out-dir ./build/cjs ./lib
build-lib: clean $(BIN)
$(BIN)/tsup

build-web: $(BIN)
build-web: clean $(BIN)
$(BIN)/webpack --mode=production

# Allows usage of `make install`, `make link`
Expand Down
30 changes: 19 additions & 11 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { defineConfig, globalIgnores } from "eslint/config";
import react from "eslint-plugin-react";
import globals from "globals";
import babelParser from "@babel/eslint-parser";
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
Expand All @@ -15,29 +16,34 @@ const compat = new FlatCompat({
allConfig: js.configs.all
});

export default defineConfig([globalIgnores(["build/**/*.js"]), {
export default defineConfig([globalIgnores(["build/**"]), {
files: ["lib/**/*.{ts,tsx}"],

extends: compat.extends("eslint:recommended"),

plugins: {
react,
"@typescript-eslint": tsPlugin,
},

languageOptions: {
globals: {
...globals.browser,
...globals.node,
ReactElement: null,
ReactClass: null,
$Exact: null,
Partial: null,
$Keys: null,
MouseTouchEvent: null,
},

parser: babelParser,
parser: tsParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
project: "./tsconfig.json",
},
},

rules: {
...tsPlugin.configs.recommended.rules,

strict: 0,
quotes: [1, "single"],
curly: [1, "multi-line"],
Expand All @@ -47,12 +53,14 @@ export default defineConfig([globalIgnores(["build/**/*.js"]), {
"no-use-before-define": [1, "nofunc"],
"no-underscore-dangle": 0,

"no-unused-vars": [1, {
// Use the TS-aware unused-vars rule; disable the base rule it supersedes.
"no-unused-vars": 0,
"@typescript-eslint/no-unused-vars": [1, {
ignoreRestSiblings: true,
}],

"new-cap": 0,
"prefer-const": 1,
semi: 1,
},
}]);
}]);
68 changes: 44 additions & 24 deletions lib/Draggable.js → lib/Draggable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @flow
import * as React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
Expand All @@ -10,19 +9,18 @@ import DraggableCore 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 {ReactElement} from 'react';

type DraggableState = {
dragging: boolean,
dragged: boolean,
x: number, y: number,
slackX: number, slackY: number,
isElementSVG: boolean,
prevPropsPosition: ?ControlPosition,
prevPropsPosition: ControlPosition | null,
};

export type DraggableDefaultProps = {
...DraggableCoreDefaultProps,
export type DraggableDefaultProps = DraggableCoreDefaultProps & {
axis: 'both' | 'x' | 'y' | 'none',
bounds: Bounds | string | false,
defaultClassName: string,
Expand All @@ -32,9 +30,7 @@ export type DraggableDefaultProps = {
scale: number,
};

export type DraggableProps = {
...DraggableCoreProps,
...DraggableDefaultProps,
export type DraggableProps = DraggableCoreProps & DraggableDefaultProps & {
positionOffset: PositionOffsetControlPosition,
position: ControlPosition,
};
Expand All @@ -43,11 +39,19 @@ export type DraggableProps = {
// Define <Draggable>
//

class Draggable extends React.Component<DraggableProps, DraggableState> {
// Public-facing prop shape: every prop is optional for consumers because the
// required ones are supplied by `defaultProps`. This reproduces the historical
// hand-written declaration `React.Component<Partial<DraggableProps>, {}>` so the
// auto-generated .d.ts stays API-compatible with the old typings.
class Draggable extends React.Component<Partial<DraggableProps>, DraggableState> {

static displayName: ?string = 'Draggable';
// Internally, defaultProps guarantees every prop is present at runtime, so we
// narrow `this.props` back to the fully-resolved type for type-safe access.
declare props: DraggableProps;

static propTypes: DraggableProps = {
static displayName?: string = 'Draggable';

static propTypes = {
// Accepts all props <DraggableCore> accepts.
...DraggableCore.propTypes,

Expand Down Expand Up @@ -166,7 +170,11 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
transform: dontSetMe
};

static defaultProps: DraggableDefaultProps = {
// Typed as the full `DraggableProps` (not just the default-provided subset) so
// React's JSX LibraryManagedAttributes treats EVERY prop as optional for
// consumers, matching the historical hand-written typings. At runtime only the
// default-able props are actually populated.
static defaultProps: DraggableProps = {
...DraggableCore.defaultProps,
axis: 'both',
bounds: false,
Expand All @@ -175,11 +183,11 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
defaultClassNameDragged: 'react-draggable-dragged',
defaultPosition: {x: 0, y: 0},
scale: 1
};
} as unknown as DraggableProps;

// React 16.3+
// Arity (props, state)
static getDerivedStateFromProps({position}: DraggableProps, {prevPropsPosition}: DraggableState): ?Partial<DraggableState> {
static getDerivedStateFromProps({position}: DraggableProps, {prevPropsPosition}: DraggableState): Partial<DraggableState> | null {
// Set x/y if a new position is provided in props that is different than the previous.
if (
position &&
Expand Down Expand Up @@ -243,13 +251,17 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {

// React 19 removed ReactDOM.findDOMNode, so nodeRef is now required.
// For backward compatibility with React 18 and earlier, we still support findDOMNode if available.
findDOMNode(): ?HTMLElement {
findDOMNode(): HTMLElement | null {
if (this.props?.nodeRef) {
return this.props.nodeRef.current;
}
// ReactDOM.findDOMNode was removed in React 19
if (typeof ReactDOM.findDOMNode === 'function') {
return ReactDOM.findDOMNode(this);
// ReactDOM.findDOMNode was removed from React 19's type defs (and runtime),
// so access it dynamically to stay compatible with React 18 and earlier.
const legacyReactDOM = ReactDOM as unknown as {
findDOMNode?: (instance: unknown) => HTMLElement | null;
};
if (typeof legacyReactDOM.findDOMNode === 'function') {
return legacyReactDOM.findDOMNode(this) as HTMLElement | null;
}
return null;
}
Expand Down Expand Up @@ -336,10 +348,10 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
newState.y = y;
}

this.setState(newState);
this.setState(newState as DraggableState);
};

render(): ReactElement<any> {
render(): ReactElement {
const {
axis,
bounds,
Expand Down Expand Up @@ -385,8 +397,16 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
style = createCSSTransform(transformOpts, positionOffset);
}

// React.Children.only types its return as ReactElement<unknown>; narrow the
// single child to an element carrying optional DOM style/className props so
// we can read and merge them.
const onlyChild = React.Children.only(children) as ReactElement<{
className?: string,
style?: React.CSSProperties,
}>;

// Mark with class while dragging
const className = clsx((children.props.className || ''), defaultClassName, {
const className = clsx((onlyChild.props.className || ''), defaultClassName, {
[defaultClassNameDragging]: this.state.dragging,
[defaultClassNameDragged]: this.state.dragged
});
Expand All @@ -395,11 +415,11 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
// This makes it flexible to use whatever element is wanted (div, ul, etc)
return (
<DraggableCore {...draggableCoreProps} onStart={this.onDragStart} onDrag={this.onDrag} onStop={this.onDragStop}>
{React.cloneElement(React.Children.only(children), {
{React.cloneElement(onlyChild, {
className: className,
style: {...children.props.style, ...style},
style: {...onlyChild.props.style, ...style},
transform: svgTransform
})}
} as Partial<{className: string, style: React.CSSProperties, transform: string | null}>)}
</DraggableCore>
);
}
Expand Down
Loading
Loading