Skip to content

Commit a668e00

Browse files
authored
more granular unparsable prop detection for StyleInfo (#6631)
## Problem The `StyleInfo` type doesn't express the state when a property is set, but it cannot parsed in the desired format. For example, in `style={{ top: '300px', left: theme.left + 50 }}`, `top` can be parsed as a css number, but left cannot be. We need to be able to express this, since some code (`AdjustCssLengthProperties` and `SetCSSLength` for example) need to handle it. Once `AdjustCssLengthProperties` and `SetCSSLength` use this infra, we can make more strategies Tailwind-compatible. ## Fix Extend the `StyleInfo` type, and fix any affected code. **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
1 parent 07b81de commit a668e00

File tree

5 files changed

+198
-39
lines changed

5 files changed

+198
-39
lines changed

editor/src/components/canvas/canvas-types.ts

+36-8
Original file line numberDiff line numberDiff line change
@@ -537,19 +537,47 @@ export type SelectionLocked = 'locked' | 'locked-hierarchy' | 'selectable'
537537

538538
export type PropertyTag = { type: 'hover' } | { type: 'breakpoint'; name: string }
539539

540-
export interface WithPropertyTag<T> {
541-
tag: PropertyTag | null
540+
interface CSSStylePropertyNotFound {
541+
type: 'not-found'
542+
}
543+
544+
interface CSSStylePropertyNotParsable {
545+
type: 'not-parsable'
546+
}
547+
548+
interface ParsedCSSStyleProperty<T> {
549+
type: 'property'
550+
tags: PropertyTag[]
542551
value: T
543552
}
544553

545-
export const withPropertyTag = <T>(value: T): WithPropertyTag<T> => ({
546-
tag: null,
547-
value: value,
548-
})
554+
export type CSSStyleProperty<T> =
555+
| CSSStylePropertyNotFound
556+
| CSSStylePropertyNotParsable
557+
| ParsedCSSStyleProperty<T>
558+
559+
export function cssStylePropertyNotFound(): CSSStylePropertyNotFound {
560+
return { type: 'not-found' }
561+
}
562+
563+
export function cssStylePropertyNotParsable(): CSSStylePropertyNotParsable {
564+
return { type: 'not-parsable' }
565+
}
566+
567+
export function cssStyleProperty<T>(value: T): ParsedCSSStyleProperty<T> {
568+
return { type: 'property', tags: [], value: value }
569+
}
570+
571+
export function maybePropertyValue<T>(property: CSSStyleProperty<T>): T | null {
572+
if (property.type === 'property') {
573+
return property.value
574+
}
575+
return null
576+
}
549577

550-
export type FlexGapInfo = WithPropertyTag<CSSNumber>
578+
export type FlexGapInfo = CSSStyleProperty<CSSNumber>
551579

552-
export type FlexDirectionInfo = WithPropertyTag<FlexDirection>
580+
export type FlexDirectionInfo = CSSStyleProperty<FlexDirection>
553581

554582
export interface StyleInfo {
555583
gap: FlexGapInfo | null

editor/src/components/canvas/gap-utils.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { canvasRectangle, isInfinityRectangle } from '../../core/shared/math-uti
1717
import type { ElementPath } from '../../core/shared/project-file-types'
1818
import { assertNever } from '../../core/shared/utils'
1919
import type { StyleInfo } from './canvas-types'
20-
import { CSSCursor } from './canvas-types'
20+
import { CSSCursor, maybePropertyValue } from './canvas-types'
2121
import type { CSSNumberWithRenderedValue } from './controls/select-mode/controls-common'
2222
import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils'
2323
import type { Sides } from 'utopia-api/core'
@@ -34,6 +34,7 @@ import { treatElementAsFragmentLike } from './canvas-strategies/strategies/fragm
3434
import type { AllElementProps } from '../editor/store/editor-state'
3535
import type { GridData } from './controls/grid-controls-for-strategies'
3636
import { getNullableAutoOrTemplateBaseString } from './controls/grid-controls-for-strategies'
37+
import { optionalMap } from '../../core/shared/optional-utils'
3738

3839
export interface PathWithBounds {
3940
bounds: CanvasRectangle
@@ -238,14 +239,14 @@ export function maybeFlexGapData(
238239
}
239240

240241
const gap = element.specialSizeMeasurements.gap ?? 0
241-
const gapFromReader = info.gap?.value
242+
const gapFromReader = optionalMap(maybePropertyValue, info.gap)
242243

243244
const flexDirection = element.specialSizeMeasurements.flexDirection ?? 'row'
244245

245246
return {
246247
value: {
247248
renderedValuePx: gap,
248-
value: gapFromReader ?? null,
249+
value: gapFromReader,
249250
},
250251
direction: flexDirection,
251252
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as EP from '../../../core/shared/element-path'
2+
import { cssNumber } from '../../inspector/common/css-utils'
3+
import {
4+
cssStyleProperty,
5+
cssStylePropertyNotFound,
6+
cssStylePropertyNotParsable,
7+
} from '../canvas-types'
8+
import type { EditorRenderResult } from '../ui-jsx.test-utils'
9+
import { renderTestEditorWithCode } from '../ui-jsx.test-utils'
10+
import { InlineStylePlugin } from './inline-style-plugin'
11+
12+
describe('inline style plugin', () => {
13+
it('can parse style info from element', async () => {
14+
const editor = await renderTestEditorWithCode(
15+
`
16+
import React from 'react'
17+
import { Scene, Storyboard } from 'utopia-api'
18+
export var storyboard = (
19+
<Storyboard data-uid='sb'>
20+
<Scene
21+
id='scene'
22+
commentId='scene'
23+
data-uid='scene'
24+
style={{
25+
width: 700,
26+
height: 759,
27+
position: 'absolute',
28+
left: 212,
29+
top: 128,
30+
}}
31+
>
32+
<div
33+
data-uid='div'
34+
style={{ display: 'flex', flexDirection: 'column', gap: '2rem'}}
35+
/>
36+
</Scene>
37+
</Storyboard>
38+
)
39+
40+
`,
41+
'await-first-dom-report',
42+
)
43+
44+
const styleInfo = getStyleInfoFromInlineStyle(editor)
45+
46+
expect(styleInfo).not.toBeNull()
47+
const { flexDirection, gap } = styleInfo!
48+
expect(flexDirection).toEqual(cssStyleProperty('column'))
49+
expect(gap).toEqual(cssStyleProperty(cssNumber(2, 'rem')))
50+
})
51+
52+
it('can parse style info with missing/unparsable props', async () => {
53+
const editor = await renderTestEditorWithCode(
54+
`
55+
import React from 'react'
56+
import { Scene, Storyboard } from 'utopia-api'
57+
58+
const gap = { small: '1rem' }
59+
60+
export var storyboard = (
61+
<Storyboard data-uid='sb'>
62+
<Scene
63+
id='scene'
64+
commentId='scene'
65+
data-uid='scene'
66+
style={{
67+
width: 700,
68+
height: 759,
69+
position: 'absolute',
70+
left: 212,
71+
top: 128,
72+
}}
73+
>
74+
<div
75+
data-uid='div'
76+
style={{ display: 'flex', gap: gap.small }}
77+
/>
78+
</Scene>
79+
</Storyboard>
80+
)
81+
82+
`,
83+
'await-first-dom-report',
84+
)
85+
86+
const styleInfo = getStyleInfoFromInlineStyle(editor)
87+
88+
expect(styleInfo).not.toBeNull()
89+
const { flexDirection, gap } = styleInfo!
90+
expect(flexDirection).toEqual(cssStylePropertyNotFound())
91+
expect(gap).toEqual(cssStylePropertyNotParsable())
92+
})
93+
})
94+
95+
function getStyleInfoFromInlineStyle(editor: EditorRenderResult) {
96+
const { jsxMetadata, projectContents, elementPathTree } = editor.getEditorState().editor
97+
98+
const styleInfoReader = InlineStylePlugin.styleInfoFactory({
99+
metadata: jsxMetadata,
100+
projectContents: projectContents,
101+
elementPathTree: elementPathTree,
102+
})
103+
const styleInfo = styleInfoReader(EP.fromString('sb/scene/div'))
104+
return styleInfo
105+
}

editor/src/components/canvas/plugins/inline-style-plugin.ts

+46-23
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,66 @@
1-
import { getLayoutProperty } from '../../../core/layout/getLayoutProperty'
1+
import type { JSXAttributes, PropertyPath } from 'utopia-shared/src/types'
22
import type { StyleLayoutProp } from '../../../core/layout/layout-helpers-new'
3-
import { MetadataUtils } from '../../../core/model/element-metadata-utils'
4-
import { mapDropNulls } from '../../../core/shared/array-utils'
5-
import { defaultEither, isLeft, mapEither, right } from '../../../core/shared/either'
6-
import type { JSXElement } from '../../../core/shared/element-template'
3+
import * as Either from '../../../core/shared/either'
4+
import {
5+
getJSXAttributesAtPath,
6+
jsxSimpleAttributeToValue,
7+
} from '../../../core/shared/jsx-attribute-utils'
8+
import type { ModifiableAttribute } from '../../../core/shared/jsx-attributes'
9+
import { getJSXElementFromProjectContents } from '../../editor/store/editor-state'
10+
import { cssParsers, type ParsedCSSProperties } from '../../inspector/common/css-utils'
11+
import { stylePropPathMappingFn } from '../../inspector/common/property-path-hooks'
12+
import type { CSSStyleProperty } from '../canvas-types'
713
import {
8-
emptyComments,
9-
isJSXElement,
10-
jsExpressionValue,
11-
} from '../../../core/shared/element-template'
14+
cssStyleProperty,
15+
cssStylePropertyNotParsable,
16+
cssStylePropertyNotFound,
17+
} from '../canvas-types'
18+
import { mapDropNulls } from '../../../core/shared/array-utils'
19+
import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template'
1220
import * as PP from '../../../core/shared/property-path'
13-
import { styleStringInArray } from '../../../utils/common-constants'
14-
import type { ParsedCSSProperties } from '../../inspector/common/css-utils'
15-
import { withPropertyTag, type WithPropertyTag } from '../canvas-types'
1621
import { applyValuesAtPath, deleteValuesAtPath } from '../commands/utils/property-utils'
1722
import type { StylePlugin } from './style-plugins'
1823

24+
function getPropValue(attributes: JSXAttributes, path: PropertyPath): ModifiableAttribute {
25+
const result = getJSXAttributesAtPath(attributes, path)
26+
if (result.remainingPath != null) {
27+
return { type: 'ATTRIBUTE_NOT_FOUND' }
28+
}
29+
return result.attribute
30+
}
31+
1932
function getPropertyFromInstance<P extends StyleLayoutProp, T = ParsedCSSProperties[P]>(
2033
prop: P,
21-
element: JSXElement,
22-
): WithPropertyTag<T> | null {
23-
return defaultEither(
24-
null,
25-
mapEither(withPropertyTag, getLayoutProperty(prop, right(element.props), styleStringInArray)),
26-
) as WithPropertyTag<T> | null
34+
attributes: JSXAttributes,
35+
): CSSStyleProperty<NonNullable<T>> | null {
36+
const attribute = getPropValue(attributes, stylePropPathMappingFn(prop, ['style']))
37+
if (attribute.type === 'ATTRIBUTE_NOT_FOUND') {
38+
return cssStylePropertyNotFound()
39+
}
40+
const simpleValue = jsxSimpleAttributeToValue(attribute)
41+
if (Either.isLeft(simpleValue)) {
42+
return cssStylePropertyNotParsable()
43+
}
44+
const parser = cssParsers[prop] as (value: unknown) => Either.Either<string, T>
45+
const parsed = parser(simpleValue.value)
46+
if (Either.isLeft(parsed) || parsed.value == null) {
47+
return cssStylePropertyNotParsable()
48+
}
49+
return cssStyleProperty(parsed.value)
2750
}
2851

2952
export const InlineStylePlugin: StylePlugin = {
3053
name: 'Inline Style',
3154
styleInfoFactory:
32-
({ metadata }) =>
55+
({ projectContents }) =>
3356
(elementPath) => {
34-
const instance = MetadataUtils.findElementByElementPath(metadata, elementPath)
35-
if (instance == null || isLeft(instance.element) || !isJSXElement(instance.element.value)) {
57+
const element = getJSXElementFromProjectContents(elementPath, projectContents)
58+
if (element == null) {
3659
return null
3760
}
3861

39-
const gap = getPropertyFromInstance('gap', instance.element.value)
40-
const flexDirection = getPropertyFromInstance('flexDirection', instance.element.value)
62+
const gap = getPropertyFromInstance('gap', element.props)
63+
const flexDirection = getPropertyFromInstance('flexDirection', element.props)
4164

4265
return {
4366
gap: gap,

editor/src/components/canvas/plugins/tailwind-style-plugin.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ import type { Parser } from '../../inspector/common/css-utils'
66
import { cssParsers } from '../../inspector/common/css-utils'
77
import { mapDropNulls } from '../../../core/shared/array-utils'
88
import type { StylePlugin } from './style-plugins'
9-
import type { WithPropertyTag } from '../canvas-types'
10-
import { withPropertyTag } from '../canvas-types'
119
import type { Config } from 'tailwindcss/types/config'
10+
import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types'
1211
import * as UCL from './tailwind-style-plugin-utils/update-class-list'
1312
import { assertNever } from '../../../core/shared/utils'
1413

15-
function parseTailwindProperty<T>(value: unknown, parse: Parser<T>): WithPropertyTag<T> | null {
14+
function parseTailwindProperty<T>(
15+
value: unknown,
16+
parse: Parser<T>,
17+
): CSSStyleProperty<NonNullable<T>> | null {
1618
const parsed = parse(value, null)
17-
if (isLeft(parsed)) {
19+
if (isLeft(parsed) || parsed.value == null) {
1820
return null
1921
}
20-
return withPropertyTag(parsed.value)
22+
return cssStyleProperty(parsed.value)
2123
}
2224

2325
const TailwindPropertyMapping: Record<string, string> = {

0 commit comments

Comments
 (0)