From 7b065b9c2d9a8f1494fce94cf00040ba467737a7 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Thu, 23 Jul 2026 16:04:40 +0200 Subject: [PATCH] Replace `react-resizable` `react-resizable` depends on `react-draggable`, whose browser bundle references `process.env.DRAGGABLE_DEBUG`. Vite's development dependency optimizer leaves that Node-specific expression in the browser bundle, so starting a resize with `npm run serve` throws `ReferenceError: process is not defined`. This is tracked upstream in [react-grid-layout/react-draggable#806](https://github.com/react-grid-layout/react-draggable/issues/806). Replace the dependency with a focused pointer-event implementation for the east, south, and southeast layout resize handles. Preserve the existing minimum dimensions and continue tracking drags outside the narrow handle hit areas. Clean up document listeners when a drag ends, is canceled, loses focus, or the component unmounts. Remove `react-resizable` and its now-unused `react-draggable` transitive dependency. --- package-lock.json | 29 ----- package.json | 1 - src/css/Layout.module.css | 10 +- src/ui/components/Layout.tsx | 56 ++-------- src/ui/components/LayoutResizeHandles.tsx | 126 ++++++++++++++++++++++ 5 files changed, 138 insertions(+), 84 deletions(-) create mode 100644 src/ui/components/LayoutResizeHandles.tsx diff --git a/package-lock.json b/package-lock.json index c9dfa6e18..60bc16d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "markdown-it": "^14.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable": "^4.0.2", "react-sidebar": "^3.0.2", "react-toastify": "^11.0.3" }, @@ -6244,40 +6243,12 @@ "react": "^19.2.8" } }, - "node_modules/react-draggable": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.7.0.tgz", - "integrity": "sha512-kTpANmKWVnFXiZ76Ag2ZowiFStuBYnJ606PI1TbUsOg29/400/JNIxI9+CuenhiAqFuXWJffz6F4UI3R51kUug==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/react-resizable": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-4.0.2.tgz", - "integrity": "sha512-jZD9ghYRmyJCw0+awYctSZ+9pmX1WXQvzDrTovELYc8obC/BShTI2r4c14LIVzeQ+vJZNb0yKM7bG2eqv7Vfyg==", - "license": "MIT", - "dependencies": { - "prop-types": "15.x", - "react-draggable": "^4.5.0" - }, - "peerDependencies": { - "react": ">= 16.3", - "react-dom": ">= 16.3" - } - }, "node_modules/react-sidebar": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/react-sidebar/-/react-sidebar-3.0.2.tgz", diff --git a/package.json b/package.json index 7a4fe4530..0904cbff5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "markdown-it": "^14.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-resizable": "^4.0.2", "react-sidebar": "^3.0.2", "react-toastify": "^11.0.3" }, diff --git a/src/css/Layout.module.css b/src/css/Layout.module.css index e2a3544d5..45e01f222 100644 --- a/src/css/Layout.module.css +++ b/src/css/Layout.module.css @@ -3,11 +3,13 @@ top: 0; width: inherit; height: inherit; +} - :global(.react-resizable) { - width: 0 !important; - height: 0 !important; - } +.handleEast, +.handleSouth, +.handleSouthEast { + touch-action: none; + user-select: none; } .handleEast { diff --git a/src/ui/components/Layout.tsx b/src/ui/components/Layout.tsx index 1bdd73c47..f2111b3ef 100644 --- a/src/ui/components/Layout.tsx +++ b/src/ui/components/Layout.tsx @@ -1,12 +1,10 @@ import * as React from "react"; -import { ResizableBox } from "react-resizable"; import { type LayoutStateRef } from "../../livesplit-core"; import { type WebRenderer } from "../../livesplit-core/livesplit_core"; import AutoRefresh from "../../util/AutoRefresh"; import { type UrlCache } from "../../util/UrlCache"; import { type GeneralSettings } from "../views/MainSettings"; - -import classes from "../../css/Layout.module.css"; +import { LayoutResizeHandles } from "./LayoutResizeHandles"; export function Layout({ getState, @@ -70,53 +68,11 @@ export function Layout({ }} /> {allowResize && ( -
- e.stopPropagation()} - className={classes.handleEast} - /> - } - onResize={(_event, data) => - onResize(data.size.width, data.size.height) - } - /> - e.stopPropagation()} - className={classes.handleSouth} - /> - } - onResize={(_event, data) => - onResize(data.size.width, data.size.height) - } - /> - e.stopPropagation()} - className={classes.handleSouthEast} - /> - } - onResize={(_event, data) => - onResize(data.size.width, data.size.height) - } - /> -
+ )} diff --git a/src/ui/components/LayoutResizeHandles.tsx b/src/ui/components/LayoutResizeHandles.tsx new file mode 100644 index 000000000..8ce101b9c --- /dev/null +++ b/src/ui/components/LayoutResizeHandles.tsx @@ -0,0 +1,126 @@ +import * as React from "react"; + +import classes from "../../css/Layout.module.css"; + +const MINIMUM_WIDTH = 100; +const MINIMUM_HEIGHT = 40; + +type ResizeAxis = "horizontal" | "vertical" | "both"; + +export function LayoutResizeHandles({ + width, + height, + onResize, +}: { + width: number; + height: number; + onResize: (width: number, height: number) => void; +}) { + const stopListening = React.useRef<(() => void) | null>(null); + + React.useEffect( + () => () => { + stopListening.current?.(); + }, + [], + ); + + const startResize = + (axis: ResizeAxis) => (event: React.PointerEvent) => { + // Ignore secondary pointers and non-primary mouse buttons. Pointer + // events cover mouse, touch, and pen input without needing separate + // compatibility paths for each input type. + if (!event.isPrimary || event.button !== 0) { + return; + } + + const pointerId = event.pointerId; + const startX = event.clientX; + const startY = event.clientY; + let lastWidth = width; + let lastHeight = height; + const ownerDocument = event.currentTarget.ownerDocument; + const ownerWindow = ownerDocument.defaultView; + + // Listen on the document while dragging so the hit area can remain + // deliberately narrow without losing movement as soon as the + // pointer leaves it. The cleanup ref also handles the component + // being unmounted or the window losing focus mid-drag. + function resize(pointerEvent: PointerEvent) { + if (pointerEvent.pointerId !== pointerId) { + return; + } + + const newWidth = + axis === "vertical" + ? width + : Math.max( + MINIMUM_WIDTH, + width + pointerEvent.clientX - startX, + ); + const newHeight = + axis === "horizontal" + ? height + : Math.max( + MINIMUM_HEIGHT, + height + pointerEvent.clientY - startY, + ); + + if (newWidth !== lastWidth || newHeight !== lastHeight) { + lastWidth = newWidth; + lastHeight = newHeight; + onResize(newWidth, newHeight); + } + pointerEvent.preventDefault(); + } + + function stopResize(pointerEvent: PointerEvent) { + if (pointerEvent.pointerId === pointerId) { + cleanup(); + } + } + + function cleanup() { + ownerDocument.removeEventListener("pointermove", resize); + ownerDocument.removeEventListener("pointerup", stopResize); + ownerDocument.removeEventListener("pointercancel", stopResize); + ownerWindow?.removeEventListener("blur", cleanup); + stopListening.current = null; + } + + stopListening.current?.(); + stopListening.current = cleanup; + ownerDocument.addEventListener("pointermove", resize, { + passive: false, + }); + ownerDocument.addEventListener("pointerup", stopResize); + ownerDocument.addEventListener("pointercancel", stopResize); + ownerWindow?.addEventListener("blur", cleanup); + event.preventDefault(); + }; + + const handleProps = { + "aria-hidden": true, + onClick: (event: React.MouseEvent) => event.stopPropagation(), + } as const; + + return ( +
+
+
+
+
+ ); +}