Skip to content

Commit 35148f9

Browse files
authored
Tailwind aware padding (#6637)
## Problem The padding controls cannot be used to read/write values in projects that use Tailwind for styling. ## Fix Use the style plugins and the `StyleInfo` system to make this possible. Specifically, - Add the padding shorthand prop/padding longhand prop to the `StyleInfo` interface - Update `InlineStylePlugin` and `TailwindStylePlugin` to support the new props in `StyleInfo` - Refactor the padding strategy, the padding control handle and the subdued padding control to read element styles through a `StyleInfoReader` instance - Add a new property patcher in `style-plugins@patchers` to take care of patching removed padding props - Add tests with a tailwind project to the padding strategy test suite ### Out of scope The jump in the bounding box after the interaction ends ([video](https://screenshot.click/14-02-e9ktf-say96.mp4)) will be addressed on a follow-up PR ### 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 0bd1759 commit 35148f9

11 files changed

+222
-57
lines changed

editor/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
"@types/w3c-css-typed-object-model-level-1": "20180410.0.5",
172172
"@use-it/interval": "0.1.3",
173173
"@vercel/stega": "0.1.0",
174-
"@xengine/tailwindcss-class-parser": "1.1.17",
174+
"@xengine/tailwindcss-class-parser": "1.1.18",
175175
"ajv": "6.4.0",
176176
"anser": "2.1.0",
177177
"antd": "4.3.5",

editor/pnpm-lock.yaml

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx

+94-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import * as EP from '../../../../core/shared/element-path'
12
import { assertNever } from '../../../../core/shared/utils'
3+
import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config'
4+
import { createModifiedProject } from '../../../../sample-projects/sample-project-utils.test-utils'
25
import type { Modifiers } from '../../../../utils/modifiers'
3-
import { cmdModifier, shiftModifier } from '../../../../utils/modifiers'
4-
import { expectSingleUndo2Saves, wait } from '../../../../utils/utils.test-utils'
6+
import { cmdModifier } from '../../../../utils/modifiers'
7+
import {
8+
expectSingleUndo2Saves,
9+
selectComponentsForTest,
10+
setFeatureForBrowserTestsUseInDescribeBlockOnly,
11+
wait,
12+
} from '../../../../utils/utils.test-utils'
13+
import { StoryboardFilePath } from '../../../editor/store/editor-state'
514
import { cssNumber } from '../../../inspector/common/css-utils'
615
import type { EdgePiece } from '../../canvas-types'
716
import { isHorizontalEdgePiece } from '../../canvas-types'
@@ -39,6 +48,7 @@ import {
3948
getPrintedUiJsCode,
4049
makeTestProjectCodeWithSnippet,
4150
renderTestEditorWithCode,
51+
renderTestEditorWithModel,
4252
} from '../../ui-jsx.test-utils'
4353
import { PaddingTearThreshold, SetPaddingStrategyName } from './set-padding-strategy'
4454

@@ -745,6 +755,88 @@ describe('Padding resize strategy', () => {
745755
})
746756
})
747757
})
758+
759+
describe('Tailwind', () => {
760+
setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true)
761+
762+
const TailwindProject = (classes: string) =>
763+
createModifiedProject({
764+
[StoryboardFilePath]: `
765+
import React from 'react'
766+
import { Scene, Storyboard } from 'utopia-api'
767+
export var storyboard = (
768+
<Storyboard data-uid='sb'>
769+
<Scene
770+
id='scene'
771+
commentId='scene'
772+
data-uid='scene'
773+
style={{
774+
width: 700,
775+
height: 759,
776+
position: 'absolute',
777+
left: 212,
778+
top: 128,
779+
}}
780+
>
781+
<div
782+
data-uid='mydiv'
783+
data-testid='mydiv'
784+
className='top-10 left-10 absolute flex flex-row ${classes}'
785+
>
786+
<div className='bg-red-500 w-10 h-10' data-uid='child-1' />
787+
<div className='bg-red-500 w-10 h-10' data-uid='child-2' />
788+
</div>
789+
</Scene>
790+
</Storyboard>
791+
)
792+
793+
`,
794+
[TailwindConfigPath]: `
795+
const TailwindConfig = { }
796+
export default TailwindConfig
797+
`,
798+
'app.css': `
799+
@tailwind base;
800+
@tailwind components;
801+
@tailwind utilities;`,
802+
})
803+
804+
it('can set tailwind padding', async () => {
805+
const editor = await renderTestEditorWithModel(
806+
TailwindProject('p-12'),
807+
'await-first-dom-report',
808+
)
809+
await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')])
810+
await testPaddingResizeForEdge(editor, 50, 'top', 'precise')
811+
await editor.getDispatchFollowUpActionsFinished()
812+
const div = editor.renderedDOM.getByTestId('mydiv')
813+
expect(div.className).toEqual('top-10 left-10 absolute flex flex-row p-[6rem_3rem_3rem_3rem]')
814+
})
815+
816+
it('can remove tailwind padding', async () => {
817+
const editor = await renderTestEditorWithModel(
818+
TailwindProject('p-4'),
819+
'await-first-dom-report',
820+
)
821+
await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')])
822+
await testPaddingResizeForEdge(editor, -150, 'top', 'precise')
823+
await editor.getDispatchFollowUpActionsFinished()
824+
const div = editor.renderedDOM.getByTestId('mydiv')
825+
expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pb-4 pl-4 pr-4')
826+
})
827+
828+
it('can set tailwind padding longhand', async () => {
829+
const editor = await renderTestEditorWithModel(
830+
TailwindProject('pt-12'),
831+
'await-first-dom-report',
832+
)
833+
await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')])
834+
await testPaddingResizeForEdge(editor, 50, 'top', 'precise')
835+
await editor.getDispatchFollowUpActionsFinished()
836+
const div = editor.renderedDOM.getByTestId('mydiv')
837+
expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pt-24')
838+
})
839+
})
748840
})
749841

