Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A Less Aggressive Auto-Collapse #6598

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { CanvasRectangle } from '../../../core/shared/math-utils'
import { isFiniteRectangle, sizesEqual } from '../../../core/shared/math-utils'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import * as EP from '../../../core/shared/element-path'
import type { ElementInstanceMetadataMap } from '../../../core/shared/element-template'
Expand All @@ -14,16 +16,23 @@ import type { CanvasFrameAndTarget } from '../canvas-types'
import type { BaseCommand, CanvasCommand, CommandFunctionResult } from './commands'
import { foldAndApplyCommandsSimple } from './commands'
import { setCssLengthProperty, setExplicitCssValue } from './set-css-length-command'
import { setProperty } from './set-property-command'
import { showToastCommand } from './show-toast-command'
import { isZeroSizedElement } from '../controls/outline-utils'
import type { HuggingElementContentsStatus } from '../hugging-utils'
import { getHuggingElementContentsStatus } from '../hugging-utils'

export interface IntendedBoundsAndChildrenState extends CanvasFrameAndTarget {
elementFrame: CanvasRectangle
huggingElementContentsStatus: HuggingElementContentsStatus
}

export interface PushIntendedBoundsAndUpdateHuggingElements extends BaseCommand {
type: 'PUSH_INTENDED_BOUNDS_AND_UPDATE_HUGGING_ELEMENTS'
value: Array<CanvasFrameAndTarget>
value: Array<IntendedBoundsAndChildrenState>
}

