Skip to content

Commit

Permalink
Respect spans when resizing grid cells (#6685)
Browse files Browse the repository at this point in the history
**Problem:**

Resizing grid cells always forces explicit numerical placement pins,
chomping any `span`s that may have been defined on the cell.

**Fix:**

After determining the right numerical positions for the resized cell
bounds, do a normalization pass so that the new grid props are rewritten
to respect the new bounds but also to express them with `spans` if the
original pins were spans in the first place.

For example, assuming enlarging to the right by 1 cell:

| Initial | Result |
|--------|----------|
| `gridColumn: span 2` | `gridColumn: span 3` |
| `gridColumn 3 / span 2` | `gridColumn: 3 / span 3` |

**Manual Tests:**
I hereby swear that:

- [x] I opened a hydrogen project and it loaded
- [x] I could navigate to various routes in Play mode

Fixes #6683
  • Loading branch information
ruggi authored and liady committed Dec 13, 2024
1 parent 69d83bc commit a584f20
Show file tree
Hide file tree
Showing 2 changed files with 300 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,185 @@ export var storyboard = (
gridRowEnd: 'auto',
})
})

describe('spans', () => {
it('respects column start spans', async () => {
const editor = await renderTestEditorWithCode(
makeProjectCodeWithCustomPlacement({ gridColumn: 'span 2', gridRow: '2' }),
'await-first-dom-report',
)

// enlarge to the right
{
await runCellResizeTest(
editor,
'column-end',
gridCellTargetId(EP.fromString('sb/grid'), 2, 3),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'auto',
gridColumnStart: 'span 3',
gridRowEnd: 'auto',
gridRowStart: '2',
})
}

// shrink from the left
{
await runCellResizeTest(
editor,
'column-start',
gridCellTargetId(EP.fromString('sb/grid'), 2, 2),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: '4',
gridColumnStart: 'span 2',
gridRowEnd: 'auto',
gridRowStart: '2',
})
}

// enlarge back from the left
{
await runCellResizeTest(
editor,
'column-start',
gridCellTargetId(EP.fromString('sb/grid'), 2, 1),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'auto',
gridColumnStart: 'span 3',
gridRowEnd: 'auto',
gridRowStart: '2',
})
}
})
it('respects column end spans', async () => {
const editor = await renderTestEditorWithCode(
makeProjectCodeWithCustomPlacement({ gridColumn: '2 / span 2', gridRow: '2' }),
'await-first-dom-report',
)

// enlarge to the right
{
await runCellResizeTest(
editor,
'column-end',
gridCellTargetId(EP.fromString('sb/grid'), 2, 4),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'span 3',
gridColumnStart: '2',
gridRowEnd: 'auto',
gridRowStart: '2',
})
}
})
it('respects row start spans', async () => {
const editor = await renderTestEditorWithCode(
makeProjectCodeWithCustomPlacement({ gridColumn: '2', gridRow: 'span 2' }),
'await-first-dom-report',
)

// enlarge to the bottom
{
await runCellResizeTest(
editor,
'row-end',
gridCellTargetId(EP.fromString('sb/grid'), 3, 2),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'auto',
gridColumnStart: '2',
gridRowEnd: 'auto',
gridRowStart: 'span 3',
})
}

// shrink from the top
{
await runCellResizeTest(
editor,
'row-start',
gridCellTargetId(EP.fromString('sb/grid'), 2, 2),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'auto',
gridColumnStart: '2',
gridRowEnd: '4',
gridRowStart: 'span 2',
})
}

// enlarge back from the top
{
await runCellResizeTest(
editor,
'row-start',
gridCellTargetId(EP.fromString('sb/grid'), 1, 2),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'auto',
gridColumnStart: '2',
gridRowEnd: 'auto',
gridRowStart: 'span 3',
})
}
})
it('respects row end spans', async () => {
const editor = await renderTestEditorWithCode(
makeProjectCodeWithCustomPlacement({ gridColumn: '2', gridRow: '2 / span 2' }),
'await-first-dom-report',
)

// enlarge to the bottom
{
await runCellResizeTest(
editor,
'row-end',
gridCellTargetId(EP.fromString('sb/grid'), 4, 2),
EP.fromString('sb/grid/cell'),
)

const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
editor.renderedDOM.getByTestId('cell').style
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
gridColumnEnd: 'auto',
gridColumnStart: '2',
gridRowEnd: 'span 3',
gridRowStart: '2',
})
}
})
})
})

