Skip to content

Commit

Permalink
absolute duplicate flex child (#4310)
Browse files Browse the repository at this point in the history
* absolute duplicate flex child

* add contain: layout if necessary

* tests

* update tests
  • Loading branch information
bkrmendy authored Oct 4, 2023
1 parent d874a60 commit 816eb5c
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('Absolute Duplicate Strategy', () => {

expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
makeTestProjectCodeWithSnippet(`
<div style={{ width: '100%', height: '100%', position: 'relative' }} data-uid='aaa'>
<div style={{ width: '100%', height: '100%', position: 'relative', contain: 'layout' }} data-uid='aaa'>
<div
style={{ backgroundColor: '#aaaaaa33', position: 'absolute', left: 40, top: 50, width: 200, height: 120 }}
data-uid='hello'
Expand All @@ -97,6 +97,52 @@ describe('Absolute Duplicate Strategy', () => {
)
})

it('duplicates flex child when pressing alt', async () => {
const renderResult = await renderTestEditorWithCode(
makeTestProjectCodeWithSnippet(`
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', gap: 42, padding: '56px 45px 56px 43px' }} data-uid='aaa'>
<div
style={{ backgroundColor: '#d382c5', width: 200, height: 120 }}
data-uid='bbb'
data-testid='bbb'
/>
</div>
`),
'await-first-dom-report',
)

expectElementWithTestIdNotToBeRendered(renderResult, ImmediateParentOutlinesTestId([]))
expectElementWithTestIdNotToBeRendered(renderResult, ImmediateParentBoundsTestId([]))

const target = EP.appendNewElementPath(TestScenePath, ['container', 'aaa', 'bbb'])

FOR_TESTS_setNextGeneratedUid('hello')
const dragDelta = windowPoint({ x: 40, y: -25 })
await dragElement(renderResult, 'bbb', dragDelta, altModifier, () => {
expectElementWithTestIdNotToBeRendered(renderResult, ImmediateParentOutlinesTestId([target]))
expectElementWithTestIdNotToBeRendered(renderResult, ImmediateParentBoundsTestId([target]))
})

await renderResult.getDispatchFollowUpActionsFinished()

expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
makeTestProjectCodeWithSnippet(`
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', gap: 42, padding: '56px 45px 56px 43px', contain: 'layout' }} data-uid='aaa'>
<div
style={{ backgroundColor: '#d382c5', width: 200, height: 120 }}
data-uid='hello'
data-testid='bbb'
/>
<div
style={{ backgroundColor: '#d382c5', width: 200, height: 120, position: 'absolute', left: 83, top: 31 }}
data-uid='bbb'
data-testid='bbb'
/>
</div>
`),
)
})

it('duplicates the selected absolute element when pressing alt, even if the parent is static', async () => {
const renderResult = await renderTestEditorWithCode(
makeTestProjectCodeWithSnippet(`
Expand All @@ -119,7 +165,7 @@ describe('Absolute Duplicate Strategy', () => {

expect(getPrintedUiJsCode(renderResult.getEditorState())).toEqual(
makeTestProjectCodeWithSnippet(`
<div style={{ width: '100%', height: '100%' }} data-uid='aaa'>
<div style={{ width: '100%', height: '100%', contain: 'layout' }} data-uid='aaa'>
<div
style={{ backgroundColor: '#aaaaaa33', position: 'absolute', left: 40, top: 50, width: 200, height: 120 }}
data-uid='hello'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { generateUidWithExistingComponents } from '../../../../core/model/element-template-utils'
import * as EP from '../../../../core/shared/element-path'
import type { ElementPath } from '../../../../core/shared/project-file-types'
import type {
AllElementProps,
DerivedState,
EditorState,
EditorStatePatch,
Expand All @@ -26,12 +26,18 @@ import type {
} from '../canvas-strategy-types'
import {
controlWithProps,
defaultCustomStrategyState,
getTargetPathsFromInteractionTarget,
strategyApplicationResult,
} from '../canvas-strategy-types'
import type { InteractionSession } from '../interaction-state'
import { flattenSelection } from './shared-move-strategies-helpers'
import { replaceFragmentLikePathsWithTheirChildrenRecursive } from './fragment-like-helpers'
import { getElementFragmentLikeType } from './fragment-like-helpers'
import { setProperty } from '../../commands/set-property-command'
import * as PP from '../../../../core/shared/property-path'
import type { ElementInstanceMetadataMap } from '../../../../core/shared/element-template'
import type { ElementPathTrees } from '../../../../core/shared/element-path-tree'
import { strictEvery } from '../../../../core/shared/array-utils'

export function absoluteDuplicateStrategy(
canvasState: InteractionCanvasState,
Expand All @@ -52,10 +58,14 @@ export function absoluteDuplicateStrategy(
const isDragging = interactionSession.interactionData.drag != null
const flattenedSelectionForMultiSelect = flattenSelection(selectedElements)

if (!isApplicable(canvasState, flattenedSelectionForMultiSelect)) {
const result = isApplicable(flattenedSelectionForMultiSelect)

if (result.type === 'not-applicable') {
return null
}

const { commonParentPath } = result

return {
id: 'ABSOLUTE_DUPLICATE',
name: 'Duplicate',
Expand Down Expand Up @@ -98,11 +108,25 @@ export function absoluteDuplicateStrategy(

newPaths.push(newPath)
duplicateCommands.push(duplicateElement('always', selectedElement, newUid, 'before'))

if (
!isElementSizelessDiv(
canvasState.startingMetadata,
canvasState.startingAllElementProps,
canvasState.startingElementPathTree,
selectedElement,
)
) {
duplicateCommands.push(
setProperty('always', selectedElement, PP.create('style', 'position'), 'absolute'),
)
}
})

return strategyApplicationResult(
[
...duplicateCommands,
...maybeAddContainLayoutCommand(commonParentPath),
setElementsToRerenderCommand([...selectedElements, ...newPaths]),
updateSelectedViews('always', selectedElements),
updateFunctionCommand('always', (editorState, derivedState, commandLifecycle) =>
Expand Down Expand Up @@ -138,18 +162,25 @@ function runMoveStrategy(
strategyLifecycle: InteractionLifecycle,
): Array<EditorStatePatch> {
const moveCommands =
absoluteMoveStrategy(canvasState, interactionSession)?.strategy.apply(strategyLifecycle)
.commands ?? []
absoluteMoveStrategy(
canvasState,
interactionSession,
defaultCustomStrategyState(),
'do-not-run-applicability-check',
)?.strategy.apply(strategyLifecycle).commands ?? []

return foldAndApplyCommandsInner(editorState, derivedState, [], moveCommands, commandLifecycle)
.statePatches
}

function isApplicable(
canvasState: InteractionCanvasState,
filteredSelectedElements: ElementPath[],
) {
return filteredSelectedElements.every((element) => {
type IsAbsoluteMoveApplicableResult =
| {
type: 'not-applicable'
}
| { type: 'applicable'; commonParentPath: ElementPath }

function isApplicable(filteredSelectedElements: ElementPath[]): IsAbsoluteMoveApplicableResult {
const applicable = strictEvery(filteredSelectedElements, (element) => {
// for a multiselected elements, we only apply drag-to-duplicate if they are siblings
// otherwise this would lead to an unpredictable behavior
// we can revisit this once we have a more predictable reparenting
Expand All @@ -158,23 +189,30 @@ function isApplicable(
EP.parentPath(element),
)

const unrolledChildren = replaceFragmentLikePathsWithTheirChildrenRecursive(
canvasState.startingMetadata,
canvasState.startingAllElementProps,
canvasState.startingElementPathTree,
[element],
)
return !EP.isRootElementOfInstance(element) && allDraggedElementsHaveTheSameParent
})

const isElementAbsolute = unrolledChildren.every((path) =>
MetadataUtils.isPositionAbsolute(
MetadataUtils.findElementByElementPath(canvasState.startingMetadata, path),
),
)
if (!applicable) {
return { type: 'not-applicable' }
}

return (
!EP.isRootElementOfInstance(element) &&
allDraggedElementsHaveTheSameParent &&
isElementAbsolute
)
})
return { type: 'applicable', commonParentPath: EP.parentPath(filteredSelectedElements[0]) }
}

function isElementSizelessDiv(
metadata: ElementInstanceMetadataMap,
allElementProps: AllElementProps,
elementPathTrees: ElementPathTrees,
elementPath: ElementPath,
): boolean {
return (
getElementFragmentLikeType(metadata, allElementProps, elementPathTrees, elementPath) ===
'sizeless-div'
)
}

function maybeAddContainLayoutCommand(elementPath: ElementPath): CanvasCommand[] {
return EP.isStoryboardPath(elementPath)
? []
: [setProperty('always', elementPath, PP.create('style', 'contain'), 'layout')]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { ImmediateParentBounds } from '../../controls/parent-bounds'
import { ImmediateParentOutlines } from '../../controls/parent-outlines'
import { ZeroSizedElementControls } from '../../controls/zero-sized-element-controls'
import type { InteractionCanvasState, MoveStrategy } from '../canvas-strategy-types'
import type {
CustomStrategyState,
InteractionCanvasState,
MoveStrategy,
} from '../canvas-strategy-types'
import {
controlWithProps,
emptyStrategyApplicationResult,
Expand All @@ -17,9 +21,15 @@ import {
flattenSelection,
} from './shared-move-strategies-helpers'

export type ShouldRunApplicabilityCheck =
| 'run-applicability-check'
| 'do-not-run-applicability-check'

export function absoluteMoveStrategy(
canvasState: InteractionCanvasState,
interactionSession: InteractionSession | null,
_: CustomStrategyState,
runApplicabilityCheck: ShouldRunApplicabilityCheck = 'run-applicability-check',
): MoveStrategy | null {
const originalTargets = flattenSelection(
getTargetPathsFromInteractionTarget(canvasState.interactionTarget),
Expand All @@ -39,7 +49,7 @@ export function absoluteMoveStrategy(
)
})

if (!isApplicable) {
if (runApplicabilityCheck === 'run-applicability-check' && !isApplicable) {
return null
}
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function dragByPixels(
startingAllElementProps,
),
interactionSession,
defaultCustomStrategyState(),
)!.strategy.apply('end-interaction')

expect(strategyResult.customStatePatch).toEqual({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from '../canvas-strategy-types'
import {
controlWithProps,
defaultCustomStrategyState,
emptyStrategyApplicationResult,
getTargetPathsFromInteractionTarget,
strategyApplicationResult,
Expand Down Expand Up @@ -177,10 +178,14 @@ export function baseAbsoluteReparentStrategy(
})

const moveCommands =
absoluteMoveStrategy(canvasState, {
...interactionSession,
updatedTargetPaths: updatedTargetPaths,
})?.strategy.apply(strategyLifecycle).commands ?? []
absoluteMoveStrategy(
canvasState,
{
...interactionSession,
updatedTargetPaths: updatedTargetPaths,
},
defaultCustomStrategyState(),
)?.strategy.apply(strategyLifecycle).commands ?? []

const elementsToRerender = EP.uniqueElementPaths([
...customStrategyState.elementsToRerender,
Expand All @@ -202,9 +207,11 @@ export function baseAbsoluteReparentStrategy(
)
} else {
const moveCommands =
absoluteMoveStrategy(canvasState, interactionSession)?.strategy.apply(
strategyLifecycle,
).commands ?? []
absoluteMoveStrategy(
canvasState,
interactionSession,
defaultCustomStrategyState(),
)?.strategy.apply(strategyLifecycle).commands ?? []
return strategyApplicationResult(moveCommands)
}
},
Expand Down

0 comments on commit 816eb5c

Please sign in to comment.