From d637f429a1131a02725bafb776948a908f6db945 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:46:54 +0200 Subject: [PATCH] Fix/inspector mixed values (#4208) --- .../inspector/common/control-styles.ts | 10 +++- ...spector-end-to-end-tests.spec.browser2.tsx | 13 ++++-- .../inspector/fill-hug-fixed-control.tsx | 5 +- .../components/inspector/inspector-common.ts | 46 +++++++++---------- editor/src/core/shared/utils.ts | 2 +- editor/src/uuiui/inputs/base-input.tsx | 13 ++++++ editor/src/uuiui/inputs/number-input.tsx | 29 +++++++----- editor/src/uuiui/inputs/string-input.tsx | 11 ++--- 8 files changed, 79 insertions(+), 50 deletions(-) diff --git a/editor/src/components/inspector/common/control-styles.ts b/editor/src/components/inspector/common/control-styles.ts index 38a14b9866d1..818287bfbb4d 100644 --- a/editor/src/components/inspector/common/control-styles.ts +++ b/editor/src/components/inspector/common/control-styles.ts @@ -93,9 +93,17 @@ const controlStylesByStatus: { [key: string]: ControlStyles } = mapArrayToDictio trackColor = 'var(--control-styles-interactive-unset-track-color)' railColor = 'var(--control-styles-interactive-unset-rail-color)' break + case 'multiselect-mixed-simple-or-unset': + mixed = true + interactive = true + showContent = true + mainColor = colorTheme.fg6Opacity50.value + secondaryColor = colorTheme.fg6Opacity50.value + trackColor = colorTheme.fg6Opacity50.value + strokePrimaryColor = colorTheme.fg6Opacity50.value + break case 'controlled': case 'multiselect-controlled': - case 'multiselect-mixed-simple-or-unset': interactive = true mainColor = colorTheme.dynamicBlue.value secondaryColor = colorTheme.dynamicBlue.value diff --git a/editor/src/components/inspector/common/inspector-end-to-end-tests.spec.browser2.tsx b/editor/src/components/inspector/common/inspector-end-to-end-tests.spec.browser2.tsx index da32523d4b27..beef7fccea4e 100644 --- a/editor/src/components/inspector/common/inspector-end-to-end-tests.spec.browser2.tsx +++ b/editor/src/components/inspector/common/inspector-end-to-end-tests.spec.browser2.tsx @@ -61,6 +61,7 @@ import { MetadataUtils } from '../../../core/model/element-metadata-utils' import type { InvalidGroupState } from '../../canvas/canvas-strategies/strategies/group-helpers' import { invalidGroupStateToString } from '../../canvas/canvas-strategies/strategies/group-helpers' import selectEvent from 'react-select-event' +import { MixedPlaceholder } from '../../../uuiui/inputs/base-input' async function getControl( controlTestId: string, @@ -371,28 +372,32 @@ describe('inspector tests with real metadata', () => { )) as HTMLInputElement matchInlineSnapshotBrowser(metadata.computedStyle?.['width'], `"266px"`) - matchInlineSnapshotBrowser(widthControl.value, `"266"`) + matchInlineSnapshotBrowser(widthControl.value, '""') + matchInlineSnapshotBrowser(widthControl.placeholder, `"${MixedPlaceholder}"`) matchInlineSnapshotBrowser( widthControl.attributes.getNamedItemNS(null, 'data-controlstatus')?.value, `"multiselect-mixed-simple-or-unset"`, ) matchInlineSnapshotBrowser(metadata.computedStyle?.['height'], `"124px"`) - matchInlineSnapshotBrowser(heightControl.value, `"124"`) + matchInlineSnapshotBrowser(heightControl.value, `""`) + matchInlineSnapshotBrowser(heightControl.placeholder, `"${MixedPlaceholder}"`) matchInlineSnapshotBrowser( heightControl.attributes.getNamedItemNS(null, 'data-controlstatus')?.value, `"multiselect-mixed-simple-or-unset"`, ) matchInlineSnapshotBrowser(metadata.computedStyle?.['top'], `"98px"`) - matchInlineSnapshotBrowser(topControl.value, `"98"`) + matchInlineSnapshotBrowser(topControl.value, `""`) + matchInlineSnapshotBrowser(topControl.placeholder, `"${MixedPlaceholder}"`) matchInlineSnapshotBrowser( topControl.attributes.getNamedItemNS(null, 'data-controlstatus')?.value, `"multiselect-mixed-simple-or-unset"`, ) matchInlineSnapshotBrowser(metadata.computedStyle?.['left'], `"55px"`) - matchInlineSnapshotBrowser(leftControl.value, `"55"`) + matchInlineSnapshotBrowser(leftControl.value, `""`) + matchInlineSnapshotBrowser(leftControl.placeholder, `"${MixedPlaceholder}"`) matchInlineSnapshotBrowser( leftControl.attributes.getNamedItemNS(null, 'data-controlstatus')?.value, `"multiselect-mixed-simple-or-unset"`, diff --git a/editor/src/components/inspector/fill-hug-fixed-control.tsx b/editor/src/components/inspector/fill-hug-fixed-control.tsx index 1ab295f25ded..ccb1e73bc0f2 100644 --- a/editor/src/components/inspector/fill-hug-fixed-control.tsx +++ b/editor/src/components/inspector/fill-hug-fixed-control.tsx @@ -55,6 +55,7 @@ import { unsetProperty, setProp_UNSAFE } from '../editor/actions/action-creators import { FlexCol } from 'utopia-api' import { PinControl } from './controls/pin-control' import type { FramePinsInfo } from './common/layout-property-path-hooks' +import { MixedPlaceholder } from '../../uuiui/inputs/base-input' export const FillFixedHugControlId = (segment: 'width' | 'height'): string => `hug-fixed-fill-${segment}` @@ -610,7 +611,7 @@ const GroupConstraintSelect = React.memo( case 'constrained': return colorTheme.brandNeonPink.value case 'mixed': - return colorTheme.primary.value + return colorTheme.fg6Opacity50.value case 'not-constrained': return colorTheme.subduedForeground.value default: @@ -752,7 +753,7 @@ function checkGroupChildConstraint( const groupChildConstraintOptionValues = { constrained: groupChildConstraintOption('constrained'), notConstrained: groupChildConstraintOption('not-constrained'), - mixed: { value: 'mixed', label: 'Mixed' }, + mixed: { value: 'mixed', label: MixedPlaceholder }, } const groupChildConstraintOptions: Array = [ diff --git a/editor/src/components/inspector/inspector-common.ts b/editor/src/components/inspector/inspector-common.ts index 257e474312ac..477a1b2d53b6 100644 --- a/editor/src/components/inspector/inspector-common.ts +++ b/editor/src/components/inspector/inspector-common.ts @@ -27,10 +27,10 @@ import { parseCSSLengthPercent, parseCSSNumber, } from './common/css-utils' -import { assertNever, fastForEach } from '../../core/shared/utils' +import { assertNever } from '../../core/shared/utils' import { defaultEither, foldEither, isLeft, isRight, right } from '../../core/shared/either' import { elementOnlyHasTextChildren } from '../../core/model/element-template-utils' -import { forceNotNull, optionalMap } from '../../core/shared/optional-utils' +import { optionalMap } from '../../core/shared/optional-utils' import type { CSSProperties } from 'react' import type { CanvasCommand } from '../canvas/commands/commands' import { deleteProperties } from '../canvas/commands/delete-properties-command' @@ -45,17 +45,7 @@ import { setPropHugStrategies, } from './inspector-strategies/inspector-strategies' import { commandsForFirstApplicableStrategy } from './inspector-strategies/inspector-strategy' -import { - CanvasRectangle, - roundUpToNearestHalf, - SimpleRectangle, -} from '../../core/shared/math-utils' -import { - canvasRectangle, - isFiniteRectangle, - isInfinityRectangle, - zeroRectIfNullOrInfinity, -} from '../../core/shared/math-utils' +import { isFiniteRectangle, isInfinityRectangle } from '../../core/shared/math-utils' import { inlineHtmlElements } from '../../utils/html-elements' import { intersection } from '../../core/shared/set-utils' import { showToastCommand } from '../canvas/commands/show-toast-command' @@ -733,24 +723,34 @@ export function detectFillHugFixedStateMultiselect( if (elementPaths.length === 1) { return detectFillHugFixedState(axis, metadata, elementPaths[0]) } else { + function fixedHugFillWithControlStatus( + fixedHugFill: FixedHugFill | null, + controlStatus: ControlStatus, + ) { + return { + fixedHugFill: fixedHugFill, + controlStatus: controlStatus, + } + } + const results = elementPaths.map((path) => detectFillHugFixedState(axis, metadata, path)) - let controlStatus: ControlStatus = results[0]?.controlStatus ?? 'off' - let value: FixedHugFill | null = results[0]?.fixedHugFill + const value: FixedHugFill | null = results[0]?.fixedHugFill - fastForEach(results, (result) => { - if (!isFixedHugFillEqual(result, results[0])) { - controlStatus = 'multiselect-mixed-simple-or-unset' - } + const isMixed = results.some((result) => { + return !isFixedHugFillEqual(result, results[0]) }) + if (isMixed) { + return fixedHugFillWithControlStatus(value, 'multiselect-mixed-simple-or-unset') + } const allControlStatus = uniq(results.map((result) => result.controlStatus)) if (allControlStatus.includes('unoverwritable')) { - controlStatus = 'multiselect-unoverwritable' + return fixedHugFillWithControlStatus(value, 'multiselect-unoverwritable') } else if (allControlStatus.includes('controlled')) { - controlStatus = 'multiselect-controlled' + return fixedHugFillWithControlStatus(value, 'multiselect-controlled') + } else { + return fixedHugFillWithControlStatus(value, results[0]?.controlStatus ?? 'off') } - - return { fixedHugFill: value, controlStatus: controlStatus } } } diff --git a/editor/src/core/shared/utils.ts b/editor/src/core/shared/utils.ts index e56a65cdcdc1..4cc417fdf85c 100644 --- a/editor/src/core/shared/utils.ts +++ b/editor/src/core/shared/utils.ts @@ -23,7 +23,7 @@ export function identity(t: T): T { return t } -export function fastForEach(a: readonly T[], fn: (t: T, index: number) => void) { +export function fastForEach(a: readonly T[], fn: (t: T, index: number) => void): void { for (var i = 0, len = a.length; i < len; i++) { if (i in a) { fn(a[i]!, i) diff --git a/editor/src/uuiui/inputs/base-input.tsx b/editor/src/uuiui/inputs/base-input.tsx index 95d7f38ce30c..f970f445e0e7 100644 --- a/editor/src/uuiui/inputs/base-input.tsx +++ b/editor/src/uuiui/inputs/base-input.tsx @@ -167,3 +167,16 @@ export const InspectorInput = React.memo( ) }), ) + +export const MixedPlaceholder = 'Mixed' +export const UnknownPlaceholer = 'Unknown' + +export function getInputPlaceholder(controlStyles: ControlStyles): string { + if (controlStyles.unknown) { + return UnknownPlaceholer + } else if (controlStyles.mixed) { + return MixedPlaceholder + } else { + return '' + } +} diff --git a/editor/src/uuiui/inputs/number-input.tsx b/editor/src/uuiui/inputs/number-input.tsx index fbb58afd9d32..990dddac6204 100644 --- a/editor/src/uuiui/inputs/number-input.tsx +++ b/editor/src/uuiui/inputs/number-input.tsx @@ -43,7 +43,7 @@ import { Icn } from '../icn' import { useColorTheme, UtopiaTheme } from '../styles/theme' import { FlexRow } from '../widgets/layout/flex-row' import type { BaseInputProps, BoxCorners, ChainedType } from './base-input' -import { getBorderRadiusStyles, InspectorInput } from './base-input' +import { getBorderRadiusStyles, getInputPlaceholder, InspectorInput } from './base-input' import { usePropControlledStateV2 } from '../../components/inspector/common/inspector-utils' export type LabelDragDirection = 'horizontal' | 'vertical' @@ -179,17 +179,30 @@ export const NumberInput = React.memo( onMouseLeave, }) => { const ref = React.useRef(null) - const controlStyles = getControlStyles(controlStatus) const colorTheme = useColorTheme() - const { showContent } = controlStyles + const controlStyles = React.useMemo(() => { + return getControlStyles(controlStatus) + }, [controlStatus]) - const [mixed, setMixed] = React.useState(controlStyles.mixed) + const { mixed, showContent } = React.useMemo( + () => ({ + mixed: controlStyles.mixed, + showContent: controlStyles.showContent, + }), + [controlStyles], + ) const [value, setValue] = usePropControlledStateV2(propsValue ?? null) const [displayValue, setDisplayValue] = usePropControlledStateV2( getDisplayValue(value, defaultUnitToHide, mixed, showContent), ) + React.useEffect(() => { + if (mixed) { + setDisplayValue('') + } + }, [mixed, setDisplayValue]) + const valueUnit = React.useMemo(() => value?.unit ?? null, [value]) const [isActuallyFocused, setIsActuallyFocused] = React.useState(false) @@ -496,7 +509,6 @@ export const NumberInput = React.memo( inputProps.onChange(e) } setValueChangedSinceFocus(true) - setMixed(false) setDisplayValue(e.target.value) }, [inputProps, setDisplayValue], @@ -606,12 +618,7 @@ export const NumberInput = React.memo( [scrubOnMouseMove, scrubOnMouseUp, setGlobalCursor, value], ) - let placeholder: string = '' - if (controlStyles.unknown) { - placeholder = 'unknown' - } else if (controlStyles.mixed) { - placeholder = 'mixed' - } + const placeholder = getInputPlaceholder(controlStyles) const chainedStyles: Interpolation | undefined = (chained === 'first' || chained === 'middle') && !isFocused diff --git a/editor/src/uuiui/inputs/string-input.tsx b/editor/src/uuiui/inputs/string-input.tsx index 304906dd015d..0a7de8aec585 100644 --- a/editor/src/uuiui/inputs/string-input.tsx +++ b/editor/src/uuiui/inputs/string-input.tsx @@ -8,8 +8,8 @@ import type { ControlStatus } from '../../components/inspector/common/control-st import type { ControlStyles } from '../../components/inspector/common/control-styles' import { getControlStyles } from '../../components/inspector/common/control-styles' import { preventDefault, stopPropagation } from '../../components/inspector/common/inspector-utils' -import { useColorTheme, UtopiaTheme } from '../styles/theme' -import { InspectorInput, InspectorInputEmotionStyle } from './base-input' +import { useColorTheme } from '../styles/theme' +import { InspectorInputEmotionStyle, getInputPlaceholder } from './base-input' interface StringInputOptions { focusOnMount?: boolean @@ -73,12 +73,7 @@ export const StringInput = React.memo( [inputPropsKeyDown], ) - let placeholder = initialPlaceHolder - if (controlStyles.unknown) { - placeholder = 'unknown' - } else if (controlStyles.mixed) { - placeholder = 'mixed' - } + const placeholder = getInputPlaceholder(controlStyles) return (