Skip to content

Commit

Permalink
fix(grids) Maintain Grid Cell Pins. (#6691)
Browse files Browse the repository at this point in the history
- Added `printPinAsString` utility function.
- Implemented `getGridRelativeContainingBlock` to calculate a new
containing block by
building a minimal purely HTML reproduction of the grid so as to use the
browser
  logic for the position and size of the containing block.
- Refactored out `getNewGridElementProps` from the start of
`runGridChangeElementLocation`
  so that it can be used to identify the new grid position on its own.
- Refactored out `getMoveCommandsForDrag` from
`getMoveCommandsForSelectedElement`.
- Replaced `gridChildAbsoluteMoveCommands` with a call to
`getMoveCommandsForDrag`
  in the Grid Absolute Move strategy.
- Added `GridElementChildContainingBlockKey` for simplicity.
  • Loading branch information
seanparsons authored and liady committed Dec 13, 2024
1 parent 76855f9 commit 78a7eaf
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
getCommandsForGridItemPlacement,
sortElementsByGridPosition,
} from './grid-helpers'
import type { GridCellCoordinates } from './grid-cell-bounds'

export const gridChangeElementLocationStrategy: CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
Expand Down Expand Up @@ -171,20 +172,23 @@ function getCommandsAndPatchForGridChangeElementLocation(
}
}

