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

Ensure containing block around absolute wrapper. #4278

Merged
merged 3 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fireEvent } from '@testing-library/react'
import { forElementOptic } from '../../core/model/common-optics'
import { forElementChildOptic } from '../../core/model/common-optics'
import {
conditionalWhenFalseOptic,
jsxConditionalExpressionOptic,
Expand Down Expand Up @@ -912,7 +912,7 @@ describe('canvas context menu', () => {
const conditionalPath = EP.fromString(
`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:aaa/conditional`,
)
const inactiveElementOptic = forElementOptic(conditionalPath)
const inactiveElementOptic = forElementChildOptic(conditionalPath)
.compose(jsxConditionalExpressionOptic)
.compose(conditionalWhenFalseOptic)
const inactiveElement = unsafeGet(inactiveElementOptic, renderResult.getEditorState())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6707,7 +6707,7 @@ export var storyboard = (
expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
makeTestProjectCodeWithSnippet(
`<div data-uid='aaa' style={{contain: 'layout', width: 300, height: 300}}>
<div data-uid='bbb' style={{display: 'flex', gap: 10, padding: 10}}>
<div data-uid='bbb' style={{display: 'flex', gap: 10, padding: 10, contain: 'layout'}}>
<div data-uid='ccc' style={{width: 100, height: 60}} />
<div style={{backgroundColor: '#aaaaaa33', position: 'absolute'}} data-uid='${testUID}'>
<div data-uid='ddd' style={{flexGrow: 1, height: '100%'}} />
Expand Down Expand Up @@ -6812,7 +6812,7 @@ export var storyboard = (
expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
formatTestProjectCode(
makeTestProjectCodeWithSnippet(`
<div data-uid='aaa'>
<div data-uid='aaa' style={{ contain: 'layout' }}>
<Group
style={{ position: 'absolute', left: 0, top: 0 }}
data-uid='grp'
Expand Down Expand Up @@ -6949,7 +6949,7 @@ export var storyboard = (
expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
formatTestProjectCode(
makeTestProjectCodeWithSnippet(`
<div data-uid='aaa'>
<div data-uid='aaa' style={{ contain: 'layout' }}>
<Group
style={{ position: 'absolute', left: 0, top: 0 }}
data-uid='grp'
Expand Down
4 changes: 3 additions & 1 deletion editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ import { getConditionalCaseCorrespondingToBranchPath } from '../../../core/model
import { deleteProperties } from '../../canvas/commands/delete-properties-command'
import { treatElementAsFragmentLike } from '../../canvas/canvas-strategies/strategies/fragment-like-helpers'
import {
fixParentContainingBlockSettings,
isTextContainingConditional,
unwrapConditionalClause,
unwrapTextContainingConditional,
Expand Down Expand Up @@ -2139,12 +2140,13 @@ export const UPDATE_FNS = {
if (newPath == null) {
return editor
}
const withFixedParents = fixParentContainingBlockSettings(updatedEditor, newPath)

// TODO maybe update frames and position
const frameChanges: Array<PinOrFlexFrameChange> = []
const withWrapperViewAdded = {
...setCanvasFramesInnerNew(
includeToast(detailsOfUpdate, updatedEditor),
includeToast(detailsOfUpdate, withFixedParents),
frameChanges,
null,
),
Expand Down
113 changes: 110 additions & 3 deletions editor/src/components/editor/actions/wrap-unwrap-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
getConditionalActiveCase,
getConditionalBranch,
} from '../../../core/model/conditionals'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import {
MetadataUtils,
getSimpleAttributeAtPath,
propertyHasSimpleValue,
} from '../../../core/model/element-metadata-utils'
import {
generateUidWithExistingComponents,
insertJSXElementChildren,
Expand All @@ -15,6 +19,7 @@ import {
getUtopiaJSXComponentsFromSuccess,
} from '../../../core/model/project-file-utils'
import * as EP from '../../../core/shared/element-path'
import * as PP from '../../../core/shared/property-path'
import type {
ElementInstanceMetadataMap,
JSXConditionalExpression,
Expand All @@ -32,14 +37,17 @@ import {
jsxFragment,
jsxTextBlock,
} from '../../../core/shared/element-template'
import { modify } from '../../../core/shared/optics/optic-utilities'
import { modify, toFirst } from '../../../core/shared/optics/optic-utilities'
import { forceNotNull, optionalMap } from '../../../core/shared/optional-utils'
import type { ElementPath, Imports } from '../../../core/shared/project-file-types'
import type { IndexPosition } from '../../../utils/utils'
import { absolute } from '../../../utils/utils'
import type { EditorDispatch } from '../action-types'
import type { DerivedState, EditorState } from '../store/editor-state'
import { modifyUnderlyingTargetElement } from '../store/editor-state'
import {
modifyUnderlyingTargetElement,
withUnderlyingTargetFromEditorState,
} from '../store/editor-state'
import type { ConditionalClauseInsertionPath, InsertionPath } from '../store/insertion-path'
import {
childInsertionPath,
Expand All @@ -55,6 +63,10 @@ import { mergeImports } from '../../../core/workers/common/project-file-utils'
import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
import { fixUtopiaElementGeneric } from '../../../core/shared/uid-utils'
import { getAllUniqueUids } from '../../../core/model/get-unique-ids'
import { foldEither, right } from '../../../core/shared/either'
import { editorStateToElementChildOptic } from '../../../core/model/common-optics'
import { fromField, fromTypeGuard } from '../../../core/shared/optics/optic-creators'
import { setJSXValueAtPath } from '../../../core/shared/jsx-attributes'

export function unwrapConditionalClause(
editor: EditorState,
Expand Down Expand Up @@ -215,6 +227,101 @@ export function isTextContainingConditional(
return false
}

function elementHasPositionOf(targetElement: JSXElement, positionValue: string): boolean {
return propertyHasSimpleValue(
right(targetElement.props),
PP.create('style', 'position'),
positionValue,
)
}

function elementHasContainLayout(targetElement: JSXElement): boolean {
return propertyHasSimpleValue(right(targetElement.props), PP.create('style', 'contain'), 'layout')
}

export function fixParentContainingBlockSettings(
editorState: EditorState,
targetPath: ElementPath,
): EditorState {
// Determine if the current status of this element means that we potentially need to look
// into its ancestors.
const possibleTargetElement = toFirst(
editorStateToElementChildOptic(targetPath).compose(fromTypeGuard(isJSXElement)),
editorState,
)
const targetNecessitatesAncestorChecks: boolean = foldEither(
() => {
return false
},
(targetElement) => {
// Currently any element with `position: 'absolute'` is a likely candidate.
const targetHasPositionAbsolute = elementHasPositionOf(targetElement, 'absolute')
return targetHasPositionAbsolute
},
possibleTargetElement,
)

let shouldUpdateParent: boolean = false
const parentPath = EP.parentPath(targetPath)
const parentOptic = editorStateToElementChildOptic(parentPath).compose(
fromTypeGuard(isJSXElement),
)
// Check the status of the parent to see if that needs to be updated.
// Do not go outside of the current component, at least for now.
if (targetNecessitatesAncestorChecks && EP.isFromSameInstanceAs(targetPath, parentPath)) {
// Determine if the parent needs to be fixed up.
const possibleParentElement = toFirst(parentOptic, editorState)
shouldUpdateParent = foldEither(
() => {
return false
},
(parentElement) => {
// If the parent does not specify `position: 'absolute'`, `position: 'relative'`
// or it doesn't already have `contain: 'layout'`, then it needs updating.
const parentDefinesContainingBlock =
elementHasPositionOf(parentElement, 'absolute') ||
elementHasPositionOf(parentElement, 'relative') ||
elementHasContainLayout(parentElement)
return !parentDefinesContainingBlock
},
possibleParentElement,
)
}

// Update the parent if necessary.
if (shouldUpdateParent) {
let attributesUpdated: boolean = false
const updatedEditorState = modify(
parentOptic.compose(fromField('props')),
(attributes) => {
// Try to update the attributes, which may not be possible if they're
// defined by an expression.
const maybeUpdatedAttributes = setJSXValueAtPath(
attributes,
PP.create('style', 'contain'),
jsExpressionValue('layout', emptyComments),
)
return foldEither(
() => {
return attributes
},
(updatedAttributes) => {
attributesUpdated = true
return updatedAttributes
},
maybeUpdatedAttributes,
)
},
editorState,
)
if (attributesUpdated) {
return updatedEditorState
}
}

return editorState
}

export function wrapElementInsertions(
editor: EditorState,
derivedState: DerivedState,
Expand Down
4 changes: 2 additions & 2 deletions editor/src/components/editor/conditionals.spec.browser2.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable jest/expect-expect */
import { act, within } from '@testing-library/react'
import { forElementOptic } from '../../core/model/common-optics'
import { forElementChildOptic } from '../../core/model/common-optics'
import { conditionalWhenTrueOptic, maybeConditionalExpression } from '../../core/model/conditionals'
import { MetadataUtils } from '../../core/model/element-metadata-utils'
import { isRight } from '../../core/shared/either'
Expand Down Expand Up @@ -195,7 +195,7 @@ describe('conditionals', () => {
'await-first-dom-report',
)

const helloWorldUIDOptic: Optic<EditorStorePatched, string> = forElementOptic(
const helloWorldUIDOptic: Optic<EditorStorePatched, string> = forElementChildOptic(
EP.appendNewElementPath(TestScenePath, ['aaa', 'conditional']),
)
.compose(fromTypeGuard(isJSXConditionalExpression))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
makeTestProjectCodeWithSnippet,
renderTestEditorWithCode,
} from '../../components/canvas/ui-jsx.test-utils'
import { forElementOptic } from '../../core/model/common-optics'
import { forElementChildOptic } from '../../core/model/common-optics'
import type { ConditionalCase } from '../../core/model/conditionals'
import {
conditionalClauseAsBoolean,
Expand Down Expand Up @@ -1097,7 +1097,7 @@ describe('conditionals in the navigator', () => {
)

// Need the underlying value in the clause to be able to construct the navigator entry.
const inactiveElementOptic = forElementOptic(EP.parentPath(elementPathToDrag))
const inactiveElementOptic = forElementChildOptic(EP.parentPath(elementPathToDrag))
.compose(jsxConditionalExpressionOptic)
.compose(conditionalWhenFalseOptic)
const inactiveElement = unsafeGet(inactiveElementOptic, renderResult.getEditorState())
Expand Down Expand Up @@ -1148,7 +1148,7 @@ describe('conditionals in the navigator', () => {
await renderResult.getDispatchFollowUpActionsFinished()

// Need the underlying value in the clause to be able to construct the navigator entry.
const removedOriginalLocationOptic = forElementOptic(EP.parentPath(elementPathToDrag))
const removedOriginalLocationOptic = forElementChildOptic(EP.parentPath(elementPathToDrag))
.compose(jsxConditionalExpressionOptic)
.compose(conditionalWhenFalseOptic)
const removedOriginalLocation = unsafeGet(
Expand Down Expand Up @@ -1247,7 +1247,7 @@ describe('conditionals in the navigator', () => {
await renderResult.getDispatchFollowUpActionsFinished()

// Need the underlying value in the clause to be able to construct the navigator entry.
const removedOriginalLocationOptic = forElementOptic(EP.parentPath(elementPathToDrag))
const removedOriginalLocationOptic = forElementChildOptic(EP.parentPath(elementPathToDrag))
.compose(jsxConditionalExpressionOptic)
.compose(conditionalWhenTrueOptic)
const removedOriginalLocation = unsafeGet(
Expand Down Expand Up @@ -1353,7 +1353,7 @@ describe('conditionals in the navigator', () => {
`${BakedInStoryboardUID}/${TestSceneUID}/containing-div/conditional1/else-div`,
)
const elseDivElement = unsafeGet(
forElementOptic(elementPathToDelete),
forElementChildOptic(elementPathToDelete),
renderResult.getEditorState(),
)

Expand Down Expand Up @@ -1385,7 +1385,7 @@ describe('conditionals in the navigator', () => {
await renderResult.getDispatchFollowUpActionsFinished()

// Need the underlying value in the clause to be able to construct the navigator entry.
const removedOriginalLocationOptic = forElementOptic(EP.parentPath(elementPathToDelete))
const removedOriginalLocationOptic = forElementChildOptic(EP.parentPath(elementPathToDelete))
.compose(jsxConditionalExpressionOptic)
.compose(conditionalWhenFalseOptic)
const removedOriginalLocation = unsafeGet(
Expand Down Expand Up @@ -1515,7 +1515,7 @@ describe('conditionals in the navigator', () => {
startingEditorStore: EditorStorePatched,
endingEditorStore: EditorStorePatched,
) => {
const expectedPasteTargetOptic = forElementOptic(
const expectedPasteTargetOptic = forElementChildOptic(
EP.parentPath(pasteTestCase.pathToPasteInto),
)
.compose(fromTypeGuard(isJSXConditionalExpression))
Expand Down Expand Up @@ -1546,7 +1546,7 @@ describe('conditionals in the navigator', () => {

// Getting info relating to what element will be pasted into.
const elementToPasteInto = unsafeGet(
forElementOptic(pasteTestCase.pathToPasteInto),
forElementChildOptic(pasteTestCase.pathToPasteInto),
renderResult.getEditorState(),
)

Expand Down Expand Up @@ -1872,7 +1872,7 @@ describe('Navigator conditional override toggling', () => {
const elementPath = EP.fromString(`${BakedInStoryboardUID}/conditional/${elementUID}`)

// Need the underlying value in the clause to be able to construct the navigator entry.
const inactiveElementOptic = forElementOptic(EP.parentPath(elementPath))
const inactiveElementOptic = forElementChildOptic(EP.parentPath(elementPath))
.compose(jsxConditionalExpressionOptic)
.compose(elementUID === 'true-div' ? conditionalWhenTrueOptic : conditionalWhenFalseOptic)

Expand Down
28 changes: 18 additions & 10 deletions editor/src/core/model/common-optics.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import type { EditorStorePatched } from '../../components/editor/store/editor-state'
import type { EditorState, EditorStorePatched } from '../../components/editor/store/editor-state'
import {
modifyUnderlyingForOpenFile,
withUnderlyingTargetFromEditorState,
} from '../../components/editor/store/editor-state'
import type { JSXElementChild } from '../shared/element-template'
import { fromField } from '../shared/optics/optic-creators'
import type { Optic } from '../shared/optics/optics'
import { traversal } from '../shared/optics/optics'
import type { ElementPath } from '../shared/project-file-types'

export function forElementOptic(target: ElementPath): Optic<EditorStorePatched, JSXElementChild> {
function from(store: EditorStorePatched): Array<JSXElementChild> {
return withUnderlyingTargetFromEditorState(target, store.editor, [], (_, element) => {
export function editorStateToElementChildOptic(
target: ElementPath,
): Optic<EditorState, JSXElementChild> {
function from(editorState: EditorState): Array<JSXElementChild> {
return withUnderlyingTargetFromEditorState(target, editorState, [], (_, element) => {
return [element]
})
}
function update(
store: EditorStorePatched,
editorState: EditorState,
modify: (child: JSXElementChild) => JSXElementChild,
): EditorStorePatched {
return {
...store,
editor: modifyUnderlyingForOpenFile(target, store.editor, modify),
}
): EditorState {
return modifyUnderlyingForOpenFile(target, editorState, modify)
}
return traversal(from, update)
}

export function forElementChildOptic(
target: ElementPath,
): Optic<EditorStorePatched, JSXElementChild> {
return fromField<EditorStorePatched, 'editor'>('editor').compose(
editorStateToElementChildOptic(target),
)
}
13 changes: 13 additions & 0 deletions editor/src/core/model/element-metadata-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2718,6 +2718,19 @@ export function getSimpleAttributeAtPath(
)
}

export function propertyHasSimpleValue(
attributes: PropsOrJSXAttributes,
property: PropertyPath,
value: string | number | boolean | null | undefined,
): boolean {
const propertyFromProps = getSimpleAttributeAtPath(attributes, property)
return foldEither(
() => false,
(valueFromProps) => valueFromProps === value,
propertyFromProps,
)
}

// This function creates a fake metadata for the given element
// Useful when metadata is needed before the real on is created.
export function createFakeMetadataForElement(
Expand Down