diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index 84ffddf7177b..f908adf080f9 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -757,7 +757,12 @@ function editorDispatchInner( const priorSimpleLocks = storedState.unpatchedEditor.lockedElements.simpleLock const updatedSimpleLocks = doNotUpdateLocks ? priorSimpleLocks - : updateSimpleLocks(storedState.unpatchedEditor.jsxMetadata, metadata, priorSimpleLocks) + : updateSimpleLocks( + storedState.unpatchedEditor.jsxMetadata, + metadata, + elementPathTree, + priorSimpleLocks, + ) if (result.unpatchedEditor.canvas.interactionSession != null) { result = { ...result, diff --git a/editor/src/core/shared/element-locking.spec.browser2.tsx b/editor/src/core/shared/element-locking.spec.browser2.tsx new file mode 100644 index 000000000000..e8c621676ca0 --- /dev/null +++ b/editor/src/core/shared/element-locking.spec.browser2.tsx @@ -0,0 +1,61 @@ +import { renderTestEditorWithCode } from '../../components/canvas/ui-jsx.test-utils' +import * as EP from './element-path' + +function testCode(snippet: string) { + return ` +import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = ( + + + + + +) + +export var App = (props) => { + return ( +
+ ${snippet} +
+ ) +}` +} + +describe('Auto-locking elements', () => { + it('Root element of component locked when it is not a leaf element', async () => { + const renderResult = await renderTestEditorWithCode( + testCode('
Hello
'), + 'await-first-dom-report', + ) + + expect(renderResult.getEditorState().editor.lockedElements.simpleLock.map(EP.toString)).toEqual( + ['sb/sc/app:app-root'], + ) + }) + + it('Root element of component is not locked when it is a leaf element', async () => { + const renderResult = await renderTestEditorWithCode(testCode('Hello'), 'await-first-dom-report') + + expect(renderResult.getEditorState().editor.lockedElements.simpleLock.map(EP.toString)).toEqual( + [], + ) + }) +}) diff --git a/editor/src/core/shared/element-locking.ts b/editor/src/core/shared/element-locking.ts index 7624331311b6..b38db3e39d01 100644 --- a/editor/src/core/shared/element-locking.ts +++ b/editor/src/core/shared/element-locking.ts @@ -8,17 +8,24 @@ import { MetadataUtils } from '../model/element-metadata-utils' export function updateSimpleLocks( priorMetadata: ElementInstanceMetadataMap, newMetadata: ElementInstanceMetadataMap, + pathTree: ElementPathTrees, currentSimpleLockedItems: Array, ): Array { let result: Array = [...currentSimpleLockedItems] for (const [key, value] of Object.entries(newMetadata)) { - // This entry is the root element of an instance or a remix Outlet, and it isn't present in the previous metadata, - // which implies that it has been newly added. - if ( - (EP.isRootElementOfInstance(value.elementPath) || - MetadataUtils.isProbablyRemixOutlet(newMetadata, value.elementPath)) && - !(key in priorMetadata) - ) { + // The entry isn't present in the previous metadata, which implies that it has been newly added. + const isNewlyAdded = !(key in priorMetadata) + + // You rarely want to select a root element of a component instance, except when it is a leaf element + const isNonLeafRootElement = + EP.isRootElementOfInstance(value.elementPath) && + MetadataUtils.getChildrenPathsOrdered(newMetadata, pathTree, value.elementPath).length > 0 + + // Remix Outlet are rarely needed to be selected + const isRemixOutlet = MetadataUtils.isProbablyRemixOutlet(newMetadata, value.elementPath) + + // This entry is a newly added non-leaf root element or a remix Outlet, it should be autolocked + if (isNewlyAdded && (isNonLeafRootElement || isRemixOutlet)) { result.push(value.elementPath) } }