export function runGridChangeElementLocation(
jsxMetadata: ElementInstanceMetadataMap,
interface NewGridElementProps {
gridElementProperties: GridElementProperties
targetCellCoords: GridCellCoordinates
targetRootCell: GridCellCoordinates
}

export function getNewGridElementProps(
interactionData: DragInteractionData,
selectedElementMetadata: ElementInstanceMetadata,
gridCellGlobalFrames: GridCellGlobalFrames,
gridTemplate: GridContainerProperties,
newPathAfterReparent: ElementPath | null,
): CanvasCommand[] {
): NewGridElementProps | null {
if (interactionData.drag == null) {
return []
return null
}

const isReparent = newPathAfterReparent != null
const pathForCommands = isReparent ? newPathAfterReparent : selectedElementMetadata.elementPath // when reparenting, we want to use the new path for commands

const gridConfig = isReparent
? {
Expand All @@ -197,7 +201,7 @@ export function runGridChangeElementLocation(
selectedElementMetadata,
)
if (gridConfig == null) {
return []
return null
}
const { mouseCellPosInOriginalElement, originalCellBounds } = gridConfig

Expand All @@ -207,7 +211,7 @@ export function runGridChangeElementLocation(
mouseCellPosInOriginalElement,
)
if (targetGridCellData == null) {
return []
return null
}
const { targetCellCoords, targetRootCell } = targetGridCellData

Expand Down Expand Up @@ -283,10 +287,39 @@ export function runGridChangeElementLocation(
gridRowEnd: rowBounds.end,
}

return {
gridElementProperties: gridProps,
targetCellCoords: targetCellCoords,
targetRootCell: targetRootCell,
}
}

export function runGridChangeElementLocation(
jsxMetadata: ElementInstanceMetadataMap,
interactionData: DragInteractionData,
selectedElementMetadata: ElementInstanceMetadata,
gridCellGlobalFrames: GridCellGlobalFrames,
gridTemplate: GridContainerProperties,
newPathAfterReparent: ElementPath | null,
): CanvasCommand[] {
const newGridElementProps = getNewGridElementProps(
interactionData,
selectedElementMetadata,
gridCellGlobalFrames,
newPathAfterReparent,
)
if (newGridElementProps == null) {
return []
}
const { gridElementProperties, targetCellCoords, targetRootCell } = newGridElementProps

const isReparent = newPathAfterReparent != null
const pathForCommands = isReparent ? newPathAfterReparent : selectedElementMetadata.elementPath // when reparenting, we want to use the new path for commands

const gridCellMoveCommands = getCommandsForGridItemPlacement(
pathForCommands,
gridTemplate,
gridProps,
gridElementProperties,
)

// The siblings of the grid element being moved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,101 @@ export var storyboard = (
</Storyboard>
)
`

const ProjectCodeWithBRPins = `import { Scene, Storyboard } from 'utopia-api'
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
id='playground-scene'
commentId='playground-scene'
style={{
width: 700,
height: 759,
position: 'absolute',
left: 212,
top: 128,
}}
data-label='Playground'
data-uid='scene'
>
<div
data-uid='grid'
style={{
backgroundColor: '#fefefe',
position: 'absolute',
left: 123,
top: 133,
width: 461,
height: 448,
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: '1fr 1fr',
gridGap: 0,
}}
>
<div
data-uid='child'
data-testid='child'
style={{
backgroundColor: '#59a6ed',
position: 'absolute',
right: 12,
bottom: 16,
width: 144,
height: 134,
gridColumn: 1,
gridRow: 1,
}}
/>
</div>
</Scene>
</Storyboard>
)
`

it('can move absolute element inside a grid cell maintaining right and bottom pins', async () => {
const editor = await renderTestEditorWithCode(
ProjectCodeWithBRPins,
'await-first-dom-report',
)

const child = editor.renderedDOM.getByTestId('child')

{
const { bottom, right, gridColumn, gridRow } = child.style
expect({ bottom, right, gridColumn, gridRow }).toEqual({
gridColumn: '1',
gridRow: '1',
right: '12px',
bottom: '16px',
})
}

await selectComponentsForTest(editor, [EP.fromString('sb/scene/grid/child')])

const childBounds = child.getBoundingClientRect()
const childCenter = windowPoint({
x: Math.floor(childBounds.left + childBounds.width / 2),
y: Math.floor(childBounds.top + childBounds.height / 2),
})

const endPoint = offsetPoint(childCenter, windowPoint({ x: 20, y: 20 }))

const dragTarget = editor.renderedDOM.getByTestId(
GridCellTestId(EP.fromString('sb/scene/grid/child')),
)
await mouseDownAtPoint(dragTarget, childCenter)
await mouseMoveToPoint(dragTarget, endPoint)
await mouseUpAtPoint(dragTarget, endPoint)

const { bottom, right, gridColumn, gridRow } = child.style
expect({ bottom, right, gridColumn, gridRow }).toEqual({
gridColumn: '1',
gridRow: '1',
right: '-8px',
bottom: '-4px',
})
})
it('can move absolute element inside a grid cell', async () => {
const editor = await renderTestEditorWithCode(ProjectCode, 'await-first-dom-report')

Expand Down Expand Up @@ -729,7 +824,7 @@ export var storyboard = (
gridRowStart: '1',
gridRowEnd: 'auto',
left: '59px',
top: '59.5px',
top: '60px',
})
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
type GridContainerProperties,
type GridElementProperties,
} from '../../../../core/shared/element-template'
import type { CanvasRectangle } from '../../../../core/shared/math-utils'
import {
localRectangle,
Size,
zeroRectIfNullOrInfinity,
type CanvasRectangle,
type LocalRectangle,
} from '../../../../core/shared/math-utils'
import * as PP from '../../../../core/shared/property-path'
import { assertNever } from '../../../../core/shared/utils'
import type { GridDimension } from '../../../inspector/common/css-utils'
Expand Down Expand Up @@ -727,3 +733,155 @@ export function gridIdentifierToString(identifier: GridIdentifier): string {
assertNever(identifier)
}
}

function printPinAsString(
gridTemplate: GridContainerProperties,
pin: GridPositionOrSpan,
axis: 'row' | 'column',
): string {
const printPinResult = printPin(gridTemplate, pin, axis)
if (typeof printPinResult === 'number') {
return `${printPinResult}`
} else {
return printPinResult
}
}

const TemporaryGridID = 'temporary-grid'

export function getGridRelativeContainingBlock(
gridMetadata: ElementInstanceMetadata,
cellMetadata: ElementInstanceMetadata,
cellProperties: GridElementProperties,
): LocalRectangle {
const gridProperties = gridMetadata.specialSizeMeasurements.containerGridProperties

// Create containing fragment.
const fragment = document.createDocumentFragment()

// Create offset container that potentially provides the layout positioning.
const offsetContainer = document.createElement('div')
offsetContainer.id = TemporaryGridID
offsetContainer.style.position = 'absolute'
offsetContainer.style.left = '0'
offsetContainer.style.top = '0'
fragment.appendChild(offsetContainer)

// Create a grid element with the appropriate properties.
const gridElement = document.createElement('div')
gridElement.style.display = 'grid'
gridElement.style.position = gridMetadata.specialSizeMeasurements.position ?? 'initial'
const gridGlobalFrame = zeroRectIfNullOrInfinity(gridMetadata.globalFrame)
gridElement.style.left = `${gridGlobalFrame.x}px`
gridElement.style.top = `${gridGlobalFrame.y}px`
gridElement.style.width = `${gridGlobalFrame.width}px`
gridElement.style.height = `${gridGlobalFrame.height}px`

// Gap needs to be set only if the other two are not present or we'll have rendering issues
// due to how measurements are calculated.
if (
gridMetadata.specialSizeMeasurements.rowGap != null &&
gridMetadata.specialSizeMeasurements.columnGap != null
) {
const gap = gridMetadata.specialSizeMeasurements.gap
gridElement.style.gap = gap == null ? 'initial' : `${gap}px`
} else {
const rowGap = gridMetadata.specialSizeMeasurements.rowGap
gridElement.style.rowGap = rowGap == null ? 'initial' : `${rowGap}px`
const columnGap = gridMetadata.specialSizeMeasurements.columnGap
gridElement.style.columnGap = columnGap == null ? 'initial' : `${columnGap}px`
}

// Include the padding.
const gridPadding = gridMetadata.specialSizeMeasurements.padding
gridElement.style.paddingLeft = gridPadding.left == null ? 'initial' : `${gridPadding.left}px`
gridElement.style.paddingTop = gridPadding.top == null ? 'initial' : `${gridPadding.top}px`
gridElement.style.paddingRight = gridPadding.right == null ? 'initial' : `${gridPadding.right}px`
gridElement.style.paddingBottom =
gridPadding.bottom == null ? 'initial' : `${gridPadding.bottom}px`

// Keep the grid hidden from view so that it doesn't flash visibly in the editor.
gridElement.style.visibility = 'hidden'

if (gridProperties.gridTemplateColumns != null) {
gridElement.style.gridTemplateColumns = printGridAutoOrTemplateBase(
gridProperties.gridTemplateColumns,
)
}
if (gridProperties.gridTemplateRows != null) {
gridElement.style.gridTemplateRows = printGridAutoOrTemplateBase(
gridProperties.gridTemplateRows,
)
}
if (gridProperties.gridAutoColumns != null) {
gridElement.style.gridAutoColumns = printGridAutoOrTemplateBase(gridProperties.gridAutoColumns)
}
if (gridProperties.gridAutoRows != null) {
gridElement.style.gridAutoRows = printGridAutoOrTemplateBase(gridProperties.gridAutoRows)
}
if (gridProperties.gridAutoFlow != null) {
gridElement.style.gridAutoFlow = gridProperties.gridAutoFlow
}
offsetContainer.appendChild(gridElement)

// Create a child of the grid element with the appropriate properties.
const gridChildElement = document.createElement('div')

if (cellProperties.gridColumnStart != null) {
gridChildElement.style.gridColumnStart = printPinAsString(
gridProperties,
cellProperties.gridColumnStart,
'column',
)
}
if (cellProperties.gridColumnEnd != null) {
gridChildElement.style.gridColumnEnd = printPinAsString(
gridProperties,
cellProperties.gridColumnEnd,
'column',
)
}
if (cellProperties.gridRowStart != null) {
gridChildElement.style.gridRowStart = printPinAsString(
gridProperties,
cellProperties.gridRowStart,
'row',
)
}
if (cellProperties.gridRowEnd != null) {
gridChildElement.style.gridRowEnd = printPinAsString(
gridProperties,
cellProperties.gridRowEnd,
'row',
)
}
gridChildElement.style.position = cellMetadata.specialSizeMeasurements.position ?? 'initial'
// Fill out the entire space available.
gridChildElement.style.top = '0'
gridChildElement.style.left = '0'
gridChildElement.style.bottom = '0'
gridChildElement.style.right = '0'

gridElement.appendChild(gridChildElement)

// Get the result and cleanup the temporary elements.
try {
document.body.appendChild(fragment)
const gridProvidesBounds =
gridMetadata.specialSizeMeasurements.providesBoundsForAbsoluteChildren
const boundingRect = gridChildElement.getBoundingClientRect()
// If the grid provides the bounds, then we need to remove it's position, otherwise
// we need to include its position in the offset container.
return localRectangle({
x: boundingRect.left - (gridProvidesBounds ? gridGlobalFrame.x : 0),
y: boundingRect.top - (gridProvidesBounds ? gridGlobalFrame.y : 0),
width: boundingRect.width,
height: boundingRect.height,
})
} finally {
const gridElementFromDocument = document.getElementById(TemporaryGridID)
if (gridElementFromDocument != null) {
gridElementFromDocument.remove()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,12 @@ export var storyboard = (
style={{
backgroundColor: '#f0f',
position: 'absolute',
left: 300,
top: 300,
width: 79,
height: 86,
gridColumn: 1,
gridRow: 1,
top: 300,
left: 300,
}}
data-uid='dragme'
data-testid='dragme'
Expand Down
Loading

0 comments on commit 78a7eaf

Please sign in to comment.