diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx index 2ddbdf0315ce..1f07c6144cce 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx @@ -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' @@ -948,3 +1127,42 @@ export var storyboard = ( function unsafeCast(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 = ( + +
+
+
+ +) +` +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts index 8fe12fcc8849..7e3ec36af17c 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts @@ -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, @@ -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 = ( @@ -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)], @@ -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 +}