export function pushIntendedBoundsAndUpdateHuggingElements(
value: Array<CanvasFrameAndTarget>,
value: Array<IntendedBoundsAndChildrenState>,
): PushIntendedBoundsAndUpdateHuggingElements {
return {
type: 'PUSH_INTENDED_BOUNDS_AND_UPDATE_HUGGING_ELEMENTS',
Expand Down Expand Up @@ -60,23 +69,6 @@ export const runPushIntendedBoundsAndUpdateHuggingElements = (
}
}

function getHuggingElementContentsStatus(
jsxMetadata: ElementInstanceMetadataMap,
path: ElementPath,
): 'empty' | 'contains-only-absolute' | 'contains-some-absolute' | 'non-empty' {
const children = MetadataUtils.getChildrenUnordered(jsxMetadata, path)
const absoluteChildren = children.filter(MetadataUtils.isPositionAbsolute).length
if (children.length === 0) {
return 'empty'
} else if (absoluteChildren === children.length) {
return 'contains-only-absolute'
} else if (absoluteChildren > 0) {
return 'contains-some-absolute'
} else {
return 'non-empty'
}
}

function applyUpdateResizeHuggingElementsCommands(
editor: EditorState,
command: PushIntendedBoundsAndUpdateHuggingElements,
Expand All @@ -89,17 +81,30 @@ function applyUpdateResizeHuggingElementsCommands(
editor.jsxMetadata,
frameAndTarget.target,
)

// If the element isn't hugging its parent in either direction, skip this case.
if (
metadata == null ||
!(isHuggingParent(metadata, 'width') || isHuggingParent(metadata, 'height'))
) {
continue
}

// If the element still has non-absolute children, skip this case.
const status = getHuggingElementContentsStatus(editor.jsxMetadata, frameAndTarget.target)
if (status === 'non-empty') {
continue
}

// If the element is now empty, but used to contain only absolute children, and is zero-sized, skip this case.
if (
status === 'empty' &&
frameAndTarget.huggingElementContentsStatus === 'contains-only-absolute' &&
isZeroSizedElement(frameAndTarget.elementFrame)
) {
continue
}

function setCSSDimension(
flexDirection: FlexDirection | null,
prop: 'left' | 'top' | 'width' | 'height',
Expand Down Expand Up @@ -128,39 +133,8 @@ function applyUpdateResizeHuggingElementsCommands(
'height',
frameAndTarget.frame.height,
),
showToastCommand('Added fixed width and height', 'NOTICE', 'added-width-height'),
)

const parentPath = EP.parentPath(frameAndTarget.target)
const parentElement = MetadataUtils.findElementByElementPath(editor.jsxMetadata, parentPath)
const parentIsHugging =
parentElement != null &&
(isHuggingParent(parentElement, 'width') || isHuggingParent(parentElement, 'height'))
const shouldSetAbsolutePosition =
EP.isStoryboardPath(parentPath) || parentElement == null || parentIsHugging
if (shouldSetAbsolutePosition) {
commands.push(
setProperty('always', frameAndTarget.target, PP.create('style', 'position'), 'absolute'),
setCSSDimension(
metadata.specialSizeMeasurements.flexDirection,
'left',
frameAndTarget.frame.x,
),
setCSSDimension(
metadata.specialSizeMeasurements.flexDirection,
'top',
frameAndTarget.frame.y,
),
showToastCommand(
'Converted to fixed size and absolute position',
'NOTICE',
'convert-to-fixed-size',
),
)
} else {
commands.push(
showToastCommand('Added fixed width and height', 'NOTICE', 'added-width-height'),
)
}
}
}
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function makeScenePath(trailingPath: string): ElementPath {
describe('push intended bounds', () => {
describe('hugging elements', () => {
describe('flex containers', () => {
xit('keeps the container dimensions when becoming empty (flex) (delete, single element)', async () => {
it('keeps the container dimensions when becoming empty (flex) (delete, single element)', async () => {
const renderResult = await renderTestEditorWithCode(
formatTestProjectCode(
makeTestProjectCodeWithSnippet(`
Expand Down Expand Up @@ -74,7 +74,7 @@ describe('push intended bounds', () => {
),
)
})
xit('keeps the container dimensions when becoming empty (flex) (delete, multiple elements)', async () => {
it('keeps the container dimensions when becoming empty (flex) (delete, multiple elements)', async () => {
const renderResult = await renderTestEditorWithCode(
formatTestProjectCode(
makeTestProjectCodeWithSnippet(`
Expand Down Expand Up @@ -177,7 +177,7 @@ describe('push intended bounds', () => {
),
)
})
xit('keeps the container dimensions when becoming empty (zero-sized)', async () => {
it('keeps the container dimensions when becoming empty (zero-sized)', async () => {
const renderResult = await renderTestEditorWithCode(
formatTestProjectCode(`
import * as React from 'react'
Expand Down Expand Up @@ -205,13 +205,13 @@ describe('push intended bounds', () => {

export var storyboard = (
<Storyboard data-uid='sb'>
<div data-uid='container' data-testid='container' style={{ backgroundColor: 'red', width: 80, height: 80, position: 'absolute', left: 100, top: 100 }} />
<div data-uid='container' data-testid='container' style={{ backgroundColor: 'red' }} />
</Storyboard>
)
`),
)
})
xit('keeps the container dimensions when becoming empty (inside a flex container)', async () => {
it('keeps the container dimensions when becoming empty (inside a flex container)', async () => {
const renderResult = await renderTestEditorWithCode(
formatTestProjectCode(`
import * as React from 'react'
Expand Down Expand Up @@ -251,6 +251,46 @@ describe('push intended bounds', () => {
`),
)
})
it('keeps the container dimensions when becoming empty (inside a grid container)', async () => {
const renderResult = await renderTestEditorWithCode(
formatTestProjectCode(`
import * as React from 'react'
import { Storyboard, Group } from 'utopia-api'

export var storyboard = (
<Storyboard data-uid='sb'>
<div data-uid='grid' style={{ display: 'grid', gridTemplateColumns: '1fr', gridRowColumns: '1fr', alignItems: 'center', justifyContent: 'center', padding: 10, gap: 10, backgroundColor: 'white', width: 100, height: 100, position: 'absolute', left: 21, top: 17 }}>
<div data-uid='container' data-testid='container' style={{ backgroundColor: 'red' }}>
<div data-uid='delete-me' style={{ backgroundColor: 'yellow', width: 80, height: 80 }}>
delete me pls
</div>
</div>
</div>
</Storyboard>
)
`),
'await-first-dom-report',
)

await selectComponentsForTest(renderResult, [EP.fromString(`sb/grid/container/delete-me`)])

await renderResult.dispatch([deleteSelected()], true)

expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
formatTestProjectCode(`
import * as React from 'react'
import { Storyboard, Group } from 'utopia-api'

export var storyboard = (
<Storyboard data-uid='sb'>
<div data-uid='grid' style={{ display: 'grid', gridTemplateColumns: '1fr', gridRowColumns: '1fr', alignItems: 'center', justifyContent: 'center', padding: 10, gap: 10, backgroundColor: 'white', width: 100, height: 100, position: 'absolute', left: 21, top: 17 }}>
<div data-uid='container' data-testid='container' style={{ backgroundColor: 'red', width: 80, height: 80 }} />
</div>
</Storyboard>
)
`),
)
})
})
})
})
26 changes: 26 additions & 0 deletions editor/src/components/canvas/hugging-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { MetadataUtils } from '../../core/model/element-metadata-utils'
import type { ElementInstanceMetadataMap } from '../../core/shared/element-template'
import type { ElementPath } from '../../core/shared/project-file-types'

export type HuggingElementContentsStatus =
| 'empty'
| 'contains-only-absolute'
| 'contains-some-absolute'
| 'non-empty'

export function getHuggingElementContentsStatus(
jsxMetadata: ElementInstanceMetadataMap,
path: ElementPath,
): HuggingElementContentsStatus {
const children = MetadataUtils.getChildrenUnordered(jsxMetadata, path)
const absoluteChildren = children.filter(MetadataUtils.isPositionAbsolute).length
if (children.length === 0) {
return 'empty'
} else if (absoluteChildren === children.length) {
return 'contains-only-absolute'
} else if (absoluteChildren > 0) {
return 'contains-some-absolute'
} else {
return 'non-empty'
}
}
Loading
Loading