From 09db5c4bbd079349412cebc6f8a897947d538359 Mon Sep 17 00:00:00 2001 From: Balint Gabor <127662+gbalint@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:18:03 +0200 Subject: [PATCH] Fix selection issues (#6128) **Problem:** 1. In a selected scene, dragging an element should drag the scene not the element 2. In a selected scene, clicking on an element should select the element 3. In a selected scene, dragging an element under the threshold not move anything, but select the element 4. In a selected scene, cmd-dragging an element should drag the element not the scene 5. In a selected scene, cmd-clicking an element should select the element 6. In a selected scene, cmd-dragging an element under the drag threshold should not move anything, but select the element **Fix:** To fix 1-2-3 we need to make sure that dragging below the threshold is handled as a "click", so the selection happens on mouse up. To fix 4-5-6 we need to make sure the cmd-mousedown already selects the element, which guarantees that the following dragging operates on the element (and not the scene) Meanwhile, the failing tests uncovered that we have an extra bug: you can deselect a selected item if you click on a non-visible (e.g. cropped) part of it. To fix it I modified the behavior of `findValidTarget` in `dont-prefer-selected` mode: it means we should change our selection to something different if possible, but we should still not deselect it. To make this clearer I renamed `don't-prefer-selected` to `prefer-more-specific-selection` As I fixed the tests I had to realize that our dragging helper functions are not good enough: we can specifiy modifier keys, but we can not specify separate modifier keys for the mouse down and for the mouse move events. Which is necessary because e.g. cmd changes the behavior differently in the two cases: cmd on mouse down changes how selection work, so it changes what will be dragged, while cmd on mouse move changes which strategy will be applied (cmd allows reparenting). **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Preview mode Fixes https://github.com/concrete-utopia/utopia/issues/6012 --- ...eparent-strategy-helpers.spec.browser2.tsx | 4 +- .../absolute-move-strategy.spec.browser2.tsx | 18 +- ...solute-reparent-strategy.spec.browser2.tsx | 12 +- ...solute-and-move-strategy.spec.browser2.tsx | 15 +- .../select-mode/select-mode-hooks.tsx | 106 +++++++---- .../select-mode/select-mode.spec.browser2.tsx | 168 +++++++++++++++++- .../controls/selection-area.spec.browser2.tsx | 9 +- .../text-edit-mode/text-edit-mode-hooks.tsx | 2 +- .../canvas/event-helpers.test-utils.tsx | 9 +- 9 files changed, 285 insertions(+), 58 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx index 2878285b03f9..ec806d991cf4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-flex-reparent-strategy-helpers.spec.browser2.tsx @@ -37,6 +37,7 @@ async function dragElement( modifiers: Modifiers, includeMouseUp: boolean, midDragCallback?: () => Promise<void>, + mouseDownModifiers?: Modifiers, ): Promise<void> { const targetElement = renderResult.renderedDOM.getByTestId(targetTestId) const targetElementBounds = targetElement.getBoundingClientRect() @@ -51,10 +52,11 @@ async function dragElement( if (includeMouseUp) { await mouseDragFromPointToPoint(canvasControlsLayer, startPoint, endPoint, { modifiers: modifiers, + mouseDownModifiers: mouseDownModifiers, midDragCallback: midDragCallback, }) } else { - await mouseDownAtPoint(canvasControlsLayer, startPoint, { modifiers: modifiers }) + await mouseDownAtPoint(canvasControlsLayer, startPoint) await mouseMoveToPoint(canvasControlsLayer, endPoint, { modifiers: modifiers, eventOptions: { buttons: 1 }, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx index 6c18d89c5a5f..537d4d8ca82c 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-move-strategy.spec.browser2.tsx @@ -52,6 +52,7 @@ async function dragByPixels( delta: WindowPoint, testid: string, modifiers: Modifiers = emptyModifiers, + mouseDownModifiers?: Modifiers, ) { const targetElement = editor.renderedDOM.getByTestId(testid) const targetElementBounds = targetElement.getBoundingClientRect() @@ -60,6 +61,7 @@ async function dragByPixels( await mouseDragFromPointWithDelta(canvasControlsLayer, targetElementCenter, delta, { modifiers, + mouseDownModifiers, midDragCallback: async () => { NO_OP() }, @@ -72,8 +74,9 @@ async function dragElement( dragDelta: WindowPoint, modifiers: Modifiers, midDragCallback?: () => Promise<void>, + mouseDownModifiers?: Modifiers, ): Promise<void> { - await mouseDownAtPoint(canvasControlsLayer, startPoint, { modifiers: cmdModifier }) + await mouseDownAtPoint(canvasControlsLayer, startPoint, { modifiers: mouseDownModifiers }) await mouseDragFromPointWithDelta(canvasControlsLayer, startPoint, dragDelta, { modifiers, midDragCallback, @@ -316,7 +319,7 @@ describe('Absolute Move Strategy', () => { await selectComponentsForTest(editor, [targetElement]) - await dragByPixels(editor, windowPoint({ x: 15, y: 15 }), 'bbb', cmdModifier) + await dragByPixels(editor, windowPoint({ x: 15, y: 15 }), 'bbb') expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( makeTestProjectCodeWithSnippet( @@ -947,8 +950,6 @@ describe('Absolute Move Strategy', () => { EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:aaa/bbb/ccc`), ]) - // await wait(10000) - await dragByPixels(editor, windowPoint({ x: 15, y: 15 }), 'bbb') expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( @@ -1215,7 +1216,14 @@ describe('Absolute Move Strategy', () => { const startPoint = windowPoint({ x: targetElementBounds.x + 5, y: targetElementBounds.y + 5 }) const dragDelta = windowPoint({ x: 40, y: 25 }) - await dragElement(canvasControlsLayer, startPoint, dragDelta, emptyModifiers) + await dragElement( + canvasControlsLayer, + startPoint, + dragDelta, + cmdModifier, + undefined, + cmdModifier, + ) await renderResult.getDispatchFollowUpActionsFinished() expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual( diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx index 49083754b234..20884f475651 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-reparent-strategy.spec.browser2.tsx @@ -72,6 +72,7 @@ async function dragElement( modifiers: Modifiers, checkCursor: CheckCursor | null, midDragCallback: (() => Promise<void>) | null, + mouseDownModifiers?: Modifiers, ) { const targetElement = renderResult.renderedDOM.getByTestId(targetTestId) const targetElementBounds = targetElement.getBoundingClientRect() @@ -90,6 +91,7 @@ async function dragElement( await mouseClickAtPoint(canvasControlsLayer, startPoint, { modifiers: cmdModifier }) await mouseDragFromPointWithDelta(canvasControlsLayer, startPoint, dragDelta, { modifiers: modifiers, + mouseDownModifiers: mouseDownModifiers, midDragCallback: combinedMidDragCallback, }) } @@ -1325,7 +1327,15 @@ export var ${BakedInStoryboardVariableName} = (props) => { ) const dragDelta = windowPoint({ x: 50, y: 0 }) - await dragElement(renderResult, 'child-1', dragDelta, cmdModifier, null, null) + await dragElement( + renderResult, + 'child-1', + dragDelta, + cmdModifier, + null, + null, + cmdModifier, + ) await renderResult.getDispatchFollowUpActionsFinished() diff --git a/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx index e711710d7e34..014c148bdef7 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/convert-to-absolute-and-move-strategy.spec.browser2.tsx @@ -55,6 +55,7 @@ import { import { selectComponentsForTest } from '../../../../utils/utils.test-utils' import { ConvertToAbsoluteAndMoveStrategyID } from './convert-to-absolute-and-move-strategy' import CanvasActions from '../../canvas-actions' +import { ctrlModifier } from '../../../../utils/modifiers' const complexProject = () => { const code = ` @@ -1140,12 +1141,7 @@ describe('Convert to absolute/escape hatch', () => { y: elementBounds.y + 10, }, { - modifiers: { - alt: false, - cmd: true, - ctrl: true, - shift: false, - }, + modifiers: ctrlModifier, }, ) @@ -1805,12 +1801,7 @@ describe('Escape hatch strategy on awkward project', () => { y: 15, }, { - modifiers: { - alt: false, - cmd: true, - ctrl: true, - shift: false, - }, + modifiers: ctrlModifier, }, ) diff --git a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx index 33dabf21c3f4..2703490e0f4b 100644 --- a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx +++ b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx @@ -303,7 +303,7 @@ function getCandidateSelectableViews( export function useFindValidTarget(): ( selectableViews: Array<ElementPath>, mousePoint: WindowPoint | null, - preferAlreadySelected: 'prefer-selected' | 'dont-prefer-selected', + preferAlreadySelected: 'prefer-selected' | 'prefer-more-specific-selection', ) => { elementPath: ElementPath isSelected: boolean @@ -325,7 +325,7 @@ export function useFindValidTarget(): ( ( selectableViews: Array<ElementPath>, mousePoint: WindowPoint | null, - preferAlreadySelected: 'prefer-selected' | 'dont-prefer-selected', + preferAlreadySelected: 'prefer-selected' | 'prefer-more-specific-selection', ) => { const { selectedViews, @@ -336,26 +336,43 @@ export function useFindValidTarget(): ( elementPathTree, allElementProps, } = storeRef.current - const validElementMouseOver: ElementPath | null = - preferAlreadySelected === 'prefer-selected' - ? getSelectionOrValidTargetAtPoint( - componentMetadata, - selectedViews, - hiddenInstances, - selectableViews, - mousePoint, - canvasScale, - canvasOffset, - elementPathTree, - allElementProps, - ) - : getValidTargetAtPoint( - selectableViews, - mousePoint, - canvasScale, - canvasOffset, - componentMetadata, - ) + const validElementMouseOver: ElementPath | null = (() => { + if (preferAlreadySelected === 'prefer-selected') { + return getSelectionOrValidTargetAtPoint( + componentMetadata, + selectedViews, + hiddenInstances, + selectableViews, + mousePoint, + canvasScale, + canvasOffset, + elementPathTree, + allElementProps, + ) + } + const newSelection = getValidTargetAtPoint( + selectableViews, + mousePoint, + canvasScale, + canvasOffset, + componentMetadata, + ) + if (newSelection != null) { + return newSelection + } + return getSelectionOrValidTargetAtPoint( + componentMetadata, + selectedViews, + hiddenInstances, + selectableViews, + mousePoint, + canvasScale, + canvasOffset, + elementPathTree, + allElementProps, + ) + })() + const validElementPath: ElementPath | null = validElementMouseOver != null ? validElementMouseOver : null if (validElementPath != null) { @@ -452,7 +469,11 @@ export function useCalculateHighlightedViews( return React.useCallback( (targetPoint: WindowPoint, eventCmdPressed: boolean) => { const selectableViews: Array<ElementPath> = getHighlightableViews(eventCmdPressed, false) - const validElementPath = findValidTarget(selectableViews, targetPoint, 'dont-prefer-selected') + const validElementPath = findValidTarget( + selectableViews, + targetPoint, + 'prefer-more-specific-selection', + ) const validElementPathForHover = findValidTarget( selectableViews, targetPoint, @@ -528,14 +549,15 @@ export function useHighlightCallbacks( function getPreferredSelectionForEvent( eventType: 'mousedown' | 'mouseup' | string, isDoubleClick: boolean, -): 'prefer-selected' | 'dont-prefer-selected' { + cmdModifier: boolean, +): 'prefer-selected' | 'prefer-more-specific-selection' { // mousedown keeps selection on a single click to allow dragging overlapping elements and selection happens on mouseup // with continuous clicking mousedown should select switch (eventType) { case 'mousedown': - return isDoubleClick ? 'dont-prefer-selected' : 'prefer-selected' + return isDoubleClick || cmdModifier ? 'prefer-more-specific-selection' : 'prefer-selected' case 'mouseup': - return isDoubleClick ? 'prefer-selected' : 'dont-prefer-selected' + return isDoubleClick ? 'prefer-selected' : 'prefer-more-specific-selection' default: return 'prefer-selected' } @@ -561,6 +583,7 @@ function useSelectOrLiveModeSelectAndHover( ) const windowToCanvasCoordinates = useWindowToCanvasCoordinates() const interactionSessionHappened = React.useRef(false) + const draggedOverThreshold = React.useRef(false) const didWeHandleMouseDown = React.useRef(false) // this is here to avoid selecting when closing text editing const { onMouseMove: innerOnMouseMove } = useHighlightCallbacks( @@ -582,6 +605,14 @@ function useSelectOrLiveModeSelectAndHover( editorStoreRef.current.editor.canvas.interactionSession, editorStoreRef.current.editor.keysPressed['space'], ) || event.buttons === 4 + + const draggingOverThreshold = + editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.type === 'DRAG' + ? editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.drag != null + : false + + draggedOverThreshold.current = draggedOverThreshold.current || draggingOverThreshold + if (isDragIntention) { return } @@ -601,20 +632,23 @@ function useSelectOrLiveModeSelectAndHover( (event: React.MouseEvent<HTMLDivElement>) => { const isLeftClick = event.button === 0 const isRightClick = event.type === 'contextmenu' && event.detail === 0 - const isDragIntention = + const isCanvasPanIntention = editorStoreRef.current.editor.keysPressed['space'] || event.button === 1 - const hasInteractionSessionWithMouseMoved = + + const draggingOverThreshold = editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.type === 'DRAG' ? editorStoreRef.current.editor.canvas.interactionSession?.interactionData?.drag != null : false + const hasInteractionSession = editorStoreRef.current.editor.canvas.interactionSession != null const hadInteractionSessionThatWasCancelled = interactionSessionHappened.current && !hasInteractionSession const activeControl = editorStoreRef.current.editor.canvas.interactionSession?.activeControl + const mouseUpSelectionAllowed = didWeHandleMouseDown.current && - !hadInteractionSessionThatWasCancelled && + (!hadInteractionSessionThatWasCancelled || !draggedOverThreshold.current) && (activeControl == null || activeControl.type === 'BOUNDING_AREA') if (event.type === 'mousedown') { @@ -625,6 +659,7 @@ function useSelectOrLiveModeSelectAndHover( interactionSessionHappened.current = false // didWeHandleMouseDown is used to avoid selecting when closing text editing didWeHandleMouseDown.current = false + draggedOverThreshold.current = false if (!mouseUpSelectionAllowed) { // We should skip this mouseup @@ -633,8 +668,8 @@ function useSelectOrLiveModeSelectAndHover( } if ( - isDragIntention || - hasInteractionSessionWithMouseMoved || + isCanvasPanIntention || + draggingOverThreshold || !active || !(isLeftClick || isRightClick) ) { @@ -643,8 +678,13 @@ function useSelectOrLiveModeSelectAndHover( } const doubleClick = event.type === 'mousedown' && event.detail > 0 && event.detail % 2 === 0 + const cmdMouseDown = event.type === 'mousedown' && event.metaKey const selectableViews = getSelectableViewsForSelectMode(event.metaKey, doubleClick) - const preferAlreadySelected = getPreferredSelectionForEvent(event.type, doubleClick) + const preferAlreadySelected = getPreferredSelectionForEvent( + event.type, + doubleClick, + event.metaKey, + ) const foundTarget = findValidTarget( selectableViews, windowPoint(point(event.clientX, event.clientY)), @@ -658,7 +698,7 @@ function useSelectOrLiveModeSelectAndHover( if (foundTarget != null || isDeselect) { if ( event.button !== 2 && - event.type !== 'mouseup' && + (event.type !== 'mouseup' || cmdMouseDown) && foundTarget != null && draggingAllowed && // grid has its own drag handling diff --git a/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx b/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx index d13d6108bffa..42b2fd55f15c 100644 --- a/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx +++ b/editor/src/components/canvas/controls/select-mode/select-mode.spec.browser2.tsx @@ -33,7 +33,6 @@ import type { Modifiers } from '../../../../utils/modifiers' import { cmdModifier, emptyModifiers, shiftCmdModifier } from '../../../../utils/modifiers' import { FOR_TESTS_setNextGeneratedUids } from '../../../../core/model/element-template-utils.test-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' -import { setFeatureForBrowserTestsUseInDescribeBlockOnly } from '../../../../utils/utils.test-utils' async function fireSingleClickEvents( target: HTMLElement, @@ -1532,6 +1531,7 @@ describe('mouseup selection', () => { </div> `) + const ScenePath = EP.elementPath([[BakedInStoryboardUID, TestSceneUID]]) const RedPath = EP.elementPath([ [BakedInStoryboardUID, TestSceneUID, TestAppUID], ['app-root', 'red'], @@ -1562,7 +1562,171 @@ describe('mouseup selection', () => { return getDOMRectCentre(elementRect) } - it('mouseup in the gap between a multi-selection will select the element behind if no drag happens', async () => { + describe('Interactions in selected scene', () => { + it('Can select element in selected scene by clicking on it', async () => { + const renderResult = await renderTestEditorWithCode( + MouseupTestProject, + 'await-first-dom-report', + ) + + await renderResult.dispatch([selectComponents([ScenePath], false)], true) + + const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID) + + const redCentre = await getElementCentre('red', renderResult) + await mouseDownAtPoint(canvasControlsLayer, redCentre) + + // selection should not change on mouse down + expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath]) + await mouseUpAtPoint(canvasControlsLayer, redCentre) + + // selection should change on mouse up + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + + // Check nothing has changed in the project + expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(MouseupTestProject) + }) + + it('Can select element in selected scene by dragging it under the drag threshold', async () => { + const renderResult = await renderTestEditorWithCode( + MouseupTestProject, + 'await-first-dom-report', + ) + + await renderResult.dispatch([selectComponents([ScenePath], false)], true) + + const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID) + + const redCentre = await getElementCentre('red', renderResult) + await mouseMoveToPoint(canvasControlsLayer, redCentre) + + await mouseDownAtPoint(canvasControlsLayer, redCentre) + // selection should not change on mouse down + expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath]) + + await mouseMoveToPoint(canvasControlsLayer, { x: redCentre.x + 1, y: redCentre.y + 1 }) + + await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 1, y: redCentre.y + 1 }) + + // selection should change on mouse up because dragging was below threshold, so this interaction was something like a click + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + + // Check nothing has changed in the project + expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(MouseupTestProject) + }) + + it('When the scene is selected, dragging an element inside it drags the scene and the selection doesnt change', async () => { + const renderResult = await renderTestEditorWithCode( + MouseupTestProject, + 'await-first-dom-report', + ) + + await renderResult.dispatch([selectComponents([ScenePath], false)], true) + + const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID) + + const redCentre = await getElementCentre('red', renderResult) + await mouseMoveToPoint(canvasControlsLayer, redCentre) + await mouseDownAtPoint(canvasControlsLayer, redCentre) + + // selection should not change on mouse down + expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath]) + + await mouseMoveToPoint(canvasControlsLayer, { x: redCentre.x + 100, y: redCentre.y + 100 }) + await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 100, y: redCentre.y + 100 }) + + // selection should not change on mouse up because dragging of the scene was successful + expect(renderResult.getEditorState().editor.selectedViews).toEqual([ScenePath]) + + // Dragging was successful so the project has changed + expect(getPrintedUiJsCode(renderResult.getEditorState())).not.toEqual(MouseupTestProject) + }) + + it('When the scene is selected, cmd-mousedown selects the element inside it', async () => { + const renderResult = await renderTestEditorWithCode( + MouseupTestProject, + 'await-first-dom-report', + ) + + await renderResult.dispatch([selectComponents([ScenePath], false)], true) + + const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID) + + const redCentre = await getElementCentre('red', renderResult) + await mouseMoveToPoint(canvasControlsLayer, redCentre) + + await mouseDownAtPoint(canvasControlsLayer, redCentre, { modifiers: cmdModifier }) + + // selection should change on mouse down because cmd was down + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + }) + + it('When the scene is selected, cmd-drag drags the element inside it', async () => { + const renderResult = await renderTestEditorWithCode( + MouseupTestProject, + 'await-first-dom-report', + ) + + await renderResult.dispatch([selectComponents([ScenePath], false)], true) + + const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID) + + const redCentre = await getElementCentre('red', renderResult) + await mouseMoveToPoint(canvasControlsLayer, redCentre) + + await mouseDownAtPoint(canvasControlsLayer, redCentre, { modifiers: cmdModifier }) + + // selection should change on mouse down because cmd was down. This is necessary, the following drag will move the element inside + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + + await mouseMoveToPoint( + canvasControlsLayer, + { x: redCentre.x + 100, y: redCentre.y + 100 }, + { modifiers: cmdModifier }, + ) + await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 100, y: redCentre.y + 100 }) + + // selection is still on dragged the element + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + + // Dragging was successful so the project has changed + expect(getPrintedUiJsCode(renderResult.getEditorState())).not.toEqual(MouseupTestProject) + }) + + it('When the scene is selected, cmd-dragging it below the drag threshold selects the element inside it', async () => { + const renderResult = await renderTestEditorWithCode( + MouseupTestProject, + 'await-first-dom-report', + ) + + await renderResult.dispatch([selectComponents([ScenePath], false)], true) + + const canvasControlsLayer = renderResult.renderedDOM.getByTestId(CanvasControlsContainerID) + + const redCentre = await getElementCentre('red', renderResult) + await mouseMoveToPoint(canvasControlsLayer, redCentre) + + await mouseDownAtPoint(canvasControlsLayer, redCentre, { modifiers: cmdModifier }) + + // selection should change on mouse down because cmd was down + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + + await mouseMoveToPoint( + canvasControlsLayer, + { x: redCentre.x + 100, y: redCentre.y + 100 }, + { modifiers: cmdModifier }, + ) + await mouseUpAtPoint(canvasControlsLayer, { x: redCentre.x + 1, y: redCentre.y + 1 }) + + // selection is still on the element which was already selected on mousedown + expect(renderResult.getEditorState().editor.selectedViews).toEqual([RedPath]) + + // Dragging was not successful so the project hasn't changed + expect(getPrintedUiJsCode(renderResult.getEditorState())).not.toEqual(MouseupTestProject) + }) + }) + + it('mouseup in the gap between a multi-selection will not select the element behind if no drag happens', async () => { const renderResult = await renderTestEditorWithCode( MouseupTestProject, 'await-first-dom-report', diff --git a/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx b/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx index ba2520178615..c7133afb532a 100644 --- a/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx +++ b/editor/src/components/canvas/controls/selection-area.spec.browser2.tsx @@ -739,6 +739,7 @@ export var ${BakedInStoryboardVariableName} = (props) => { moveBeforeMouseDown: true, staggerMoveEvents: true, modifiers: shiftModifier, + mouseDownModifiers: shiftModifier, }, ) @@ -759,6 +760,7 @@ export var ${BakedInStoryboardVariableName} = (props) => { moveBeforeMouseDown: true, staggerMoveEvents: true, modifiers: shiftModifier, + mouseDownModifiers: shiftModifier, }, ) @@ -813,7 +815,12 @@ export var ${BakedInStoryboardVariableName} = (props) => { container, { x: rect.x + 620, y: rect.y + 100 }, { x: rect.x + 670, y: rect.y + 310 }, - { moveBeforeMouseDown: true, staggerMoveEvents: true, modifiers: shiftModifier }, + { + moveBeforeMouseDown: true, + staggerMoveEvents: true, + modifiers: shiftModifier, + mouseDownModifiers: shiftModifier, + }, ) expect(renderResult.getEditorState().editor.selectedViews.map(EP.toString)).toEqual([ diff --git a/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx b/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx index bb735835fff2..9cf14a4857c9 100644 --- a/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx +++ b/editor/src/components/canvas/controls/text-edit-mode/text-edit-mode-hooks.tsx @@ -50,7 +50,7 @@ export function useTextEditModeSelectAndHover(active: boolean): MouseCallbacks { const foundTarget = findValidTarget( textEditableViews, windowPoint(point(event.clientX, event.clientY)), - 'dont-prefer-selected', + 'prefer-more-specific-selection', ) if (foundTarget == null) { return diff --git a/editor/src/components/canvas/event-helpers.test-utils.tsx b/editor/src/components/canvas/event-helpers.test-utils.tsx index d05cdd786d87..66073a73ad42 100644 --- a/editor/src/components/canvas/event-helpers.test-utils.tsx +++ b/editor/src/components/canvas/event-helpers.test-utils.tsx @@ -178,6 +178,7 @@ export async function mouseDragFromPointWithDelta( dragDelta: Point, options: { modifiers?: Modifiers + mouseDownModifiers?: Modifiers eventOptions?: MouseEventInit staggerMoveEvents?: boolean midDragCallback?: () => Promise<void> @@ -197,6 +198,7 @@ export async function mouseDragFromPointToPoint( endPoint: Point, options: { modifiers?: Modifiers + mouseDownModifiers?: Modifiers eventOptions?: MouseEventInit staggerMoveEvents?: boolean midDragCallback?: () => Promise<void> @@ -216,10 +218,13 @@ export async function mouseDragFromPointToPoint( if (options.moveBeforeMouseDown) { await mouseMoveToPoint(eventSourceElement, startPoint, options) } + + const mouseDownOptions = { ...options, modifiers: options.mouseDownModifiers } + if (options.realMouseDown) { - dispatchMouseDownEventAtPoint(startPoint, options) + dispatchMouseDownEventAtPoint(startPoint, mouseDownOptions) } else { - await mouseDownAtPoint(eventSourceElement, startPoint, options) + await mouseDownAtPoint(eventSourceElement, startPoint, mouseDownOptions) } if (staggerMoveEvents) {