Skip to content

Commit 6618f76

Browse files
committed
refactor: migrate source from Flow to TypeScript
Convert lib/*.js (Flow) to TypeScript (.ts/.tsx) and replace the Babel+Flow toolchain with tsup (CJS+ESM+auto-generated .d.ts), keeping webpack for the UMD bundle. Types are now generated from source instead of hand-maintained in typings/index.d.ts, eliminating Flow/TS drift. Public API is unchanged (minor-version-safe): CJS keeps module.exports===Draggable plus .default and .DraggableCore (PR #254/#266); UMD bundle still exposes global ReactDraggable with react/react-dom externals; generated .d.ts matches the previously-shipped surface and the unchanged typings/test.tsx compiles against it. DraggableCore children stays React.ReactNode in the public types (the internal Flow type was ReactElement) with a regression guard in test/typeCompat. Adds tsup.config.ts, root tsconfig.json, eslint @typescript-eslint, native-TS vitest, and extra unit tests (198 pass, up from 131). No version bump or CHANGELOG change.
1 parent 4c6e95a commit 6618f76

30 files changed

Lines changed: 2063 additions & 1264 deletions

.babelrc.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

.flowconfig

Lines changed: 0 additions & 11 deletions
This file was deleted.

Makefile

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,27 @@
44
# Make it parallel
55
MAKEFLAGS += j4
66
export BIN := $(shell yarn bin)
7-
.PHONY: test dev lint build build-cjs build-esm build-web clean install link publish
7+
.PHONY: test dev lint build build-lib build-web clean install link publish
88
.DEFAULT_GOAL := build
99

1010
clean:
1111
rm -rf build
1212
mkdir -p build
1313

1414
lint:
15-
@$(BIN)/flow
16-
@$(BIN)/eslint lib/* lib/utils/*
15+
@$(BIN)/eslint lib
16+
@$(BIN)/tsc --noEmit
1717
@$(BIN)/tsc -p typings
1818

19-
build: clean build-cjs build-esm build-web
19+
# tsup emits cjs + esm + dts into build/cjs (and rewrites build/cjs/cjs.js to the
20+
# legacy module.exports === Draggable shape). webpack emits the UMD global bundle.
21+
# Both depend on `clean` so the dir is reset first even under parallel make (-j).
22+
build: build-lib build-web
2023

21-
build-cjs: $(BIN)
22-
$(BIN)/babel --out-dir ./build/cjs ./lib
24+
build-lib: clean $(BIN)
25+
$(BIN)/tsup
2326

24-
build-web: $(BIN)
27+
build-web: clean $(BIN)
2528
$(BIN)/webpack --mode=production
2629

2730
# Allows usage of `make install`, `make link`

eslint.config.mjs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { defineConfig, globalIgnores } from "eslint/config";
22
import react from "eslint-plugin-react";
33
import globals from "globals";
4-
import babelParser from "@babel/eslint-parser";
4+
import tsParser from "@typescript-eslint/parser";
5+
import tsPlugin from "@typescript-eslint/eslint-plugin";
56
import path from "node:path";
67
import { fileURLToPath } from "node:url";
78
import js from "@eslint/js";
@@ -15,29 +16,34 @@ const compat = new FlatCompat({
1516
allConfig: js.configs.all
1617
});
1718

18-
export default defineConfig([globalIgnores(["build/**/*.js"]), {
19+
export default defineConfig([globalIgnores(["build/**"]), {
20+
files: ["lib/**/*.{ts,tsx}"],
21+
1922
extends: compat.extends("eslint:recommended"),
2023

2124
plugins: {
2225
react,
26+
"@typescript-eslint": tsPlugin,
2327
},
2428

2529
languageOptions: {
2630
globals: {
2731
...globals.browser,
2832
...globals.node,
29-
ReactElement: null,
30-
ReactClass: null,
31-
$Exact: null,
32-
Partial: null,
33-
$Keys: null,
34-
MouseTouchEvent: null,
3533
},
3634

37-
parser: babelParser,
35+
parser: tsParser,
36+
parserOptions: {
37+
ecmaFeatures: {
38+
jsx: true,
39+
},
40+
project: "./tsconfig.json",
41+
},
3842
},
3943

4044
rules: {
45+
...tsPlugin.configs.recommended.rules,
46+
4147
strict: 0,
4248
quotes: [1, "single"],
4349
curly: [1, "multi-line"],
@@ -47,12 +53,14 @@ export default defineConfig([globalIgnores(["build/**/*.js"]), {
4753
"no-use-before-define": [1, "nofunc"],
4854
"no-underscore-dangle": 0,
4955

50-
"no-unused-vars": [1, {
56+
// Use the TS-aware unused-vars rule; disable the base rule it supersedes.
57+
"no-unused-vars": 0,
58+
"@typescript-eslint/no-unused-vars": [1, {
5159
ignoreRestSiblings: true,
5260
}],
5361

5462
"new-cap": 0,
5563
"prefer-const": 1,
5664
semi: 1,
5765
},
58-
}]);
66+
}]);

lib/Draggable.js renamed to lib/Draggable.tsx

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// @flow
21
import * as React from 'react';
32
import PropTypes from 'prop-types';
43
import ReactDOM from 'react-dom';
@@ -10,19 +9,18 @@ import DraggableCore from './DraggableCore';
109
import type {ControlPosition, PositionOffsetControlPosition, DraggableCoreProps, DraggableCoreDefaultProps} from './DraggableCore';
1110
import log from './utils/log';
1211
import type {Bounds, DraggableEventHandler} from './utils/types';
13-
import type {Element as ReactElement} from 'react';
12+
import type {ReactElement} from 'react';
1413

1514
type DraggableState = {
1615
dragging: boolean,
1716
dragged: boolean,
1817
x: number, y: number,
1918
slackX: number, slackY: number,
2019
isElementSVG: boolean,
21-
prevPropsPosition: ?ControlPosition,
20+
prevPropsPosition: ControlPosition | null,
2221
};
2322

24-
export type DraggableDefaultProps = {
25-
...DraggableCoreDefaultProps,
23+
export type DraggableDefaultProps = DraggableCoreDefaultProps & {
2624
axis: 'both' | 'x' | 'y' | 'none',
2725
bounds: Bounds | string | false,
2826
defaultClassName: string,
@@ -32,9 +30,7 @@ export type DraggableDefaultProps = {
3230
scale: number,
3331
};
3432

35-
export type DraggableProps = {
36-
...DraggableCoreProps,
37-
...DraggableDefaultProps,
33+
export type DraggableProps = DraggableCoreProps & DraggableDefaultProps & {
3834
positionOffset: PositionOffsetControlPosition,
3935
position: ControlPosition,
4036
};
@@ -43,11 +39,19 @@ export type DraggableProps = {
4339
// Define <Draggable>
4440
//
4541

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

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

50-
static propTypes: DraggableProps = {
52+
static displayName?: string = 'Draggable';
53+
54+
static propTypes = {
5155
// Accepts all props <DraggableCore> accepts.
5256
...DraggableCore.propTypes,
5357

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

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

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

244252
// React 19 removed ReactDOM.findDOMNode, so nodeRef is now required.
245253
// For backward compatibility with React 18 and earlier, we still support findDOMNode if available.
246-
findDOMNode(): ?HTMLElement {
254+
findDOMNode(): HTMLElement | null {
247255
if (this.props?.nodeRef) {
248256
return this.props.nodeRef.current;
249257
}
250-
// ReactDOM.findDOMNode was removed in React 19
251-
if (typeof ReactDOM.findDOMNode === 'function') {
252-
return ReactDOM.findDOMNode(this);
258+
// ReactDOM.findDOMNode was removed from React 19's type defs (and runtime),
259+
// so access it dynamically to stay compatible with React 18 and earlier.
260+
const legacyReactDOM = ReactDOM as unknown as {
261+
findDOMNode?: (instance: unknown) => HTMLElement | null;
262+
};
263+
if (typeof legacyReactDOM.findDOMNode === 'function') {
264+
return legacyReactDOM.findDOMNode(this) as HTMLElement | null;
253265
}
254266
return null;
255267
}
@@ -336,10 +348,10 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
336348
newState.y = y;
337349
}
338350

339-
this.setState(newState);
351+
this.setState(newState as Pick<DraggableState, keyof DraggableState>);
340352
};
341353

342-
render(): ReactElement<any> {
354+
render(): ReactElement {
343355
const {
344356
axis,
345357
bounds,
@@ -385,8 +397,16 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
385397
style = createCSSTransform(transformOpts, positionOffset);
386398
}
387399

400+
// React.Children.only types its return as ReactElement<unknown>; narrow the
401+
// single child to an element carrying optional DOM style/className props so
402+
// we can read and merge them.
403+
const onlyChild = React.Children.only(children) as ReactElement<{
404+
className?: string,
405+
style?: React.CSSProperties,
406+
}>;
407+
388408
// Mark with class while dragging
389-
const className = clsx((children.props.className || ''), defaultClassName, {
409+
const className = clsx((onlyChild.props.className || ''), defaultClassName, {
390410
[defaultClassNameDragging]: this.state.dragging,
391411
[defaultClassNameDragged]: this.state.dragged
392412
});
@@ -395,11 +415,11 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
395415
// This makes it flexible to use whatever element is wanted (div, ul, etc)
396416
return (
397417
<DraggableCore {...draggableCoreProps} onStart={this.onDragStart} onDrag={this.onDrag} onStop={this.onDragStop}>
398-
{React.cloneElement(React.Children.only(children), {
418+
{React.cloneElement(onlyChild, {
399419
className: className,
400-
style: {...children.props.style, ...style},
420+
style: {...onlyChild.props.style, ...style},
401421
transform: svgTransform
402-
})}
422+
} as Partial<{className: string, style: React.CSSProperties, transform: string | null}>)}
403423
</DraggableCore>
404424
);
405425
}

0 commit comments

Comments
 (0)