diff --git a/package-lock.json b/package-lock.json index c9dfa6e1..60bc16d6 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 7a4fe453..0904cbff 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 e2a3544d..45e01f22 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 1bdd73c4..f2111b3e 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 00000000..8ce101b9 --- /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 ( +
+
+
+
+
+ ); +}