fix: frame selection and membership (#11250)

fix: element fully overlapping frame
This commit is contained in:
David Luzar
2026-04-28 18:23:10 +02:00
committed by GitHub
parent 2e1a529c67
commit 43fa4b5602
4 changed files with 76 additions and 8 deletions
+4 -2
View File
@@ -19,6 +19,7 @@ import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
doBoundsIntersect, doBoundsIntersect,
getElementBounds, getElementBounds,
boundsContainBounds,
} from "./bounds"; } from "./bounds";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getBoundTextElement, getContainerElement } from "./textElement"; import { getBoundTextElement, getContainerElement } from "./textElement";
@@ -101,8 +102,9 @@ export const isElementContainingFrame = (
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) => {
return getElementsWithinSelection([frame], element, elementsMap).some( return boundsContainBounds(
(e) => e.id === frame.id, getElementBounds(element, elementsMap),
getElementBounds(frame, elementsMap),
); );
}; };
+3 -5
View File
@@ -34,7 +34,6 @@ import {
elementOverlapsWithFrame, elementOverlapsWithFrame,
getContainingFrame, getContainingFrame,
getFrameChildren, getFrameChildren,
isElementIntersectingFrame,
} from "./frame"; } from "./frame";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
@@ -170,7 +169,7 @@ export const getElementsWithinSelection = (
const associatedFrame = getContainingFrame(element, elementsMap); const associatedFrame = getContainingFrame(element, elementsMap);
if ( if (
associatedFrame && associatedFrame &&
isElementIntersectingFrame(element, associatedFrame, elementsMap) elementOverlapsWithFrame(element, associatedFrame, elementsMap)
) { ) {
const frameAABB = getElementBounds(associatedFrame, elementsMap); const frameAABB = getElementBounds(associatedFrame, elementsMap);
elementAABB = [ elementAABB = [
@@ -209,10 +208,9 @@ export const getElementsWithinSelection = (
if (boundsContainBounds(selectionBounds, commonAABB)) { if (boundsContainBounds(selectionBounds, commonAABB)) {
if (framesInSelection && isFrameLikeElement(element)) { if (framesInSelection && isFrameLikeElement(element)) {
framesInSelection.add(element.id); 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 // 2. Handle the case where the label is overlapped by the selection box
+43 -1
View File
@@ -2,6 +2,7 @@ import {
convertToExcalidrawElements, convertToExcalidrawElements,
Excalidraw, Excalidraw,
} from "@excalidraw/excalidraw"; } from "@excalidraw/excalidraw";
import { arrayToMap } from "@excalidraw/common";
import { API } from "@excalidraw/excalidraw/tests/helpers/api"; import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui"; import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
@@ -10,7 +11,12 @@ import {
render, render,
} from "@excalidraw/excalidraw/tests/test-utils"; } 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 { h } = window;
const mouse = new Pointer("mouse"); 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 ( const commonTestCases = async (
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame, func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
) => { ) => {
@@ -415,6 +441,22 @@ describe("adding elements to frames", () => {
describe("dragging elements into the frame", async () => { describe("dragging elements into the frame", async () => {
await commonTestCases(dragElementIntoFrame); 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", () => { it.skip("should drag element inside, duplicate it and keep it in frame", () => {
API.setElements([frame, rect2]); API.setElements([frame, rect2]);
@@ -615,6 +615,32 @@ describe("box-selection overlap mode", () => {
assertSelectedElements([]); 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", () => { describe("inner box-selection", () => {