Skip to content

Commit 6b3c5a2

Browse files
authored
Respect spans when resizing grid cells (#6685)
**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
1 parent a9a93c2 commit 6b3c5a2

File tree

2 files changed

+300
-3
lines changed

2 files changed

+300
-3
lines changed

editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx

+218
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,185 @@ export var storyboard = (
671671
gridRowEnd: 'auto',
672672
})
673673
})
674+
675+
describe('spans', () => {
676+
it('respects column start spans', async () => {
677+
const editor = await renderTestEditorWithCode(
678+
makeProjectCodeWithCustomPlacement({ gridColumn: 'span 2', gridRow: '2' }),
679+
'await-first-dom-report',
680+
)
681+
682+
// enlarge to the right
683+
{
684+
await runCellResizeTest(
685+
editor,
686+
'column-end',
687+
gridCellTargetId(EP.fromString('sb/grid'), 2, 3),
688+
EP.fromString('sb/grid/cell'),
689+
)
690+
691+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
692+
editor.renderedDOM.getByTestId('cell').style
693+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
694+
gridColumnEnd: 'auto',
695+
gridColumnStart: 'span 3',
696+
gridRowEnd: 'auto',
697+
gridRowStart: '2',
698+
})
699+
}
700+
701+
// shrink from the left
702+
{
703+
await runCellResizeTest(
704+
editor,
705+
'column-start',
706+
gridCellTargetId(EP.fromString('sb/grid'), 2, 2),
707+
EP.fromString('sb/grid/cell'),
708+
)
709+
710+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
711+
editor.renderedDOM.getByTestId('cell').style
712+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
713+
gridColumnEnd: '4',
714+
gridColumnStart: 'span 2',
715+
gridRowEnd: 'auto',
716+
gridRowStart: '2',
717+
})
718+
}
719+
720+
// enlarge back from the left
721+
{
722+
await runCellResizeTest(
723+
editor,
724+
'column-start',
725+
gridCellTargetId(EP.fromString('sb/grid'), 2, 1),
726+
EP.fromString('sb/grid/cell'),
727+
)
728+
729+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
730+
editor.renderedDOM.getByTestId('cell').style
731+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
732+
gridColumnEnd: 'auto',
733+
gridColumnStart: 'span 3',
734+
gridRowEnd: 'auto',
735+
gridRowStart: '2',
736+
})
737+
}
738+
})
739+
it('respects column end spans', async () => {
740+
const editor = await renderTestEditorWithCode(
741+
makeProjectCodeWithCustomPlacement({ gridColumn: '2 / span 2', gridRow: '2' }),
742+
'await-first-dom-report',
743+
)
744+
745+
// enlarge to the right
746+
{
747+
await runCellResizeTest(
748+
editor,
749+
'column-end',
750+
gridCellTargetId(EP.fromString('sb/grid'), 2, 4),
751+
EP.fromString('sb/grid/cell'),
752+
)
753+
754+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
755+
editor.renderedDOM.getByTestId('cell').style
756+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
757+
gridColumnEnd: 'span 3',
758+
gridColumnStart: '2',
759+
gridRowEnd: 'auto',
760+
gridRowStart: '2',
761+
})
762+
}
763+
})
764+
it('respects row start spans', async () => {
765+
const editor = await renderTestEditorWithCode(
766+
makeProjectCodeWithCustomPlacement({ gridColumn: '2', gridRow: 'span 2' }),
767+
'await-first-dom-report',
768+
)
769+
770+
// enlarge to the bottom
771+
{
772+
await runCellResizeTest(
773+
editor,
774+
'row-end',
775+
gridCellTargetId(EP.fromString('sb/grid'), 3, 2),
776+
EP.fromString('sb/grid/cell'),
777+
)
778+
779+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
780+
editor.renderedDOM.getByTestId('cell').style
781+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
782+
gridColumnEnd: 'auto',
783+
gridColumnStart: '2',
784+
gridRowEnd: 'auto',
785+
gridRowStart: 'span 3',
786+
})
787+
}
788+
789+
// shrink from the top
790+
{
791+
await runCellResizeTest(
792+
editor,
793+
'row-start',
794+
gridCellTargetId(EP.fromString('sb/grid'), 2, 2),
795+
EP.fromString('sb/grid/cell'),
796+
)
797+
798+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
799+
editor.renderedDOM.getByTestId('cell').style
800+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
801+
gridColumnEnd: 'auto',
802+
gridColumnStart: '2',
803+
gridRowEnd: '4',
804+
gridRowStart: 'span 2',
805+
})
806+
}
807+
808+
// enlarge back from the top
809+
{
810+
await runCellResizeTest(
811+
editor,
812+
'row-start',
813+
gridCellTargetId(EP.fromString('sb/grid'), 1, 2),
814+
EP.fromString('sb/grid/cell'),
815+
)
816+
817+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
818+
editor.renderedDOM.getByTestId('cell').style
819+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
820+
gridColumnEnd: 'auto',
821+
gridColumnStart: '2',
822+
gridRowEnd: 'auto',
823+
gridRowStart: 'span 3',
824+
})
825+
}
826+
})
827+
it('respects row end spans', async () => {
828+
const editor = await renderTestEditorWithCode(
829+
makeProjectCodeWithCustomPlacement({ gridColumn: '2', gridRow: '2 / span 2' }),
830+
'await-first-dom-report',
831+
)
832+
833+
// enlarge to the bottom
834+
{
835+
await runCellResizeTest(
836+
editor,
837+
'row-end',
838+
gridCellTargetId(EP.fromString('sb/grid'), 4, 2),
839+
EP.fromString('sb/grid/cell'),
840+
)
841+
842+
const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } =
843+
editor.renderedDOM.getByTestId('cell').style
844+
expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({
845+
gridColumnEnd: 'auto',
846+
gridColumnStart: '2',
847+
gridRowEnd: 'span 3',
848+
gridRowStart: '2',
849+
})
850+
}
851+
})
852+
})
674853
})
675854