const ProjectCode = `import * as React from 'react'
Expand Down Expand Up @@ -948,3 +1127,42 @@ export var storyboard = (
function unsafeCast<T>(a: unknown): T {
return a as T
}

function makeProjectCodeWithCustomPlacement(params: {
gridColumn: string
gridRow: string
}): string {
return `import * as React from 'react'
import { Storyboard } from 'utopia-api'
export var storyboard = (
<Storyboard data-uid='sb'>
<div
style={{
backgroundColor: '#aaaaaa33',
position: 'absolute',
width: 600,
height: 400,
display: 'grid',
gap: 10,
gridTemplateColumns: '1fr 1fr 1fr 1fr',
gridTemplateRows: '1fr 1fr 1fr 1fr',
}}
data-uid='grid'
>
<div
style={{
backgroundColor: '#aaaaaa33',
alignSelf: 'stretch',
justifySelf: 'stretch',
gridColumn: '${params.gridColumn}',
gridRow: '${params.gridRow}',
}}
data-uid='cell'
data-testid='cell'
/>
</div>
</Storyboard>
)
`
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import * as EP from '../../../../core/shared/element-path'
import type {
GridElementProperties,
GridPositionOrSpan,
GridPositionValue,
} from '../../../../core/shared/element-template'
import { gridSpanNumeric, isGridSpan } from '../../../../core/shared/element-template'
import {
type CanvasRectangle,
isInfinityRectangle,
rectangleIntersection,
} from '../../../../core/shared/math-utils'
import { gridContainerIdentifier, gridItemIdentifier } from '../../../editor/store/editor-state'
import { cssKeyword } from '../../../inspector/common/css-utils'
import { isFillOrStretchModeAppliedOnAnySide } from '../../../inspector/inspector-common'
import {
controlsForGridPlaceholders,
Expand All @@ -21,7 +28,7 @@ import {
strategyApplicationResult,
} from '../canvas-strategy-types'
import type { InteractionSession } from '../interaction-state'
import { findOriginalGrid, getCommandsForGridItemPlacement } from './grid-helpers'
import { getCommandsForGridItemPlacement } from './grid-helpers'
import { resizeBoundingBoxFromSide } from './resize-helpers'

export const gridResizeElementStrategy: CanvasStrategyFactory = (
Expand Down Expand Up @@ -104,15 +111,60 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
null,
)

const gridProps = getNewGridPropsFromResizeBox(resizeBoundingBox, allCellBounds)
const gridPropsNumeric = getNewGridPropsFromResizeBox(resizeBoundingBox, allCellBounds)

if (gridProps == null) {
if (gridPropsNumeric == null) {
return emptyStrategyApplicationResult
}

const gridTemplate =
selectedElementMetadata.specialSizeMeasurements.parentContainerGridProperties

const elementGridPropertiesFromProps =
selectedElementMetadata.specialSizeMeasurements.elementGridPropertiesFromProps

const columnCount =
gridPropsNumeric.gridColumnEnd.numericalPosition -
gridPropsNumeric.gridColumnStart.numericalPosition
const rowCount =
gridPropsNumeric.gridRowEnd.numericalPosition -
gridPropsNumeric.gridRowStart.numericalPosition

const gridProps: GridElementProperties = {
gridColumnStart: normalizePositionAfterResize(
elementGridPropertiesFromProps.gridColumnStart,
gridPropsNumeric.gridColumnStart,
columnCount,
'start',
elementGridPropertiesFromProps.gridColumnEnd,
gridPropsNumeric.gridColumnEnd,
),
gridColumnEnd: normalizePositionAfterResize(
elementGridPropertiesFromProps.gridColumnEnd,
gridPropsNumeric.gridColumnEnd,
columnCount,
'end',
elementGridPropertiesFromProps.gridColumnStart,
gridPropsNumeric.gridColumnStart,
),
gridRowStart: normalizePositionAfterResize(
elementGridPropertiesFromProps.gridRowStart,
gridPropsNumeric.gridRowStart,
rowCount,
'start',
elementGridPropertiesFromProps.gridRowEnd,
gridPropsNumeric.gridRowEnd,
),
gridRowEnd: normalizePositionAfterResize(
elementGridPropertiesFromProps.gridRowEnd,
gridPropsNumeric.gridRowEnd,
rowCount,
'end',
elementGridPropertiesFromProps.gridRowStart,
gridPropsNumeric.gridRowStart,
),
}

return strategyApplicationResult(
getCommandsForGridItemPlacement(selectedElement, gridTemplate, gridProps),
[EP.parentPath(selectedElement)],
Expand Down Expand Up @@ -158,3 +210,30 @@ function getNewGridPropsFromResizeBox(
gridColumnEnd: { numericalPosition: newColumnEnd },
}
}

// After a resize happens and we know the numerical grid positioning of the new bounds,
// return a normalized version of the new position so that it respects any spans that
// may have been there before the resize, and/or default it to 'auto' when it would become redundant.
function normalizePositionAfterResize(
position: GridPositionOrSpan | null,
resizedPosition: GridPositionValue,
size: number, // the number of cols/rows the cell occupies
bound: 'start' | 'end',
counterpart: GridPositionOrSpan | null,
counterpartResizedPosition: GridPositionValue,
): GridPositionOrSpan | null {
if (isGridSpan(position)) {
if (size === 1) {
return cssKeyword('auto')
}
return gridSpanNumeric(size)
}
if (
isGridSpan(counterpart) &&
counterpartResizedPosition.numericalPosition === 1 &&
bound === 'end'
) {
return cssKeyword('auto')
}
return resizedPosition
}

0 comments on commit a584f20

Please sign in to comment.