Skip to content

Commit

Permalink
Feature/show implicit constrained group children (#4389)
Browse files Browse the repository at this point in the history
* introducing getConstraintsIncludingImplicitForElement

* removing pins and frames from the function name

* put data-testid on react select SingleValue

* unifying isHugFromStyleAttribute and isHugFromStyleAttribute

* Update popup-list.tsx

* checkpoint

* expanding Constraints to be able to detect more scenarios

* fix implicit group constraint handling

* one change at a time. only explicit constraints were respected on master, so be it

* Update performance-regression-tests.spec.tsx.snap

* actually, left right top and bottom should become detected-only pin types

* truing up tests now that I walked back on the implicits

* adding comment TODO
  • Loading branch information
balazsbajorics authored Oct 18, 2023
1 parent 144fc5d commit 6e92d12
Show file tree
Hide file tree
Showing 11 changed files with 505 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
getJSXElementFromProjectContents,
trueUpElementChanged,
} from '../../../editor/store/editor-state'
import { getSafeGroupChildConstraintsArray } from '../../../inspector/fill-hug-fixed-control'
import { detectFillHugFixedState } from '../../../inspector/inspector-common'
import {
detectFillHugFixedState,
getConstraintsIncludingImplicitForElement,
} from '../../../inspector/inspector-common'
import type { EdgePosition } from '../../canvas-types'
import { EdgePositionLeft, EdgePositionTop, EdgePositionTopLeft } from '../../canvas-types'
import { isEdgePositionEqualTo } from '../../canvas-utils'
Expand Down Expand Up @@ -415,7 +417,12 @@ function getConstrainedSizes(
EP.isDescendantOf(element.elementPath, path),
)
for (const element of descendants) {
const constraintsArray = getSafeGroupChildConstraintsArray(allElementProps, element.elementPath)
const constraintsArray = getConstraintsIncludingImplicitForElement(
jsxMetadata,
allElementProps,
element.elementPath,
'only-explicit-constraints', // if we set this to include-implicit-constraints, we can probably delete isDimensionConstrained
)
const constraints = {
width: isDimensionConstrained(jsxMetadata, element.elementPath, constraintsArray, 'width'),
height: isDimensionConstrained(jsxMetadata, element.elementPath, constraintsArray, 'height'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import type { ProjectContentTreeRoot } from '../../../assets'
import { showToastCommand } from '../../commands/show-toast-command'
import {
getConvertIndividualElementToAbsoluteCommands,
isHugFromStyleAttribute,
sizeToVisualDimensions,
} from '../../../inspector/inspector-common'

Expand Down Expand Up @@ -745,13 +746,6 @@ function createSetParentsToFixedSizeCommands(
return []
}

const HuggingWidthHeightValues = ['max-content', 'min-content', 'fit-content', 'auto']

function isHuggingParent(element: JSXElement, property: 'width' | 'height') {
const simpleAttribute = defaultEither(
null,
getSimpleAttributeAtPath(right(element.props), PP.create('style', property)),
)

return simpleAttribute == null || HuggingWidthHeightValues.includes(simpleAttribute)
return isHugFromStyleAttribute(element.props, property, 'include-all-hugs')
}
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ function setElementPinsForLocalRectangleEnsureTwoPinsPerDimension(
}

function setPinPreserveHug(pin: 'width' | 'height', value: number): Array<SetCssLengthProperty> {
const pinIsAlreadyHug = isHugFromStyleAttribute(elementCurrentProps, pin)
const pinIsAlreadyHug = isHugFromStyleAttribute(elementCurrentProps, pin, 'only-max-content')

if (pinIsAlreadyHug) {
// we don't need to convert a Hug pin, do nothing here
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LayoutPinnedProp } from '../../../core/layout/layout-helpers-new'
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
import * as EP from '../../../core/shared/element-path'
import type { ElementPathTrees } from '../../../core/shared/element-path-tree'
Expand All @@ -23,7 +24,7 @@ import type {
import { trueUpElementChanged } from '../../editor/store/editor-state'
import { cssPixelLength, type FlexDirection } from '../../inspector/common/css-utils'
import {
isHugFromStyleAttribute,
getConstraintsIncludingImplicitForElement,
isHugFromStyleAttributeOrNull,
} from '../../inspector/inspector-common'
import type { InteractionLifecycle } from '../canvas-strategies/canvas-strategy-types'
Expand All @@ -43,10 +44,10 @@ import {
setValueKeepingOriginalUnit,
} from './set-css-length-command'
import type { FrameWithAllPoints } from './utils/group-resize-utils'
import { rectangleToSixFramePoints } from './utils/group-resize-utils'
import { sixFramePointsToCanvasRectangle } from './utils/group-resize-utils'
import {
rectangleToSixFramePoints,
roundSixPointFrameToNearestWhole,
sixFramePointsToCanvasRectangle,
transformConstrainedLocalFullFrameUsingBoundingBox,
} from './utils/group-resize-utils'
import { wildcardPatch } from './wildcard-patch-command'
Expand Down Expand Up @@ -217,29 +218,22 @@ function getUpdateResizedGroupChildrenCommands(
continue
}

let constraints: Set<keyof FrameWithAllPoints> = new Set(
editor.allElementProps[EP.toString(child)]?.['data-constraints'] ?? [],
const constraints: Array<LayoutPinnedProp> = getConstraintsIncludingImplicitForElement(
editor.jsxMetadata,
editor.allElementProps,
child,
'only-explicit-constraints', // TODO make sure to match this in detectConstraintsSetForGroupChild
)

const elementMetadata = MetadataUtils.findElementByElementPath(editor.jsxMetadata, child)

const jsxElement = MetadataUtils.getJSXElementFromMetadata(editor.jsxMetadata, child)
if (jsxElement != null) {
if (isHugFromStyleAttribute(jsxElement.props, 'width')) {
constraints.add('width')
}
if (isHugFromStyleAttribute(jsxElement.props, 'height')) {
constraints.add('height')
}
}

const resizedLocalFramePoints = roundSixPointFrameToNearestWhole(
transformConstrainedLocalFullFrameUsingBoundingBox(
groupOriginalBounds,
updatedGroupBounds,
childrenBounds,
rectangleToSixFramePoints(currentLocalFrame, childrenBounds),
Array.from(constraints),
constraints,
),
)

Expand Down Expand Up @@ -481,7 +475,7 @@ function setElementPins(
),
]

if (!isHugFromStyleAttributeOrNull(targetProps, 'width')) {
if (!isHugFromStyleAttributeOrNull(targetProps, 'width', 'include-all-hugs')) {
result.push(
setCssLengthProperty(
'always',
Expand All @@ -494,7 +488,8 @@ function setElementPins(
),
)
}
if (!isHugFromStyleAttributeOrNull(targetProps, 'height')) {

if (!isHugFromStyleAttributeOrNull(targetProps, 'height', 'include-all-hugs')) {
result.push(
setCssLengthProperty(
'always',
Expand Down
210 changes: 210 additions & 0 deletions editor/src/components/inspector/constraints-section.spec.browser2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "testGroupChild", "testFrameChild"] }] */

import * as EP from '../../core/shared/element-path'
import type { ElementPath } from '../../core/shared/project-file-types'
import {
makeTestProjectCodeWithSnippet,
renderTestEditorWithCode,
} from '../canvas/ui-jsx.test-utils'
import { selectComponents } from '../editor/actions/action-creators'

const testChild = (params: {
snippet: string
elementPath: ElementPath
expectedWidthConstraintDropdownOption: string
expectedHeightConstraintDropdownOption: string
}) =>
async function testBody() {
const renderResult = await renderTestEditorWithCode(
makeTestProjectCodeWithSnippet(params.snippet),
'await-first-dom-report',
)

await renderResult.dispatch([selectComponents([params.elementPath], false)], true)

const widthConstraintDropdown = renderResult.renderedDOM.getByTestId(
'frame-child-constraint-width-label',
)
expect(widthConstraintDropdown.textContent).toEqual(
params.expectedWidthConstraintDropdownOption,
)

const heightConstraintDropdown = renderResult.renderedDOM.getByTestId(
'frame-child-constraint-height-label',
)
expect(heightConstraintDropdown.textContent).toEqual(
params.expectedHeightConstraintDropdownOption,
)
}

const testFrameChild = (params: {
snippet: string
expectedWidthConstraintDropdownOption: string
expectedHeightConstraintDropdownOption: string
}) =>
testChild({
...params,
elementPath: EP.fromString('utopia-storyboard-uid/scene-aaa/app-entity:app-root/div/target'),
snippet: `
<div data-uid='app-root' style={{ width: 300, height: 300 }}>
<div data-uid='div' style={{ position: 'absolute', width: 150, height: 150 }}>
${params.snippet}
</div>
</div>
`,
})

const testGroupChild = (params: {
snippet: string
expectedWidthConstraintDropdownOption: string
expectedHeightConstraintDropdownOption: string
}) =>
testChild({
...params,
elementPath: EP.fromString('utopia-storyboard-uid/scene-aaa/app-entity:app-root/group/target'),
snippet: `
<div data-uid='app-root' style={{ width: 300, height: 300 }}>
<Group data-uid='group'>
${params.snippet}
</Group>
</div>
`,
})

describe('Constraints Section', () => {
describe('Frame Children', () => {
it(
'Span without size shows up as Left / Top constrained',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: 15, top: 15 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Left',
expectedHeightConstraintDropdownOption: 'Top',
}),
)
it(
'Span with width shows up as Left / Top constrained',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: 15, top: 15, width: 150 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Left',
expectedHeightConstraintDropdownOption: 'Top',
}),
)
it(
'Span with width,height shows up as Left / Top constrained',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: 15, top: 15, width: 150, height: 50 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Left',
expectedHeightConstraintDropdownOption: 'Top',
}),
)

it(
'Span without size bottom/right pinned shows up as Right / Bottom constrained',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', right: 15, bottom: 15 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Right',
expectedHeightConstraintDropdownOption: 'Bottom',
}),
)

it(
'Span with size bottom/right pinned shows up as Right / Bottom constrained',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', right: 15, bottom: 15, width: 100, height: 30 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Right',
expectedHeightConstraintDropdownOption: 'Bottom',
}),
)

it(
'Span with left, width, top, height as percentage shows up as Scale',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: '15%', top: '10%', width: '50%', height: '30%' }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Scale',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)
it(
'Span with left %, width fixed shows up as Mixed',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: '15%', top: '10%', width: 50, height: '30%' }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Mixed',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Span with left %, width missing shows up as Mixed',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: '15%', top: '10%', width: 50, height: '30%' }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Mixed',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Span with left top right bottom px shows up as Left and Right, Bottom and Top',
testFrameChild({
snippet: `<span data-uid='target' style={{ position: 'absolute', left: 15, top: 30, right: 15, bottom: 50 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Left and Right',
expectedHeightConstraintDropdownOption: 'Top and Bottom',
}),
)
})

describe('Group Children', () => {
it(
'Span without size shows up as Scale / Scale constrained',
testGroupChild({
snippet: `<span data-uid='target'>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Scale',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Span with only width shows up as Scale / Scale',
testGroupChild({
snippet: `<span data-uid='target' style={{ width: 150 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Scale',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Span with max-content width shows up as Width / Scale',
testGroupChild({
snippet: `<span data-uid='target' style={{ width: 'max-content' }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Scale',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Span with max-content width height shows up as Width / Height',
testGroupChild({
snippet: `<span data-uid='target' style={{ width: 'max-content', height: 'max-content' }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Scale',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Span with size shows up as Scale',
testGroupChild({
snippet: `<span data-uid='target' style={{ width: 150, height: 30 }}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Scale',
expectedHeightConstraintDropdownOption: 'Scale',
}),
)

it(
'Div with right and bottom pins and size shows up as Right / Bottom',
testGroupChild({
snippet: `<span data-uid='target' style={{ right: 150, bottom: 50, width: 150, height: 30 }} data-constraints={['right', 'bottom']}>An implicitly constrained span</span>`,
expectedWidthConstraintDropdownOption: 'Right',
expectedHeightConstraintDropdownOption: 'Bottom',
}),
)
})
})
Loading

0 comments on commit 6e92d12

Please sign in to comment.