From 43fa4b56028478f53dbb65045df1f9f7737c4321 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:23:10 +0200 Subject: [PATCH] fix: frame selection and membership (#11250) fix: element fully overlapping frame --- packages/element/src/frame.ts | 6 ++- packages/element/src/selection.ts | 8 ++-- packages/element/tests/frame.test.tsx | 44 +++++++++++++++++++- packages/excalidraw/tests/selection.test.tsx | 26 ++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts index 787c2692f9..6138633ff4 100644 --- a/packages/element/src/frame.ts +++ b/packages/element/src/frame.ts @@ -19,6 +19,7 @@ import { getElementAbsoluteCoords, doBoundsIntersect, getElementBounds, + boundsContainBounds, } from "./bounds"; import { mutateElement } from "./mutateElement"; import { getBoundTextElement, getContainerElement } from "./textElement"; @@ -101,8 +102,9 @@ export const isElementContainingFrame = ( frame: ExcalidrawFrameLikeElement, elementsMap: ElementsMap, ) => { - return getElementsWithinSelection([frame], element, elementsMap).some( - (e) => e.id === frame.id, + return boundsContainBounds( + getElementBounds(element, elementsMap), + getElementBounds(frame, elementsMap), ); }; diff --git a/packages/element/src/selection.ts b/packages/element/src/selection.ts index 8e9c4e8086..88bb1c2c80 100644 --- a/packages/element/src/selection.ts +++ b/packages/element/src/selection.ts @@ -34,7 +34,6 @@ import { elementOverlapsWithFrame, getContainingFrame, getFrameChildren, - isElementIntersectingFrame, } from "./frame"; import { LinearElementEditor } from "./linearElementEditor"; @@ -170,7 +169,7 @@ export const getElementsWithinSelection = ( const associatedFrame = getContainingFrame(element, elementsMap); if ( associatedFrame && - isElementIntersectingFrame(element, associatedFrame, elementsMap) + elementOverlapsWithFrame(element, associatedFrame, elementsMap) ) { const frameAABB = getElementBounds(associatedFrame, elementsMap); elementAABB = [ @@ -209,10 +208,9 @@ export const getElementsWithinSelection = ( if (boundsContainBounds(selectionBounds, commonAABB)) { if (framesInSelection && isFrameLikeElement(element)) { framesInSelection.add(element.id); - } else { - elementsInSelection.push(element); - continue; } + elementsInSelection.push(element); + continue; } // 2. Handle the case where the label is overlapped by the selection box diff --git a/packages/element/tests/frame.test.tsx b/packages/element/tests/frame.test.tsx index 47f2160ac3..e92267130a 100644 --- a/packages/element/tests/frame.test.tsx +++ b/packages/element/tests/frame.test.tsx @@ -2,6 +2,7 @@ import { convertToExcalidrawElements, Excalidraw, } from "@excalidraw/excalidraw"; +import { arrayToMap } from "@excalidraw/common"; import { API } from "@excalidraw/excalidraw/tests/helpers/api"; import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui"; @@ -10,7 +11,12 @@ import { render, } from "@excalidraw/excalidraw/tests/test-utils"; -import type { ExcalidrawElement } from "../src/types"; +import { elementOverlapsWithFrame } from "../src/frame"; + +import type { + ExcalidrawElement, + ExcalidrawFrameLikeElement, +} from "../src/types"; const { h } = window; const mouse = new Pointer("mouse"); @@ -125,6 +131,26 @@ describe("adding elements to frames", () => { }); }); + it("should treat an element fully containing a frame as overlapping the frame", () => { + const containingRect = API.createElement({ + type: "rectangle", + x: -50, + y: -50, + width: 250, + height: 250, + }); + + API.setElements([containingRect, frame]); + + expect( + elementOverlapsWithFrame( + containingRect, + frame as ExcalidrawFrameLikeElement, + arrayToMap(h.elements), + ), + ).toBe(true); + }); + const commonTestCases = async ( func: typeof resizeFrameOverElement | typeof dragElementIntoFrame, ) => { @@ -415,6 +441,22 @@ describe("adding elements to frames", () => { describe("dragging elements into the frame", async () => { await commonTestCases(dragElementIntoFrame); + it("should add a dragged element fully containing the frame", () => { + const containingRect = API.createElement({ + type: "rectangle", + x: 220, + y: 20, + width: 300, + height: 300, + }); + + API.setElements([frame, containingRect]); + + dragElementIntoFrame(frame, containingRect); + + expect(API.getElement(containingRect).frameId).toBe(frame.id); + }); + it.skip("should drag element inside, duplicate it and keep it in frame", () => { API.setElements([frame, rect2]); diff --git a/packages/excalidraw/tests/selection.test.tsx b/packages/excalidraw/tests/selection.test.tsx index ef0bd46b2c..5fd7a303d7 100644 --- a/packages/excalidraw/tests/selection.test.tsx +++ b/packages/excalidraw/tests/selection.test.tsx @@ -615,6 +615,32 @@ describe("box-selection overlap mode", () => { assertSelectedElements([]); }); + + it("should not select a framed element when selection only overlaps its clipped-out outline", () => { + const frame = API.createElement({ + type: "frame", + x: 100, + y: 100, + width: 100, + height: 100, + }); + const rect1 = API.createElement({ + type: "rectangle", + x: 50, + y: 50, + width: 200, + height: 200, + frameId: frame.id, + backgroundColor: "red", + fillStyle: "solid", + }); + + API.setElements([frame, rect1]); + + boxSelect(40, 170, 70, 220); + + assertSelectedElements([]); + }); }); describe("inner box-selection", () => {