750842
async function testAdjustIndividualPaddingValue(edge: EdgePiece, precision: AdjustPrecision) {

editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ import {
3232
paddingPropForEdge,
3333
paddingToPaddingString,
3434
printCssNumberWithDefaultUnit,
35-
simplePaddingFromMetadata,
35+
simplePaddingFromStyleInfo,
3636
} from '../../padding-utils'
3737
import type { CanvasStrategyFactory } from '../canvas-strategies'
3838
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
39-
import type { InteractionCanvasState } from '../canvas-strategy-types'
39+
import type { InteractionCanvasState, StyleInfoReader } from '../canvas-strategy-types'
4040
import {
4141
controlWithProps,
4242
emptyStrategyApplicationResult,
@@ -114,6 +114,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti
114114
canvasState.startingMetadata,
115115
canvasState.startingElementPathTree,
116116
selectedElements[0],
117+
canvasState.styleInfoReader,
117118
)
118119
) {
119120
return null
@@ -180,7 +181,11 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti
180181

181182
const edgePiece = interactionSession.activeControl.edgePiece
182183
const drag = interactionSession.interactionData.drag ?? canvasVector({ x: 0, y: 0 })
183-
const padding = simplePaddingFromMetadata(canvasState.startingMetadata, selectedElement)
184+
const padding = simplePaddingFromStyleInfo(
185+
canvasState.startingMetadata,
186+
selectedElement,
187+
canvasState.styleInfoReader(selectedElement),
188+
)
184189
const paddingPropInteractedWith = paddingPropForEdge(edgePiece)
185190
const currentPadding = padding[paddingPropInteractedWith]?.renderedValuePx ?? 0
186191
const rawDelta = deltaFromEdge(drag, edgePiece)
@@ -347,6 +352,7 @@ function supportsPaddingControls(
347352
metadata: ElementInstanceMetadataMap,
348353
pathTrees: ElementPathTrees,
349354
path: ElementPath,
355+
styleInfoReader: StyleInfoReader,
350356
): boolean {
351357
const element = MetadataUtils.findElementByElementPath(metadata, path)
352358
if (element == null) {
@@ -361,7 +367,7 @@ function supportsPaddingControls(
361367
return false
362368
}
363369

364-
const padding = simplePaddingFromMetadata(metadata, path)
370+
const padding = simplePaddingFromStyleInfo(metadata, path, styleInfoReader(path))
365371
const { top, right, bottom, left } = element.specialSizeMeasurements.padding
366372
const elementHasNonzeroPaddingFromMeasurements = [top, right, bottom, left].some(
367373
(s) => s != null && s > 0,
@@ -426,9 +432,10 @@ function paddingValueIndicatorProps(
426432

427433
const edgePiece = interactionSession.activeControl.edgePiece
428434

429-
const padding = simplePaddingFromMetadata(
435+
const padding = simplePaddingFromStyleInfo(
430436
canvasState.startingMetadata,
431437
filteredSelectedElements[0],
438+
canvasState.styleInfoReader(filteredSelectedElements[0]),
432439
)
433440
const currentPadding =
434441
padding[paddingPropForEdge(edgePiece)] ?? unitlessCSSNumberWithRenderedValue(0)
@@ -554,7 +561,11 @@ function calculateAdjustDelta(
554561

555562
const edgePiece = interactionSession.activeControl.edgePiece
556563
const drag = interactionSession.interactionData.drag ?? canvasVector({ x: 0, y: 0 })
557-
const padding = simplePaddingFromMetadata(canvasState.startingMetadata, selectedElement)
564+
const padding = simplePaddingFromStyleInfo(
565+
canvasState.startingMetadata,
566+
selectedElement,
567+
canvasState.styleInfoReader(selectedElement),
568+
)
558569
const paddingPropInteractedWith = paddingPropForEdge(edgePiece)
559570
const currentPadding = padding[paddingPropForEdge(edgePiece)]?.renderedValuePx ?? 0
560571
const rawDelta = deltaFromEdge(drag, edgePiece)

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
import { InteractionSession } from './canvas-strategies/interaction-state'
2727
import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types'
2828
import type { MouseButtonsPressed } from '../../utils/mouse'
29-
import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils'
29+
import type { CSSNumber, CSSPadding, FlexDirection } from '../inspector/common/css-utils'
3030

3131
export const CanvasContainerID = 'canvas-container'
3232

@@ -584,6 +584,8 @@ export type BottomInfo = CSSStyleProperty<CSSNumber>
584584
export type WidthInfo = CSSStyleProperty<CSSNumber>
585585
export type HeightInfo = CSSStyleProperty<CSSNumber>
586586
export type FlexBasisInfo = CSSStyleProperty<CSSNumber>
587+
export type PaddingInfo = CSSStyleProperty<CSSPadding>
588+
export type PaddingSideInfo = CSSStyleProperty<CSSNumber>
587589

588590
export interface StyleInfo {
589591
gap: FlexGapInfo | null
@@ -595,6 +597,11 @@ export interface StyleInfo {
595597
width: WidthInfo | null
596598
height: HeightInfo | null
597599
flexBasis: FlexBasisInfo | null
600+
padding: PaddingInfo | null
601+
paddingTop: PaddingSideInfo | null
602+
paddingRight: PaddingSideInfo | null
603+
paddingBottom: PaddingSideInfo | null
604+
paddingLeft: PaddingSideInfo | null
598605
}
599606

600607
const emptyStyleInfo: StyleInfo = {
@@ -607,6 +614,11 @@ const emptyStyleInfo: StyleInfo = {
607614
width: null,
608615
height: null,
609616
flexBasis: null,
617+
padding: null,
618+
paddingTop: null,
619+
paddingRight: null,
620+
paddingBottom: null,
621+
paddingLeft: null,
610622
}
611623

612624
export const isStyleInfoKey = (key: string): key is keyof StyleInfo => key in emptyStyleInfo

editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx

+14-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
paddingAdjustMode,
3535
paddingFromSpecialSizeMeasurements,
3636
PaddingIndictorOffset,
37-
simplePaddingFromMetadata,
37+
simplePaddingFromStyleInfo,
3838
} from '../../padding-utils'
3939
import { useBoundingBox } from '../bounding-box-hooks'
4040
import { CanvasOffsetWrapper } from '../canvas-offset-wrapper'
@@ -43,6 +43,7 @@ import type { CSSNumberWithRenderedValue } from './controls-common'
4343
import { CanvasLabel, fallbackEmptyValue, PillHandle, useHoverWithDelay } from './controls-common'
4444
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
4545
import { mapDropNulls } from '../../../../core/shared/array-utils'
46+
import { getActivePlugin } from '../../plugins/style-plugins'
4647

4748
export const paddingControlTestId = (edge: EdgePiece): string => `padding-control-${edge}`
4849
export const paddingControlHandleTestId = (edge: EdgePiece): string =>
@@ -358,12 +359,22 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo
358359
}
359360
}, [hoveredViews, selectedElements])
360361

362+
const styleInfoReaderRef = useRefEditorState((store) =>
363+
getActivePlugin(store.editor).styleInfoFactory({
364+
projectContents: store.editor.projectContents,
365+
}),
366+
)
367+
361368
const currentPadding = React.useMemo(() => {
362369
return combinePaddings(
363370
paddingFromSpecialSizeMeasurements(elementMetadata, selectedElements[0]),
364-
simplePaddingFromMetadata(elementMetadata, selectedElements[0]),
371+
simplePaddingFromStyleInfo(
372+
elementMetadata,
373+
selectedElements[0],
374+
styleInfoReaderRef.current(selectedElements[0]),
375+
),
365376
)
366-
}, [elementMetadata, selectedElements])
377+
}, [elementMetadata, selectedElements, styleInfoReaderRef])
367378

368379
const shownByParent = selectedElementHovered || anyControlHovered
369380

editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import React from 'react'
22
import { useColorTheme } from '../../../../uuiui'
33
import { Substores, useEditorState, useRefEditorState } from '../../../editor/store/store-hook'
44
import type { EdgePiece } from '../../canvas-types'
5-
import { paddingPropForEdge, simplePaddingFromMetadata } from '../../padding-utils'
5+
import { paddingPropForEdge, simplePaddingFromStyleInfo } from '../../padding-utils'
66
import { useBoundingBox } from '../bounding-box-hooks'
77
import { CanvasOffsetWrapper } from '../canvas-offset-wrapper'
8+
import { getActivePlugin } from '../../plugins/style-plugins'
89

910
export interface SubduedPaddingControlProps {
1011
side: EdgePiece
@@ -25,9 +26,19 @@ export const SubduedPaddingControl = React.memo<SubduedPaddingControlProps>((pro
2526
const isVerticalPadding = !isHorizontalPadding
2627
const paddingKey = paddingPropForEdge(side)
2728

29+
const styleInfoReaderRef = useRefEditorState((store) =>
30+
getActivePlugin(store.editor).styleInfoFactory({
31+
projectContents: store.editor.projectContents,
32+
}),
33+
)
34+
2835
// TODO Multiselect
2936
const sideRef = useBoundingBox(targets, (ref, boundingBox) => {
30-
const padding = simplePaddingFromMetadata(elementMetadata.current, targets[0])
37+
const padding = simplePaddingFromStyleInfo(
38+
elementMetadata.current,
39+
targets[0],
40+
styleInfoReaderRef.current(targets[0]),
41+
)
3142
const paddingValue = padding[paddingKey]?.renderedValuePx ?? 0
3243

3344
const { x, y, width, height } = boundingBox

0 commit comments

Comments
 (0)