From dc9dc4c864d34efe1471ff7cf414f3b3cf7a94a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertalan=20K=C3=B6rmendy?= Date: Thu, 18 Jul 2024 11:29:31 +0200 Subject: [PATCH] grid-specific hug strategy (#6087) ## Problem The basic hug contents strategy doesn't really make sense for grids. ## Fix Create a hug contents strategy tailored to grids. When setting a grid to hug along an axis, if the grid doesn't use any `fr` units in `gridTemplate{Row | Column}` along that axis, the width/height prop is removed from that axis, so the grid is sized by its template rows or columns. If there's an `fr` unit in`gridTemplate{Row | Column}`, the strategy is not applicable because the `fr` only works if the grid is sized --- ...ze-bounding-box-strategy.spec.browser2.tsx | 90 +++++++++++++++++++ .../convert-to-flex-strategy.ts | 2 +- .../components/inspector/inspector-common.ts | 13 ++- ...c-strategy.ts => hug-contents-strategy.ts} | 71 ++++++++++++++- .../inspector-strategies.ts | 8 +- 5 files changed, 175 insertions(+), 9 deletions(-) rename editor/src/components/inspector/inspector-strategies/{hug-contents-basic-strategy.ts => hug-contents-strategy.ts} (66%) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.spec.browser2.tsx index 533fb5f8c7ff..9fead6612cd2 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.spec.browser2.tsx @@ -3124,6 +3124,96 @@ describe('Double click on resize edge', () => { expect(div.style.width).toEqual(MaxContent) expect(div.style.height).toEqual('445px') }) + + describe('grids', () => { + const project = ({ + rows: rows, + columns: columns, + }: { + rows: string + columns: string + }) => `import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = ( + + +
+ Utopia logo +
+
+
+) +` + it('removes width when right edge is clicked', async () => { + const editor = await renderTestEditorWithCode( + project({ rows: '66px 66px 66px 66px', columns: '50px 81px 96px 85px' }), + 'await-first-dom-report', + ) + const div = await doDblClickTest(editor, edgeResizeControlTestId(EdgePositionRight)) + expect(div.style.width).toEqual('') // width is removed + expect(div.style.height).toEqual('400px') + }) + it('removes height when bottom edge is clicked', async () => { + const editor = await renderTestEditorWithCode( + project({ rows: '66px 66px 66px 66px', columns: '50px 81px 96px 85px' }), + 'await-first-dom-report', + ) + const div = await doDblClickTest(editor, edgeResizeControlTestId(EdgePositionBottom)) + expect(div.style.width).toEqual('400px') + expect(div.style.height).toEqual('') // height is removed + }) + it("isn't applicable when the selected grid uses fr along the affected axis", async () => { + const editor = await renderTestEditorWithCode( + project({ rows: '66px 1fr 66px 66px', columns: '50px 81px 96px 85px' }), + 'await-first-dom-report', + ) + const div = await doDblClickTest(editor, edgeResizeControlTestId(EdgePositionBottom)) + expect(div.style.width).toEqual('400px') + expect(div.style.height).toEqual('400px') + }) + }) }) describe('double click on resize corner', () => { diff --git a/editor/src/components/common/shared-strategies/convert-to-flex-strategy.ts b/editor/src/components/common/shared-strategies/convert-to-flex-strategy.ts index aed823bad9bc..af2fc721ea09 100644 --- a/editor/src/components/common/shared-strategies/convert-to-flex-strategy.ts +++ b/editor/src/components/common/shared-strategies/convert-to-flex-strategy.ts @@ -50,7 +50,7 @@ import { onlyChildIsSpan, sizeToVisualDimensions, } from '../../inspector/inspector-common' -import { setHugContentForAxis } from '../../inspector/inspector-strategies/hug-contents-basic-strategy' +import { setHugContentForAxis } from '../../inspector/inspector-strategies/hug-contents-strategy' type FlexDirectionRowColumn = 'row' | 'column' // a limited subset as we won't never guess row-reverse or column-reverse type FlexAlignItems = 'center' | 'flex-end' diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index 3c5fbdacfb8a..c9163ded2cef 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -75,7 +75,7 @@ import { import { fixedSizeDimensionHandlingText } from '../text-editor/text-handling' import { convertToAbsolute } from '../canvas/commands/convert-to-absolute-command' import { hugPropertiesFromStyleMap } from '../../core/shared/dom-utils' -import { setHugContentForAxis } from './inspector-strategies/hug-contents-basic-strategy' +import { setHugContentForAxis } from './inspector-strategies/hug-contents-strategy' export type StartCenterEnd = 'flex-start' | 'center' | 'flex-end' @@ -255,12 +255,12 @@ export function detectAreElementsFlexContainers( export const isFlexColumn = (flexDirection: FlexDirection): boolean => flexDirection.startsWith('column') -export const hugContentsApplicableForContainer = ( +export const basicHugContentsApplicableForContainer = ( metadata: ElementInstanceMetadataMap, pathTrees: ElementPathTrees, elementPath: ElementPath, ): boolean => { - return ( + const isNonFixStickOrAbsolute = mapDropNulls( (path) => MetadataUtils.findElementByElementPath(metadata, path), MetadataUtils.getChildrenPathsOrdered(metadata, pathTrees, elementPath), @@ -272,7 +272,12 @@ export const hugContentsApplicableForContainer = ( MetadataUtils.isPositionAbsolute(element) ), ).length > 0 + + const isGrid = MetadataUtils.isGridLayoutedContainer( + MetadataUtils.findElementByElementPath(metadata, elementPath), ) + + return isNonFixStickOrAbsolute && !isGrid } export const hugContentsApplicableForText = ( @@ -999,7 +1004,7 @@ export function getFixedFillHugOptionsForElement( isGroup ? 'hug-group' : null, 'fixed', hugContentsApplicableForText(metadata, selectedView) || - (!isGroup && hugContentsApplicableForContainer(metadata, pathTrees, selectedView)) + (!isGroup && basicHugContentsApplicableForContainer(metadata, pathTrees, selectedView)) ? 'hug' : null, fillContainerApplicable(metadata, selectedView) ? 'fill' : null, diff --git a/editor/src/components/inspector/inspector-strategies/hug-contents-basic-strategy.ts b/editor/src/components/inspector/inspector-strategies/hug-contents-strategy.ts similarity index 66% rename from editor/src/components/inspector/inspector-strategies/hug-contents-basic-strategy.ts rename to editor/src/components/inspector/inspector-strategies/hug-contents-strategy.ts index 70ca1829f219..8e6395c3cf24 100644 --- a/editor/src/components/inspector/inspector-strategies/hug-contents-basic-strategy.ts +++ b/editor/src/components/inspector/inspector-strategies/hug-contents-strategy.ts @@ -1,5 +1,6 @@ import type { ElementPathTrees } from '../../../core/shared/element-path-tree' import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import type { GridTemplate } from '../../../core/shared/element-template' import { type ElementInstanceMetadataMap } from '../../../core/shared/element-template' import type { ElementPath } from '../../../core/shared/project-file-types' import * as PP from '../../../core/shared/property-path' @@ -15,7 +16,7 @@ import { cssKeyword } from '../common/css-utils' import type { Axis } from '../inspector-common' import { detectFillHugFixedState, - hugContentsApplicableForContainer, + basicHugContentsApplicableForContainer, hugContentsApplicableForText, MaxContent, nukeSizingPropsForAxisCommand, @@ -28,6 +29,8 @@ import { queueTrueUpElement } from '../../canvas/commands/queue-true-up-command' import { trueUpGroupElementChanged } from '../../../components/editor/store/editor-state' import type { AllElementProps } from '../../../components/editor/store/editor-state' import { convertSizelessDivToFrameCommands } from '../../canvas/canvas-strategies/strategies/group-conversion-helpers' +import { deleteProperties } from '../../canvas/commands/delete-properties-command' +import { assertNever } from '../../../core/shared/utils' const CHILDREN_CONVERTED_TOAST_ID = 'CHILDREN_CONVERTED_TOAST_ID' @@ -87,6 +90,70 @@ function hugContentsSingleElement( ] } +function gridTemplateUsesFr(template: GridTemplate | null) { + return ( + template != null && + template.type === 'DIMENSIONS' && + template.dimensions.some((d) => d.unit === 'fr') + ) +} + +function elementUsesFrAlongAxis( + axis: Axis, + metadata: ElementInstanceMetadataMap, + elementPath: ElementPath, +) { + const instance = MetadataUtils.findElementByElementPath(metadata, elementPath) + if (instance == null) { + return false + } + const { containerGridProperties, containerGridPropertiesFromProps } = + instance.specialSizeMeasurements + + switch (axis) { + case 'horizontal': + return ( + gridTemplateUsesFr(containerGridProperties.gridTemplateColumns) || + gridTemplateUsesFr(containerGridPropertiesFromProps.gridTemplateColumns) + ) + case 'vertical': + return ( + gridTemplateUsesFr(containerGridProperties.gridTemplateRows) || + gridTemplateUsesFr(containerGridPropertiesFromProps.gridTemplateRows) + ) + default: + assertNever(axis) + } +} + +export const hugContentsGridStrategy = ( + metadata: ElementInstanceMetadataMap, + elementPaths: ElementPath[], + axis: Axis, +): InspectorStrategy => ({ + name: 'Set Grid to Hug', + strategy: () => { + // only run the strategy if all selected elements are grids + const allSelectedElementsGrids = elementPaths.every((e) => + MetadataUtils.isGridLayoutedContainer(MetadataUtils.findElementByElementPath(metadata, e)), + ) + + // only run the strategy if no selected element uses fr along the affected + // axis, because that implies that the container needs to be sized + const anyElementUsesFrAlongAxis = elementPaths.some((e) => + elementUsesFrAlongAxis(axis, metadata, e), + ) + + if (!allSelectedElementsGrids || anyElementUsesFrAlongAxis) { + return null + } + + return elementPaths.flatMap((elementPath) => + deleteProperties('always', elementPath, [PP.create('style', widthHeightFromAxis(axis))]), + ) + }, +}) + export const hugContentsBasicStrategy = ( metadata: ElementInstanceMetadataMap, elementPaths: ElementPath[], @@ -97,7 +164,7 @@ export const hugContentsBasicStrategy = ( strategy: () => { const elements = elementPaths.filter( (path) => - hugContentsApplicableForContainer(metadata, pathTrees, path) || + basicHugContentsApplicableForContainer(metadata, pathTrees, path) || hugContentsApplicableForText(metadata, path), ) diff --git a/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts b/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts index 82550696f489..471bba6fa6b3 100644 --- a/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts +++ b/editor/src/components/inspector/inspector-strategies/inspector-strategies.ts @@ -16,7 +16,8 @@ import type { WhenToRun } from '../../../components/canvas/commands/commands' import { hugContentsAbsoluteStrategy, hugContentsBasicStrategy, -} from './hug-contents-basic-strategy' + hugContentsGridStrategy, +} from './hug-contents-strategy' import { fillContainerStrategyFlexParent, fillContainerStrategyFlow, @@ -204,7 +205,10 @@ export const setPropHugStrategies = ( elementPaths: ElementPath[], pathTrees: ElementPathTrees, axis: Axis, -): Array => [hugContentsBasicStrategy(metadata, elementPaths, pathTrees, axis)] +): Array => [ + hugContentsGridStrategy(metadata, elementPaths, axis), + hugContentsBasicStrategy(metadata, elementPaths, pathTrees, axis), +] export const setPropHugAbsoluteStrategies = ( metadata: ElementInstanceMetadataMap,