From 7511d51c51029c13e5eec0dc5a938bee743bf224 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 10 Feb 2024 13:18:00 -0500 Subject: [PATCH] Stashing WIP --- .../src/routes/examples/Nested.tsx | 25 ++++++++- .../tests/CursorStyle.spec.ts | 4 +- .../tests/utils/panels.ts | 8 ++- .../src/PanelResizeHandleRegistry.ts | 55 +++++++++++++++++-- packages/react-resizable-panels/src/index.ts | 4 ++ .../utils/rects/getIntersectingRectangle.ts} | 17 +----- .../src/utils/rects/intersects.ts | 10 ++++ .../src/utils/rects/types.ts | 6 ++ 8 files changed, 104 insertions(+), 25 deletions(-) rename packages/{react-resizable-panels-website/tests/utils/rect.ts => react-resizable-panels/src/utils/rects/getIntersectingRectangle.ts} (60%) create mode 100644 packages/react-resizable-panels/src/utils/rects/intersects.ts create mode 100644 packages/react-resizable-panels/src/utils/rects/types.ts diff --git a/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx b/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx index 76d0ce976..4a5bcf4ae 100644 --- a/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx +++ b/packages/react-resizable-panels-website/src/routes/examples/Nested.tsx @@ -23,7 +23,12 @@ export default function NestedRoute() { function Content() { return ( -
+
left
@@ -53,6 +58,24 @@ function Content() {
right
+ +
+ Modal Overlay +
); } diff --git a/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts b/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts index 6128c6e90..20d1c97b6 100644 --- a/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts +++ b/packages/react-resizable-panels-website/tests/CursorStyle.spec.ts @@ -94,9 +94,11 @@ test.describe("cursor style", () => { ); }); - test("should update cursor when dragging intersecting panels (like tmux)", async ({ + test.only("should update cursor when dragging intersecting panels (like tmux)", async ({ page, }) => { + page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); + await goToUrl( page, createElement( diff --git a/packages/react-resizable-panels-website/tests/utils/panels.ts b/packages/react-resizable-panels-website/tests/utils/panels.ts index 080c7d739..fc71ec79e 100644 --- a/packages/react-resizable-panels-website/tests/utils/panels.ts +++ b/packages/react-resizable-panels-website/tests/utils/panels.ts @@ -1,8 +1,11 @@ import { Locator, Page, expect } from "@playwright/test"; -import { assert } from "react-resizable-panels"; +import { + assert, + getIntersectingRectangle, + intersects, +} from "react-resizable-panels"; import { getBodyCursorStyle } from "./cursor"; import { verifyFuzzySizes } from "./verify"; -import { getIntersectingRectangle, intersects } from "./rect"; type Operation = { expectedCursor?: string; @@ -71,6 +74,7 @@ export async function dragResizeIntersecting( const panelGroupRect = (await panelGroup.boundingBox())!; await page.mouse.move(centerPageX, centerPageY); + await new Promise((resolve) => setTimeout(resolve, 5_000)); await expect( await dragHandleOne.getAttribute("data-resize-handle-state") ).toBe("hover"); diff --git a/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts b/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts index 37811b7a2..a2ae244df 100644 --- a/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts +++ b/packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts @@ -1,7 +1,9 @@ +import { compare } from "stacking-order"; import { Direction, ResizeEvent } from "./types"; import { resetGlobalCursorStyle, setGlobalCursorStyle } from "./utils/cursor"; import { getResizeEventCoordinates } from "./utils/events/getResizeEventCoordinates"; import { getInputType } from "./utils/getInputType"; +import { intersects } from "./utils/rects/intersects"; export type ResizeHandlerAction = "down" | "move" | "up"; export type SetResizeHandlerState = ( @@ -75,11 +77,12 @@ export function registerResizeHandle( } function handlePointerDown(event: ResizeEvent) { + const { target } = event; const { x, y } = getResizeEventCoordinates(event); isPointerDown = true; - recalculateIntersectingHandles({ x, y }); + recalculateIntersectingHandles({ target, x, y }); updateListeners(); if (intersectingHandles.length > 0) { @@ -93,10 +96,12 @@ function handlePointerMove(event: ResizeEvent) { const { x, y } = getResizeEventCoordinates(event); if (!isPointerDown) { + const { target } = event; + // Recalculate intersecting handles whenever the pointer moves, except if it has already been pressed // at that point, the handles may not move with the pointer (depending on constraints) // but the same set of active handles should be locked until the pointer is released - recalculateIntersectingHandles({ x, y }); + recalculateIntersectingHandles({ target, x, y }); } updateResizeHandlerStates("move", event); @@ -110,6 +115,7 @@ function handlePointerMove(event: ResizeEvent) { } function handlePointerUp(event: ResizeEvent) { + const { target } = event; const { x, y } = getResizeEventCoordinates(event); panelConstraintFlags.clear(); @@ -119,16 +125,29 @@ function handlePointerUp(event: ResizeEvent) { event.preventDefault(); } - recalculateIntersectingHandles({ x, y }); updateResizeHandlerStates("up", event); + recalculateIntersectingHandles({ target, x, y }); updateCursor(); updateListeners(); } -function recalculateIntersectingHandles({ x, y }: { x: number; y: number }) { +function recalculateIntersectingHandles({ + target, + x, + y, +}: { + target: EventTarget | null; + x: number; + y: number; +}) { intersectingHandles.splice(0); + let targetElement: HTMLElement | null = null; + if (target instanceof HTMLElement) { + targetElement = target; + } + registeredResizeHandlers.forEach((data) => { const { element, hitAreaMargins } = data; const { bottom, left, right, top } = element.getBoundingClientRect(); @@ -137,13 +156,37 @@ function recalculateIntersectingHandles({ x, y }: { x: number; y: number }) { ? hitAreaMargins.coarse : hitAreaMargins.fine; - const intersects = + const eventIntersects = x >= left - margin && x <= right + margin && y >= top - margin && y <= bottom + margin; - if (intersects) { + if (eventIntersects) { + // TRICKY + // We listen for pointers events at the root in order to support hit area margins + // (determining when the pointer is close enough to an element to be considered a "hit") + // Clicking on an element "above" a handle (e.g. a modal) should prevent a hit though + // so at this point we need to compare stacking order of a potentially intersecting drag handle, + // and the element that was actually clicked/touched + // + // Calculating stacking order has a cost, so we should avoid it if possible + // That is why we only check potentially intersecting handles, + // and why we skip if the event target is within the handle's DOM + if ( + targetElement !== null && + element !== targetElement && + !element.contains(targetElement) && + !targetElement.contains(element) && + compare(targetElement, element) > 0 && + intersects( + targetElement.getBoundingClientRect(), + element.getBoundingClientRect() + ) + ) { + return; + } + intersectingHandles.push(data); } }); diff --git a/packages/react-resizable-panels/src/index.ts b/packages/react-resizable-panels/src/index.ts index c382add1c..6ce0018b2 100644 --- a/packages/react-resizable-panels/src/index.ts +++ b/packages/react-resizable-panels/src/index.ts @@ -9,6 +9,8 @@ import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement"; import { getResizeHandleElementIndex } from "./utils/dom/getResizeHandleElementIndex"; import { getResizeHandleElementsForGroup } from "./utils/dom/getResizeHandleElementsForGroup"; import { getResizeHandlePanelIds } from "./utils/dom/getResizeHandlePanelIds"; +import { getIntersectingRectangle } from "./utils/rects/getIntersectingRectangle"; +import { intersects } from "./utils/rects/intersects"; import type { ImperativePanelHandle, @@ -49,6 +51,8 @@ export { // Utility methods assert, + getIntersectingRectangle, + intersects, // DOM helpers getPanelElement, diff --git a/packages/react-resizable-panels-website/tests/utils/rect.ts b/packages/react-resizable-panels/src/utils/rects/getIntersectingRectangle.ts similarity index 60% rename from packages/react-resizable-panels-website/tests/utils/rect.ts rename to packages/react-resizable-panels/src/utils/rects/getIntersectingRectangle.ts index e40c66dc5..146a5f6da 100644 --- a/packages/react-resizable-panels-website/tests/utils/rect.ts +++ b/packages/react-resizable-panels/src/utils/rects/getIntersectingRectangle.ts @@ -1,9 +1,5 @@ -export interface Rectangle { - x: number; - y: number; - width: number; - height: number; -} +import { intersects } from "./intersects"; +import { Rectangle } from "./types"; export function getIntersectingRectangle( rectOne: Rectangle, @@ -29,12 +25,3 @@ export function getIntersectingRectangle( Math.max(rectOne.y, rectTwo.y), }; } - -export function intersects(rectOne: Rectangle, rectTwo: Rectangle): boolean { - return ( - rectOne.x <= rectTwo.x + rectTwo.width && - rectOne.x + rectOne.width >= rectTwo.x && - rectOne.y <= rectTwo.y + rectTwo.height && - rectOne.y + rectOne.height >= rectTwo.y - ); -} diff --git a/packages/react-resizable-panels/src/utils/rects/intersects.ts b/packages/react-resizable-panels/src/utils/rects/intersects.ts new file mode 100644 index 000000000..5710b58ca --- /dev/null +++ b/packages/react-resizable-panels/src/utils/rects/intersects.ts @@ -0,0 +1,10 @@ +import { Rectangle } from "./types"; + +export function intersects(rectOne: Rectangle, rectTwo: Rectangle): boolean { + return ( + rectOne.x <= rectTwo.x + rectTwo.width && + rectOne.x + rectOne.width >= rectTwo.x && + rectOne.y <= rectTwo.y + rectTwo.height && + rectOne.y + rectOne.height >= rectTwo.y + ); +} diff --git a/packages/react-resizable-panels/src/utils/rects/types.ts b/packages/react-resizable-panels/src/utils/rects/types.ts new file mode 100644 index 000000000..006df0753 --- /dev/null +++ b/packages/react-resizable-panels/src/utils/rects/types.ts @@ -0,0 +1,6 @@ +export interface Rectangle { + x: number; + y: number; + width: number; + height: number; +}