Skip to content

Commit 21024fe

Browse files
authored
Style plugin-aware useInspectorInfo (#6655)
## Problem `useInspectorInfo`, a widely used helper in the inspector, cannot read element styles set by Tailwind ## Fix The fix is made up from multiple pieces: - I extended `StylePlugin` with a new function, `readStyleFromElementProps`, which reads a given style info key from the `JSXAttributes` passed to it. I updated both `InlineStylePlugin` and `TailwindStylePlugin` to support this function, and updated all affected tests. This change was originally prototyped on the [Megaspike](#6574) - I updated `useGetMultiselectedProps` to use `StylePlugin.readStyleFromElementProps` when a style prop is read. - I updated `CSSStylePropertyNotParsable` and `ParsedCSSStyleProperty` to preserve the original value read from `projectContents`, in addition to the parsed representation. This way, `CSSStyleProperties` can be used by code that expect to work with this lower-level representation (such as the internals of `useInspectorInfo`) - I updated the `SET_PROP` and `UNSET_PROP` actions to use the `setProperty` and `deleteProperty` commands under the hood. This way, any editor code using these actions will be able to use the style plugins to write element style. This change was originally prototyped on the [Megaspike](#6574) ### Out of scope This change only touches `useGetMultiselectedProps` from the internals of `useInspectorInfo`. `useGetSpiedProps` is left unchanged, since the values element style props set through Tailwind don't show up in `allElementProps` (which is the data source for spied props) ## 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 dca813f commit 21024fe

File tree

8 files changed

+182
-101
lines changed

8 files changed

+182
-101
lines changed

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ReactElement } from 'react'
2+
import type { JSExpression, PartOfJSXAttributeValue } from '../../core/shared/element-template'
23
import { ElementInstanceMetadataMap } from '../../core/shared/element-template'
34
import type { PropertyPath, ElementPath } from '../../core/shared/project-file-types'
45
import type { KeysPressed } from '../../utils/keyboard'
@@ -543,11 +544,13 @@ interface CSSStylePropertyNotFound {
543544

544545
interface CSSStylePropertyNotParsable {
545546
type: 'not-parsable'
547+
originalValue: JSExpression | PartOfJSXAttributeValue
546548
}
547549

548550
interface ParsedCSSStyleProperty<T> {
549551
type: 'property'
550552
tags: PropertyTag[]
553+
propertyValue: JSExpression | PartOfJSXAttributeValue
551554
value: T
552555
}
553556

@@ -560,12 +563,17 @@ export function cssStylePropertyNotFound(): CSSStylePropertyNotFound {
560563
return { type: 'not-found' }
561564
}
562565

563-
export function cssStylePropertyNotParsable(): CSSStylePropertyNotParsable {
564-
return { type: 'not-parsable' }
566+
export function cssStylePropertyNotParsable(
567+
originalValue: JSExpression | PartOfJSXAttributeValue,
568+
): CSSStylePropertyNotParsable {
569+
return { type: 'not-parsable', originalValue: originalValue }
565570
}
566571

567-
export function cssStyleProperty<T>(value: T): ParsedCSSStyleProperty<T> {
568-
return { type: 'property', tags: [], value: value }
572+
export function cssStyleProperty<T>(
573+
value: T,
574+
propertyValue: JSExpression | PartOfJSXAttributeValue,
575+
): ParsedCSSStyleProperty<T> {
576+
return { type: 'property', tags: [], value: value, propertyValue: propertyValue }
569577
}
570578

571579
export function maybePropertyValue<T>(property: CSSStyleProperty<T>): T | null {

editor/src/components/canvas/commands/utils/property-utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { modifyUnderlyingElementForOpenFile } from '../../../editor/store/editor
77
import { patchParseSuccessAtElementPath } from '../patch-utils'
88
import type { CSSNumber } from '../../../inspector/common/css-utils'
99
import { isCSSNumber } from '../../../inspector/common/css-utils'
10-
import { type StyleInfo, isStyleInfoKey } from '../../canvas-types'
10+
import type { StyleInfo } from '../../canvas-types'
1111

1212
export interface EditorStateWithPatch {
1313
editorStateWithChanges: EditorState

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

+21-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import * as EP from '../../../core/shared/element-path'
22
import { cssNumber } from '../../inspector/common/css-utils'
3-
import {
4-
cssStyleProperty,
5-
cssStylePropertyNotFound,
6-
cssStylePropertyNotParsable,
7-
} from '../canvas-types'
3+
import { cssStylePropertyNotFound } from '../canvas-types'
84
import type { EditorRenderResult } from '../ui-jsx.test-utils'
95
import { renderTestEditorWithCode } from '../ui-jsx.test-utils'
106
import { InlineStylePlugin } from './inline-style-plugin'
@@ -45,8 +41,18 @@ export var storyboard = (
4541

4642
expect(styleInfo).not.toBeNull()
4743
const { flexDirection, gap } = styleInfo!
48-
expect(flexDirection).toEqual(cssStyleProperty('column'))
49-
expect(gap).toEqual(cssStyleProperty(cssNumber(2, 'rem')))
44+
expect(flexDirection).toMatchObject({
45+
type: 'property',
46+
tags: [],
47+
value: 'column',
48+
propertyValue: { value: 'column' },
49+
})
50+
expect(gap).toMatchObject({
51+
type: 'property',
52+
tags: [],
53+
value: cssNumber(2, 'rem'),
54+
propertyValue: { value: '2rem' },
55+
})
5056
})
5157

5258
it('can parse style info with missing/unparsable props', async () => {
@@ -88,7 +94,14 @@ export var storyboard = (
8894
expect(styleInfo).not.toBeNull()
8995
const { flexDirection, gap } = styleInfo!
9096
expect(flexDirection).toEqual(cssStylePropertyNotFound())
91-
expect(gap).toEqual(cssStylePropertyNotParsable())
97+
expect(gap).toMatchObject({
98+
type: 'not-parsable',
99+
originalValue: {
100+
type: 'JS_PROPERTY_ACCESS',
101+
onValue: { type: 'JS_IDENTIFIER', name: 'gap' },
102+
property: 'small',
103+
},
104+
})
92105
})
93106
})
94107

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { JSXAttributes, PropertyPath } from 'utopia-shared/src/types'
2-
import type { StyleLayoutProp } from '../../../core/layout/layout-helpers-new'
32
import * as Either from '../../../core/shared/either'
43
import {
54
getJSXAttributesAtPath,
@@ -9,7 +8,7 @@ import type { ModifiableAttribute } from '../../../core/shared/jsx-attributes'
98
import { getJSXElementFromProjectContents } from '../../editor/store/editor-state'
109
import { cssParsers, type ParsedCSSProperties } from '../../inspector/common/css-utils'
1110
import { stylePropPathMappingFn } from '../../inspector/common/property-path-hooks'
12-
import type { CSSStyleProperty } from '../canvas-types'
11+
import type { CSSStyleProperty, StyleInfo } from '../canvas-types'
1312
import {
1413
cssStyleProperty,
1514
cssStylePropertyNotParsable,
@@ -29,7 +28,7 @@ function getPropValue(attributes: JSXAttributes, path: PropertyPath): Modifiable
2928
return result.attribute
3029
}
3130

32-
function getPropertyFromInstance<P extends StyleLayoutProp, T = ParsedCSSProperties[P]>(
31+
function getPropertyFromInstance<P extends keyof StyleInfo, T = ParsedCSSProperties[P]>(
3332
prop: P,
3433
attributes: JSXAttributes,
3534
): CSSStyleProperty<NonNullable<T>> | null {
@@ -39,18 +38,24 @@ function getPropertyFromInstance<P extends StyleLayoutProp, T = ParsedCSSPropert
3938
}
4039
const simpleValue = jsxSimpleAttributeToValue(attribute)
4140
if (Either.isLeft(simpleValue)) {
42-
return cssStylePropertyNotParsable()
41+
return cssStylePropertyNotParsable(attribute)
4342
}
4443
const parser = cssParsers[prop] as (value: unknown) => Either.Either<string, T>
4544
const parsed = parser(simpleValue.value)
4645
if (Either.isLeft(parsed) || parsed.value == null) {
47-
return cssStylePropertyNotParsable()
46+
return cssStylePropertyNotParsable(attribute)
4847
}
49-
return cssStyleProperty(parsed.value)
48+
return cssStyleProperty(parsed.value, attribute)
5049
}
5150

5251
export const InlineStylePlugin: StylePlugin = {
5352
name: 'Inline Style',
53+
readStyleFromElementProps: <T extends keyof StyleInfo>(
54+
attributes: JSXAttributes,
55+
prop: T,
56+
): CSSStyleProperty<NonNullable<ParsedCSSProperties[T]>> | null => {
57+
return getPropertyFromInstance(prop, attributes)
58+
},
5459
styleInfoFactory:
5560
({ projectContents }) =>
5661
(elementPath) => {

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ElementPath } from 'utopia-shared/src/types'
1+
import type { ElementPath, JSXAttributes } from 'utopia-shared/src/types'
22
import type { EditorState, EditorStatePatch } from '../../editor/store/editor-state'
33
import type {
44
InteractionLifecycle,
@@ -18,7 +18,9 @@ import type { EditorStateWithPatch } from '../commands/utils/property-utils'
1818
import { applyValuesAtPath } from '../commands/utils/property-utils'
1919
import * as PP from '../../../core/shared/property-path'
2020
import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template'
21+
import type { CSSStyleProperty } from '../canvas-types'
2122
import { isStyleInfoKey, type StyleInfo } from '../canvas-types'
23+
import type { ParsedCSSProperties } from '../../inspector/common/css-utils'
2224

2325
export interface UpdateCSSProp {
2426
type: 'set'
@@ -51,6 +53,10 @@ export type StyleUpdate = UpdateCSSProp | DeleteCSSProp
5153
export interface StylePlugin {
5254
name: string
5355
styleInfoFactory: StyleInfoFactory
56+
readStyleFromElementProps: <T extends keyof StyleInfo>(
57+
attributes: JSXAttributes,
58+
prop: T,
59+
) => CSSStyleProperty<NonNullable<ParsedCSSProperties[T]>> | null
5460
updateStyles: (
5561
editorState: EditorState,
5662
elementPath: ElementPath,

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

+53-29
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
import * as TailwindClassParser from '@xengine/tailwindcss-class-parser'
2-
import { isLeft } from '../../../core/shared/either'
2+
import { defaultEither, flatMapEither, isLeft } from '../../../core/shared/either'
33
import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options'
44
import { getElementFromProjectContents } from '../../editor/store/editor-state'
5-
import type { Parser } from '../../inspector/common/css-utils'
5+
import type { ParsedCSSProperties, 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'
99
import type { Config } from 'tailwindcss/types/config'
10-
import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types'
10+
import type { StyleInfo } from '../canvas-types'
11+
import { cssStyleProperty, isStyleInfoKey, type CSSStyleProperty } from '../canvas-types'
1112
import * as UCL from './tailwind-style-plugin-utils/update-class-list'
1213
import { assertNever } from '../../../core/shared/utils'
14+
import {
15+
jsxSimpleAttributeToValue,
16+
getModifiableJSXAttributeAtPath,
17+
} from '../../../core/shared/jsx-attribute-utils'
18+
import { emptyComments, type JSXAttributes } from 'utopia-shared/src/types'
19+
import * as PP from '../../../core/shared/property-path'
20+
import { jsExpressionValue } from '../../../core/shared/element-template'
1321

14-
function parseTailwindProperty<T>(
15-
value: unknown,
16-
parse: Parser<T>,
17-
): CSSStyleProperty<NonNullable<T>> | null {
18-
const parsed = parse(value, null)
22+
function parseTailwindProperty<T extends keyof StyleInfo>(
23+
value: string | number | undefined,
24+
prop: T,
25+
): CSSStyleProperty<NonNullable<ParsedCSSProperties[T]>> | null {
26+
const parsed = cssParsers[prop](value, null)
1927
if (isLeft(parsed) || parsed.value == null) {
2028
return null
2129
}
22-
return cssStyleProperty(parsed.value)
30+
return cssStyleProperty(parsed.value, jsExpressionValue(value, emptyComments))
2331
}
2432

2533
const TailwindPropertyMapping: Record<string, string> = {
@@ -81,6 +89,25 @@ const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ')
8189

8290
export const TailwindPlugin = (config: Config | null): StylePlugin => ({
8391
name: 'Tailwind',
92+
readStyleFromElementProps: <P extends keyof StyleInfo>(
93+
attributes: JSXAttributes,
94+
prop: P,
95+
): CSSStyleProperty<NonNullable<ParsedCSSProperties[P]>> | null => {
96+
const classNameAttribute = defaultEither(
97+
null,
98+
flatMapEither(
99+
(attr) => jsxSimpleAttributeToValue(attr),
100+
getModifiableJSXAttributeAtPath(attributes, PP.create('className')),
101+
),
102+
)
103+
104+
if (typeof classNameAttribute !== 'string') {
105+
return null
106+
}
107+
108+
const mapping = getTailwindClassMapping(classNameAttribute.split(' '), config)
109+
return parseTailwindProperty(mapping[TailwindPropertyMapping[prop]], prop)
110+
},
84111
styleInfoFactory:
85112
({ projectContents }) =>
86113
(elementPath) => {
@@ -95,56 +122,53 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({
95122
const mapping = getTailwindClassMapping(classList.split(' '), config)
96123

97124
return {
98-
gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], cssParsers.gap),
125+
gap: parseTailwindProperty(mapping[TailwindPropertyMapping.gap], 'gap'),
99126
flexDirection: parseTailwindProperty(
100127
mapping[TailwindPropertyMapping.flexDirection],
101-
cssParsers.flexDirection,
102-
),
103-
left: parseTailwindProperty(mapping[TailwindPropertyMapping.left], cssParsers.left),
104-
right: parseTailwindProperty(mapping[TailwindPropertyMapping.right], cssParsers.right),
105-
top: parseTailwindProperty(mapping[TailwindPropertyMapping.top], cssParsers.top),
106-
bottom: parseTailwindProperty(mapping[TailwindPropertyMapping.bottom], cssParsers.bottom),
107-
width: parseTailwindProperty(mapping[TailwindPropertyMapping.width], cssParsers.width),
108-
height: parseTailwindProperty(mapping[TailwindPropertyMapping.height], cssParsers.height),
109-
flexBasis: parseTailwindProperty(
110-
mapping[TailwindPropertyMapping.flexBasis],
111-
cssParsers.flexBasis,
128+
'flexDirection',
112129
),
130+
left: parseTailwindProperty(mapping[TailwindPropertyMapping.left], 'left'),
131+
right: parseTailwindProperty(mapping[TailwindPropertyMapping.right], 'right'),
132+
top: parseTailwindProperty(mapping[TailwindPropertyMapping.top], 'top'),
133+
bottom: parseTailwindProperty(mapping[TailwindPropertyMapping.bottom], 'bottom'),
134+
width: parseTailwindProperty(mapping[TailwindPropertyMapping.width], 'width'),
135+
height: parseTailwindProperty(mapping[TailwindPropertyMapping.height], 'height'),
136+
flexBasis: parseTailwindProperty(mapping[TailwindPropertyMapping.flexBasis], 'flexBasis'),
113137
padding: parseTailwindProperty(
114138
underscoresToSpaces(mapping[TailwindPropertyMapping.padding]),
115-
cssParsers.padding,
139+
'padding',
116140
),
117141
paddingTop: parseTailwindProperty(
118142
mapping[TailwindPropertyMapping.paddingTop],
119-
cssParsers.paddingTop,
143+
'paddingTop',
120144
),
121145
paddingRight: parseTailwindProperty(
122146
mapping[TailwindPropertyMapping.paddingRight],
123-
cssParsers.paddingRight,
147+
'paddingRight',
124148
),
125149
paddingBottom: parseTailwindProperty(
126150
mapping[TailwindPropertyMapping.paddingBottom],
127-
cssParsers.paddingBottom,
151+
'paddingBottom',
128152
),
129153
paddingLeft: parseTailwindProperty(
130154
mapping[TailwindPropertyMapping.paddingLeft],
131-
cssParsers.paddingLeft,
155+
'paddingLeft',
132156
),
133-
zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], cssParsers.zIndex),
157+
zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], 'zIndex'),
134158
}
135159
},
136160
updateStyles: (editorState, elementPath, updates) => {
137161
const propsToDelete = mapDropNulls(
138162
(update) =>
139-
update.type !== 'delete' || TailwindPropertyMapping[update.property] == null
163+
update.type !== 'delete' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe
140164
? null
141165
: UCL.remove(TailwindPropertyMapping[update.property]),
142166
updates,
143167
)
144168

145169
const propsToSet = mapDropNulls(
146170
(update) =>
147-
update.type !== 'set' || TailwindPropertyMapping[update.property] == null
171+
update.type !== 'set' || TailwindPropertyMapping[update.property] == null // TODO: make this type-safe
148172
? null
149173
: UCL.add({
150174
property: TailwindPropertyMapping[update.property],

0 commit comments

Comments
 (0)