From e6f3148eac50658ce8d924c1c092286054514588 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:44:14 +0200 Subject: [PATCH] Conditional auto cols/rows input in the inspector, auto template in the advanced modal (#6541) This is a followup to https://github.com/concrete-utopia/utopia/pull/6533 This PR improves the previous iteration by: 1. showing the auto template input in the inspector only if there are no template dimensions, or the auto template is set explicitly to something that's not `auto` 2. showing the auto templates for both cols and rows in the advanced modal 3. support dimming `auto` values in grid expression inputs to `fg6` https://github.com/user-attachments/assets/e3ecb7f5-36ed-4a96-80d5-d3d4278e33b1 Fixes #6540 --- .../components/inspector/common/css-utils.ts | 32 ++++ .../controls/advanced-grid-modal.tsx | 40 +++++ .../inspector/flex-section.spec.browser2.tsx | 17 +- .../src/components/inspector/flex-section.tsx | 147 +++++------------- .../grid-auto-cols-or-rows-control.tsx | 115 ++++++++++++++ .../src/components/inspector/grid-helpers.ts | 51 ++++++ .../uuiui/inputs/grid-expression-input.tsx | 8 + 7 files changed, 294 insertions(+), 116 deletions(-) create mode 100644 editor/src/components/inspector/grid-auto-cols-or-rows-control.tsx create mode 100644 editor/src/components/inspector/grid-helpers.ts diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 2f5ebadc2f04..82b0ddcfd882 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -83,6 +83,7 @@ import { parseFlex, printFlexAsAttributeValue } from '../../../printer-parsers/c import { memoize } from '../../../core/shared/memoize' import * as csstree from 'css-tree' import type { IcnProps } from '../../../uuiui' +import { cssNumberEqual } from '../../canvas/controls/select-mode/controls-common' var combineRegExp = function (regexpList: Array, flags?: string) { let source: string = '' @@ -595,6 +596,37 @@ export type GridCSSKeyword = BaseGridDimension & { value: CSSKeyword } +export function gridDimensionsAreEqual(a: GridDimension, b: GridDimension): boolean { + switch (a.type) { + case 'KEYWORD': + if (a.type !== b.type) { + return false + } + return a.value.type === b.value.type && a.value.value === b.value.value + case 'NUMBER': + if (a.type !== b.type) { + return false + } + return cssNumberEqual(a.value, b.value) + case 'MINMAX': + if (a.type !== b.type) { + return false + } + return gridDimensionsAreEqual(a.min, b.min) && gridDimensionsAreEqual(a.max, b.max) + case 'REPEAT': + if (a.type !== b.type) { + return false + } + return ( + a.times === b.times && + a.value.length === b.value.length && + a.value.every((value, index) => gridDimensionsAreEqual(value, b.value[index])) + ) + default: + assertNever(a) + } +} + type BaseGridCSSRepeat = { type: 'REPEAT' value: Array diff --git a/editor/src/components/inspector/controls/advanced-grid-modal.tsx b/editor/src/components/inspector/controls/advanced-grid-modal.tsx index 99f947ba5718..ec8fb4cb4abb 100644 --- a/editor/src/components/inspector/controls/advanced-grid-modal.tsx +++ b/editor/src/components/inspector/controls/advanced-grid-modal.tsx @@ -16,6 +16,10 @@ import { import { optionalMap } from '../../../core/shared/optional-utils' import type { FlexAlignment } from 'utopia-api/core' import { FlexJustifyContent } from 'utopia-api/core' +import { GridAutoColsOrRowsControlInner } from '../grid-auto-cols-or-rows-control' +import { Substores, useEditorState, useRefEditorState } from '../../editor/store/store-hook' +import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { selectedViewsSelector } from '../inpector-selectors' export interface AdvancedGridModalProps { id: string @@ -103,6 +107,25 @@ export const AdvancedGridModal = React.memo((props: AdvancedGridModalProps) => { [alignContentLayoutInfo], ) + const selectedViewsRef = useRefEditorState(selectedViewsSelector) + const grid = useEditorState( + Substores.metadata, + (store) => { + if (selectedViewsRef.current.length !== 1) { + return null + } + return MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + selectedViewsRef.current[0], + ) + }, + 'AdvancedGridModal grid', + ) + + if (grid == null) { + return null + } + const advancedGridModal = ( { onOpenChange={toggleAlignContentDropdown} /> + + Template + + + + + + + ) diff --git a/editor/src/components/inspector/flex-section.spec.browser2.tsx b/editor/src/components/inspector/flex-section.spec.browser2.tsx index f191ed5c125a..fb151839b8b8 100644 --- a/editor/src/components/inspector/flex-section.spec.browser2.tsx +++ b/editor/src/components/inspector/flex-section.spec.browser2.tsx @@ -2,7 +2,7 @@ import { selectComponentsForTest } from '../../utils/utils.test-utils' import { renderTestEditorWithCode } from '../canvas/ui-jsx.test-utils' import * as EP from '../../core/shared/element-path' import { act, fireEvent, screen } from '@testing-library/react' -import { GridAutoColsOrRowsControlTestId } from './flex-section' +import { GridAutoColsOrRowsControlTestId } from './grid-auto-cols-or-rows-control' describe('flex section', () => { describe('grid dimensions', () => { @@ -78,7 +78,10 @@ describe('flex section', () => { }) describe('auto cols/rows', () => { it('can set a number', async () => { - const renderResult = await renderTestEditorWithCode(gridProject, 'await-first-dom-report') + const renderResult = await renderTestEditorWithCode( + gridProjectWithoutTemplate, + 'await-first-dom-report', + ) await selectComponentsForTest(renderResult, [EP.fromString('sb/grid')]) const control: HTMLInputElement = await screen.findByTestId( GridAutoColsOrRowsControlTestId('column'), @@ -89,7 +92,10 @@ describe('flex section', () => { expect(control.value).toBe('50px') }) it('can set a keyword', async () => { - const renderResult = await renderTestEditorWithCode(gridProject, 'await-first-dom-report') + const renderResult = await renderTestEditorWithCode( + gridProjectWithoutTemplate, + 'await-first-dom-report', + ) await selectComponentsForTest(renderResult, [EP.fromString('sb/grid')]) const control: HTMLInputElement = await screen.findByTestId( GridAutoColsOrRowsControlTestId('column'), @@ -100,7 +106,10 @@ describe('flex section', () => { expect(control.value).toBe('min-content') }) it('can set an expression', async () => { - const renderResult = await renderTestEditorWithCode(gridProject, 'await-first-dom-report') + const renderResult = await renderTestEditorWithCode( + gridProjectWithoutTemplate, + 'await-first-dom-report', + ) await selectComponentsForTest(renderResult, [EP.fromString('sb/grid')]) const control: HTMLInputElement = await screen.findByTestId( GridAutoColsOrRowsControlTestId('column'), diff --git a/editor/src/components/inspector/flex-section.tsx b/editor/src/components/inspector/flex-section.tsx index 6bf0d4680eff..5114495c30a3 100644 --- a/editor/src/components/inspector/flex-section.tsx +++ b/editor/src/components/inspector/flex-section.tsx @@ -48,8 +48,6 @@ import { gridCSSNumber, isCSSKeyword, isCSSNumber, - isEmptyInputValue, - isGridCSSNumber, printArrayGridDimensions, type GridDimension, } from './common/css-utils' @@ -91,6 +89,12 @@ import { } from '../canvas/controls/select-mode/select-mode-hooks' import type { Axis } from '../canvas/gap-utils' import { GridExpressionInput } from '../../uuiui/inputs/grid-expression-input' +import { + gridDimensionDropdownKeywords, + parseGridDimensionInput, + useGridExpressionInputFocused, +} from './grid-helpers' +import { GridAutoColsOrRowsControl } from './grid-auto-cols-or-rows-control' function getLayoutSystem( layoutSystem: DetectedLayoutSystem | null | undefined, @@ -233,6 +237,12 @@ const TemplateDimensionControl = React.memo( return fromProps }, [grid, axis, values]) + const autoTemplate = React.useMemo(() => { + return axis === 'column' + ? grid.specialSizeMeasurements.containerGridPropertiesFromProps.gridAutoColumns + : grid.specialSizeMeasurements.containerGridPropertiesFromProps.gridAutoRows + }, [grid, axis]) + const onUpdateDimension = React.useCallback( (index: number) => (newValue: GridDimension) => { if (template?.type !== 'DIMENSIONS') { @@ -464,6 +474,20 @@ const TemplateDimensionControl = React.memo( const dimensionsWithGeneratedIndexes = useGeneratedIndexesFromGridDimensions(values) + const showAutoColsOrRows = React.useMemo(() => { + return ( + template?.type !== 'DIMENSIONS' || + template.dimensions.length === 0 || + (autoTemplate?.type === 'DIMENSIONS' && + autoTemplate.dimensions.length > 0 && + !( + autoTemplate.dimensions.length === 1 && + autoTemplate.dimensions[0].type === 'KEYWORD' && + autoTemplate.dimensions[0].value.value === 'auto' + )) + ) + }, [template, autoTemplate]) + return (
))} - + {when( + showAutoColsOrRows, + , + )}
) }, ) TemplateDimensionControl.displayName = 'TemplateDimensionControl' -const gridDimensionDropdownKeywords = [ - { label: 'Auto', value: cssKeyword('auto') }, - { label: 'Min-Content', value: cssKeyword('min-content') }, - { label: 'Max-Content', value: cssKeyword('max-content') }, -] - function AxisDimensionControl({ value, index, @@ -597,6 +622,7 @@ function AxisDimensionControl({ onFocus={gridExpressionInputFocused.onFocus} onBlur={gridExpressionInputFocused.onBlur} keywords={gridDimensionDropdownKeywords} + defaultValue={gridCSSKeyword(cssKeyword('auto'), null)} /> {when( (isHovered && !gridExpressionInputFocused.focused) || isOpen, @@ -1090,106 +1116,3 @@ function useGeneratedIndexesFromGridDimensions( return result }, [dimensions]) } - -const useGridExpressionInputFocused = () => { - const [focused, setFocused] = React.useState(false) - const onFocus = React.useCallback(() => setFocused(true), []) - const onBlur = React.useCallback(() => setFocused(false), []) - return { focused, onFocus, onBlur } -} - -function parseGridDimensionInput( - value: UnknownOrEmptyInput>, - currentValue: GridDimension | null, -) { - if (isCSSNumber(value)) { - const maybeUnit = - currentValue != null && isGridCSSNumber(currentValue) ? currentValue.value.unit : null - return gridCSSNumber( - cssNumber(value.value, value.unit ?? maybeUnit), - currentValue?.areaName ?? null, - ) - } else if (isCSSKeyword(value)) { - return gridCSSKeyword(value, currentValue?.areaName ?? null) - } else if (isEmptyInputValue(value)) { - return gridCSSKeyword(cssKeyword('auto'), currentValue?.areaName ?? null) - } else { - return null - } -} - -const AutoColsOrRowsControl = React.memo( - (props: { axis: 'column' | 'row'; grid: ElementInstanceMetadata }) => { - const value = React.useMemo(() => { - const template = props.grid.specialSizeMeasurements.containerGridPropertiesFromProps - const data = props.axis === 'column' ? template.gridAutoColumns : template.gridAutoRows - if (data?.type !== 'DIMENSIONS') { - return null - } - return data.dimensions[0] - }, [props.grid, props.axis]) - - const dispatch = useDispatch() - - const onUpdateDimension = React.useCallback( - (newDimension: GridDimension) => { - dispatch([ - applyCommandsAction([ - setProperty( - 'always', - props.grid.elementPath, - PP.create('style', props.axis === 'column' ? 'gridAutoColumns' : 'gridAutoRows'), - printArrayGridDimensions([newDimension]), - ), - ]), - ]) - }, - [props.grid, props.axis, dispatch], - ) - - const onUpdateNumberOrKeyword = React.useCallback( - (newValue: UnknownOrEmptyInput>) => { - const parsed = parseGridDimensionInput(newValue, null) - if (parsed == null) { - return - } - onUpdateDimension(parsed) - }, - [onUpdateDimension], - ) - - const autoColsOrRowsValueFocused = useGridExpressionInputFocused() - - return ( -
-
Default
- -
- ) - }, -) -AutoColsOrRowsControl.displayName = 'AutoColsOrRowsControl' - -export function GridAutoColsOrRowsControlTestId(axis: 'column' | 'row'): string { - return `grid-template-auto-${axis}` -} diff --git a/editor/src/components/inspector/grid-auto-cols-or-rows-control.tsx b/editor/src/components/inspector/grid-auto-cols-or-rows-control.tsx new file mode 100644 index 000000000000..6a60037d8866 --- /dev/null +++ b/editor/src/components/inspector/grid-auto-cols-or-rows-control.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import { useDispatch } from '../editor/store/dispatch-context' +import { UtopiaTheme } from '../../uuiui' +import type { + CSSKeyword, + CSSNumber, + UnknownOrEmptyInput, + ValidGridDimensionKeyword, +} from './common/css-utils' +import { + cssKeyword, + gridCSSKeyword, + printArrayGridDimensions, + type GridDimension, +} from './common/css-utils' +import { applyCommandsAction } from '../editor/actions/action-creators' +import { setProperty } from '../canvas/commands/set-property-command' +import * as PP from '../../core/shared/property-path' +import { type ElementInstanceMetadata } from '../../core/shared/element-template' +import { GridExpressionInput } from '../../uuiui/inputs/grid-expression-input' +import { + gridDimensionDropdownKeywords, + parseGridDimensionInput, + useGridExpressionInputFocused, +} from './grid-helpers' + +export const GridAutoColsOrRowsControl = React.memo( + (props: { axis: 'column' | 'row'; grid: ElementInstanceMetadata; label: string }) => { + const autoColsOrRowsValueFocused = useGridExpressionInputFocused() + + return ( +
+ +
+ ) + }, +) +GridAutoColsOrRowsControl.displayName = 'GridAutoColsOrRowsControl' + +export const GridAutoColsOrRowsControlInner = React.memo( + (props: { axis: 'column' | 'row'; grid: ElementInstanceMetadata; label: string }) => { + const value = React.useMemo(() => { + const template = props.grid.specialSizeMeasurements.containerGridPropertiesFromProps + const data = props.axis === 'column' ? template.gridAutoColumns : template.gridAutoRows + if (data?.type !== 'DIMENSIONS') { + return null + } + return data.dimensions[0] + }, [props.grid, props.axis]) + + const dispatch = useDispatch() + + const onUpdateDimension = React.useCallback( + (newDimension: GridDimension) => { + dispatch([ + applyCommandsAction([ + setProperty( + 'always', + props.grid.elementPath, + PP.create('style', props.axis === 'column' ? 'gridAutoColumns' : 'gridAutoRows'), + printArrayGridDimensions([newDimension]), + ), + ]), + ]) + }, + [props.grid, props.axis, dispatch], + ) + + const onUpdateNumberOrKeyword = React.useCallback( + (newValue: UnknownOrEmptyInput>) => { + const parsed = parseGridDimensionInput(newValue, null) + if (parsed == null) { + return + } + onUpdateDimension(parsed) + }, + [onUpdateDimension], + ) + + const autoColsOrRowsValueFocused = useGridExpressionInputFocused() + + return ( + +
{props.label}
+ +
+ ) + }, +) +GridAutoColsOrRowsControlInner.displayName = 'GridAutoColsOrRowsControlInner' + +export function GridAutoColsOrRowsControlTestId(axis: 'column' | 'row'): string { + return `grid-template-auto-${axis}` +} diff --git a/editor/src/components/inspector/grid-helpers.ts b/editor/src/components/inspector/grid-helpers.ts new file mode 100644 index 000000000000..8e5f2b967156 --- /dev/null +++ b/editor/src/components/inspector/grid-helpers.ts @@ -0,0 +1,51 @@ +import React from 'react' +import type { + CSSKeyword, + CSSNumber, + UnknownOrEmptyInput, + ValidGridDimensionKeyword, +} from './common/css-utils' +import { + cssKeyword, + cssNumber, + gridCSSKeyword, + gridCSSNumber, + isCSSKeyword, + isCSSNumber, + isEmptyInputValue, + isGridCSSNumber, + type GridDimension, +} from './common/css-utils' + +export const useGridExpressionInputFocused = () => { + const [focused, setFocused] = React.useState(false) + const onFocus = React.useCallback(() => setFocused(true), []) + const onBlur = React.useCallback(() => setFocused(false), []) + return { focused, onFocus, onBlur } +} + +export function parseGridDimensionInput( + value: UnknownOrEmptyInput>, + currentValue: GridDimension | null, +) { + if (isCSSNumber(value)) { + const maybeUnit = + currentValue != null && isGridCSSNumber(currentValue) ? currentValue.value.unit : null + return gridCSSNumber( + cssNumber(value.value, value.unit ?? maybeUnit), + currentValue?.areaName ?? null, + ) + } else if (isCSSKeyword(value)) { + return gridCSSKeyword(value, currentValue?.areaName ?? null) + } else if (isEmptyInputValue(value)) { + return gridCSSKeyword(cssKeyword('auto'), currentValue?.areaName ?? null) + } else { + return null + } +} + +export const gridDimensionDropdownKeywords = [ + { label: 'Auto', value: cssKeyword('auto') }, + { label: 'Min-Content', value: cssKeyword('min-content') }, + { label: 'Max-Content', value: cssKeyword('max-content') }, +] diff --git a/editor/src/uuiui/inputs/grid-expression-input.tsx b/editor/src/uuiui/inputs/grid-expression-input.tsx index ddbfe783de5a..f4a82b1d7f4b 100644 --- a/editor/src/uuiui/inputs/grid-expression-input.tsx +++ b/editor/src/uuiui/inputs/grid-expression-input.tsx @@ -5,6 +5,7 @@ import type { CSSProperties } from 'react' import React from 'react' import { cssKeyword, + gridDimensionsAreEqual, isGridCSSNumber, isValidGridDimensionKeyword, parseCSSNumber, @@ -37,6 +38,7 @@ interface GridExpressionInputProps { onBlur: () => void keywords: Array<{ label: string; value: CSSKeyword }> style?: CSSProperties + defaultValue: GridDimension } const DropdownWidth = 25 @@ -51,6 +53,7 @@ export const GridExpressionInput = React.memo( onBlur, keywords, style = {}, + defaultValue, }: GridExpressionInputProps) => { const colorTheme = useColorTheme() @@ -157,6 +160,10 @@ export const GridExpressionInput = React.memo( onBlur() }, [onBlur]) + const isDefault = React.useMemo(() => { + return gridDimensionsAreEqual(value, defaultValue) + }, [value, defaultValue]) + return (
{unless( inputFocused,