676855
const ProjectCode = `import * as React from 'react'
@@ -948,3 +1127,42 @@ export var storyboard = (
9481127
function unsafeCast<T>(a: unknown): T {
9491128
return a as T
9501129
}
1130+
1131+
function makeProjectCodeWithCustomPlacement(params: {
1132+
gridColumn: string
1133+
gridRow: string
1134+
}): string {
1135+
return `import * as React from 'react'
1136+
import { Storyboard } from 'utopia-api'
1137+
1138+
export var storyboard = (
1139+
<Storyboard data-uid='sb'>
1140+
<div
1141+
style={{
1142+
backgroundColor: '#aaaaaa33',
1143+
position: 'absolute',
1144+
width: 600,
1145+
height: 400,
1146+
display: 'grid',
1147+
gap: 10,
1148+
gridTemplateColumns: '1fr 1fr 1fr 1fr',
1149+
gridTemplateRows: '1fr 1fr 1fr 1fr',
1150+
}}
1151+
data-uid='grid'
1152+
>
1153+
<div
1154+
style={{
1155+
backgroundColor: '#aaaaaa33',
1156+
alignSelf: 'stretch',
1157+
justifySelf: 'stretch',
1158+
gridColumn: '${params.gridColumn}',
1159+
gridRow: '${params.gridRow}',
1160+
}}
1161+
data-uid='cell'
1162+
data-testid='cell'
1163+
/>
1164+
</div>
1165+
</Storyboard>
1166+
)
1167+
`
1168+
}

editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.ts

+82-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
22
import * as EP from '../../../../core/shared/element-path'
3+
import type {
4+
GridElementProperties,
5+
GridPositionOrSpan,
6+
GridPositionValue,
7+
} from '../../../../core/shared/element-template'
8+
import { gridSpanNumeric, isGridSpan } from '../../../../core/shared/element-template'
39
import {
410
type CanvasRectangle,
511
isInfinityRectangle,
612
rectangleIntersection,
713
} from '../../../../core/shared/math-utils'
814
import { gridContainerIdentifier, gridItemIdentifier } from '../../../editor/store/editor-state'
15+
import { cssKeyword } from '../../../inspector/common/css-utils'
916
import { isFillOrStretchModeAppliedOnAnySide } from '../../../inspector/inspector-common'
1017
import {
1118
controlsForGridPlaceholders,
@@ -21,7 +28,7 @@ import {
2128
strategyApplicationResult,
2229
} from '../canvas-strategy-types'
2330
import type { InteractionSession } from '../interaction-state'
24-
import { findOriginalGrid, getCommandsForGridItemPlacement } from './grid-helpers'
31+
import { getCommandsForGridItemPlacement } from './grid-helpers'
2532
import { resizeBoundingBoxFromSide } from './resize-helpers'
2633

2734
export const gridResizeElementStrategy: CanvasStrategyFactory = (
@@ -104,15 +111,60 @@ export const gridResizeElementStrategy: CanvasStrategyFactory = (
104111
null,
105112
)
106113

107-
const gridProps = getNewGridPropsFromResizeBox(resizeBoundingBox, allCellBounds)
114+
const gridPropsNumeric = getNewGridPropsFromResizeBox(resizeBoundingBox, allCellBounds)
108115

109-
if (gridProps == null) {
116+
if (gridPropsNumeric == null) {
110117
return emptyStrategyApplicationResult
111118
}
112119

113120
const gridTemplate =
114121
selectedElementMetadata.specialSizeMeasurements.parentContainerGridProperties
115122

123+
const elementGridPropertiesFromProps =
124+
selectedElementMetadata.specialSizeMeasurements.elementGridPropertiesFromProps
125+
126+
const columnCount =
127+
gridPropsNumeric.gridColumnEnd.numericalPosition -
128+
gridPropsNumeric.gridColumnStart.numericalPosition
129+
const rowCount =
130+
gridPropsNumeric.gridRowEnd.numericalPosition -
131+
gridPropsNumeric.gridRowStart.numericalPosition
132+
133+
const gridProps: GridElementProperties = {
134+
gridColumnStart: normalizePositionAfterResize(
135+
elementGridPropertiesFromProps.gridColumnStart,
136+
gridPropsNumeric.gridColumnStart,
137+
columnCount,
138+
'start',
139+
elementGridPropertiesFromProps.gridColumnEnd,
140+
gridPropsNumeric.gridColumnEnd,
141+
),
142+
gridColumnEnd: normalizePositionAfterResize(
143+
elementGridPropertiesFromProps.gridColumnEnd,
144+
gridPropsNumeric.gridColumnEnd,
145+
columnCount,
146+
'end',
147+
elementGridPropertiesFromProps.gridColumnStart,
148+
gridPropsNumeric.gridColumnStart,
149+
),
150+
gridRowStart: normalizePositionAfterResize(
151+
elementGridPropertiesFromProps.gridRowStart,
152+
gridPropsNumeric.gridRowStart,
153+
rowCount,
154+
'start',
155+
elementGridPropertiesFromProps.gridRowEnd,
156+
gridPropsNumeric.gridRowEnd,
157+
),
158+
gridRowEnd: normalizePositionAfterResize(
159+
elementGridPropertiesFromProps.gridRowEnd,
160+
gridPropsNumeric.gridRowEnd,
161+
rowCount,
162+
'end',
163+
elementGridPropertiesFromProps.gridRowStart,
164+
gridPropsNumeric.gridRowStart,
165+
),
166+
}
167+
116168
return strategyApplicationResult(
117169
getCommandsForGridItemPlacement(selectedElement, gridTemplate, gridProps),
118170
[EP.parentPath(selectedElement)],
@@ -158,3 +210,30 @@ function getNewGridPropsFromResizeBox(
158210
gridColumnEnd: { numericalPosition: newColumnEnd },
159211
}
160212
}
213+
214+
// After a resize happens and we know the numerical grid positioning of the new bounds,
215+
// return a normalized version of the new position so that it respects any spans that
216+
// may have been there before the resize, and/or default it to 'auto' when it would become redundant.
217+
function normalizePositionAfterResize(
218+
position: GridPositionOrSpan | null,
219+
resizedPosition: GridPositionValue,
220+
size: number, // the number of cols/rows the cell occupies
221+
bound: 'start' | 'end',
222+
counterpart: GridPositionOrSpan | null,
223+
counterpartResizedPosition: GridPositionValue,
224+
): GridPositionOrSpan | null {
225+
if (isGridSpan(position)) {
226+
if (size === 1) {
227+
return cssKeyword('auto')
228+
}
229+
return gridSpanNumeric(size)
230+
}
231+
if (
232+
isGridSpan(counterpart) &&
233+
counterpartResizedPosition.numericalPosition === 1 &&
234+
bound === 'end'
235+
) {
236+
return cssKeyword('auto')
237+
}
238+
return resizedPosition
239+
}

0 commit comments

Comments
 (0)