Skip to content

Commit

Permalink
Tailwind aware padding (#6637)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
bkrmendy authored Nov 15, 2024
1 parent 0bd1759 commit 35148f9
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 57 deletions.
2 changes: 1 addition & 1 deletion editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
"@types/w3c-css-typed-object-model-level-1": "20180410.0.5",
"@use-it/interval": "0.1.3",
"@vercel/stega": "0.1.0",
"@xengine/tailwindcss-class-parser": "1.1.17",
"@xengine/tailwindcss-class-parser": "1.1.18",
"ajv": "6.4.0",
"anser": "2.1.0",
"antd": "4.3.5",
Expand Down
8 changes: 4 additions & 4 deletions editor/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import * as EP from '../../../../core/shared/element-path'
import { assertNever } from '../../../../core/shared/utils'
import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config'
import { createModifiedProject } from '../../../../sample-projects/sample-project-utils.test-utils'
import type { Modifiers } from '../../../../utils/modifiers'
import { cmdModifier, shiftModifier } from '../../../../utils/modifiers'
import { expectSingleUndo2Saves, wait } from '../../../../utils/utils.test-utils'
import { cmdModifier } from '../../../../utils/modifiers'
import {
expectSingleUndo2Saves,
selectComponentsForTest,
setFeatureForBrowserTestsUseInDescribeBlockOnly,
wait,
} from '../../../../utils/utils.test-utils'
import { StoryboardFilePath } from '../../../editor/store/editor-state'
import { cssNumber } from '../../../inspector/common/css-utils'
import type { EdgePiece } from '../../canvas-types'
import { isHorizontalEdgePiece } from '../../canvas-types'
Expand Down Expand Up @@ -39,6 +48,7 @@ import {
getPrintedUiJsCode,
makeTestProjectCodeWithSnippet,
renderTestEditorWithCode,
renderTestEditorWithModel,
} from '../../ui-jsx.test-utils'
import { PaddingTearThreshold, SetPaddingStrategyName } from './set-padding-strategy'

Expand Down Expand Up @@ -745,6 +755,88 @@ describe('Padding resize strategy', () => {
})
})
})

describe('Tailwind', () => {
setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true)

const TailwindProject = (classes: string) =>
createModifiedProject({
[StoryboardFilePath]: `
import React from 'react'
import { Scene, Storyboard } from 'utopia-api'
export var storyboard = (
<Storyboard data-uid='sb'>
<Scene
id='scene'
commentId='scene'
data-uid='scene'
style={{
width: 700,
height: 759,
position: 'absolute',
left: 212,
top: 128,
}}
>
<div
data-uid='mydiv'
data-testid='mydiv'
className='top-10 left-10 absolute flex flex-row ${classes}'
>
<div className='bg-red-500 w-10 h-10' data-uid='child-1' />
<div className='bg-red-500 w-10 h-10' data-uid='child-2' />
</div>
</Scene>
</Storyboard>
)
`,
[TailwindConfigPath]: `
const TailwindConfig = { }
export default TailwindConfig
`,
'app.css': `
@tailwind base;
@tailwind components;
@tailwind utilities;`,
})

it('can set tailwind padding', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('p-12'),
'await-first-dom-report',
)
await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')])
await testPaddingResizeForEdge(editor, 50, 'top', 'precise')
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual('top-10 left-10 absolute flex flex-row p-[6rem_3rem_3rem_3rem]')
})

it('can remove tailwind padding', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('p-4'),
'await-first-dom-report',
)
await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')])
await testPaddingResizeForEdge(editor, -150, 'top', 'precise')
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pb-4 pl-4 pr-4')
})

it('can set tailwind padding longhand', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('pt-12'),
'await-first-dom-report',
)
await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')])
await testPaddingResizeForEdge(editor, 50, 'top', 'precise')
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pt-24')
})
})
})

async function testAdjustIndividualPaddingValue(edge: EdgePiece, precision: AdjustPrecision) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ import {
paddingPropForEdge,
paddingToPaddingString,
printCssNumberWithDefaultUnit,
simplePaddingFromMetadata,
simplePaddingFromStyleInfo,
} from '../../padding-utils'
import type { CanvasStrategyFactory } from '../canvas-strategies'
import { onlyFitWhenDraggingThisControl } from '../canvas-strategies'
import type { InteractionCanvasState } from '../canvas-strategy-types'
import type { InteractionCanvasState, StyleInfoReader } from '../canvas-strategy-types'
import {
controlWithProps,
emptyStrategyApplicationResult,
Expand Down Expand Up @@ -114,6 +114,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti
canvasState.startingMetadata,
canvasState.startingElementPathTree,
selectedElements[0],
canvasState.styleInfoReader,
)
) {
return null
Expand Down Expand Up @@ -180,7 +181,11 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti

const edgePiece = interactionSession.activeControl.edgePiece
const drag = interactionSession.interactionData.drag ?? canvasVector({ x: 0, y: 0 })
const padding = simplePaddingFromMetadata(canvasState.startingMetadata, selectedElement)
const padding = simplePaddingFromStyleInfo(
canvasState.startingMetadata,
selectedElement,
canvasState.styleInfoReader(selectedElement),
)
const paddingPropInteractedWith = paddingPropForEdge(edgePiece)
const currentPadding = padding[paddingPropInteractedWith]?.renderedValuePx ?? 0
const rawDelta = deltaFromEdge(drag, edgePiece)
Expand Down Expand Up @@ -347,6 +352,7 @@ function supportsPaddingControls(
metadata: ElementInstanceMetadataMap,
pathTrees: ElementPathTrees,
path: ElementPath,
styleInfoReader: StyleInfoReader,
): boolean {
const element = MetadataUtils.findElementByElementPath(metadata, path)
if (element == null) {
Expand All @@ -361,7 +367,7 @@ function supportsPaddingControls(
return false
}

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

const edgePiece = interactionSession.activeControl.edgePiece

const padding = simplePaddingFromMetadata(
const padding = simplePaddingFromStyleInfo(
canvasState.startingMetadata,
filteredSelectedElements[0],
canvasState.styleInfoReader(filteredSelectedElements[0]),
)
const currentPadding =
padding[paddingPropForEdge(edgePiece)] ?? unitlessCSSNumberWithRenderedValue(0)
Expand Down Expand Up @@ -554,7 +561,11 @@ function calculateAdjustDelta(

const edgePiece = interactionSession.activeControl.edgePiece
const drag = interactionSession.interactionData.drag ?? canvasVector({ x: 0, y: 0 })
const padding = simplePaddingFromMetadata(canvasState.startingMetadata, selectedElement)
const padding = simplePaddingFromStyleInfo(
canvasState.startingMetadata,
selectedElement,
canvasState.styleInfoReader(selectedElement),
)
const paddingPropInteractedWith = paddingPropForEdge(edgePiece)
const currentPadding = padding[paddingPropForEdge(edgePiece)]?.renderedValuePx ?? 0
const rawDelta = deltaFromEdge(drag, edgePiece)
Expand Down
14 changes: 13 additions & 1 deletion editor/src/components/canvas/canvas-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {
import { InteractionSession } from './canvas-strategies/interaction-state'
import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types'
import type { MouseButtonsPressed } from '../../utils/mouse'
import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils'
import type { CSSNumber, CSSPadding, FlexDirection } from '../inspector/common/css-utils'

export const CanvasContainerID = 'canvas-container'

Expand Down Expand Up @@ -584,6 +584,8 @@ export type BottomInfo = CSSStyleProperty<CSSNumber>
export type WidthInfo = CSSStyleProperty<CSSNumber>
export type HeightInfo = CSSStyleProperty<CSSNumber>
export type FlexBasisInfo = CSSStyleProperty<CSSNumber>
export type PaddingInfo = CSSStyleProperty<CSSPadding>
export type PaddingSideInfo = CSSStyleProperty<CSSNumber>

export interface StyleInfo {
gap: FlexGapInfo | null
Expand All @@ -595,6 +597,11 @@ export interface StyleInfo {
width: WidthInfo | null
height: HeightInfo | null
flexBasis: FlexBasisInfo | null
padding: PaddingInfo | null
paddingTop: PaddingSideInfo | null
paddingRight: PaddingSideInfo | null
paddingBottom: PaddingSideInfo | null
paddingLeft: PaddingSideInfo | null
}

const emptyStyleInfo: StyleInfo = {
Expand All @@ -607,6 +614,11 @@ const emptyStyleInfo: StyleInfo = {
width: null,
height: null,
flexBasis: null,
padding: null,
paddingTop: null,
paddingRight: null,
paddingBottom: null,
paddingLeft: null,
}

export const isStyleInfoKey = (key: string): key is keyof StyleInfo => key in emptyStyleInfo
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
paddingAdjustMode,
paddingFromSpecialSizeMeasurements,
PaddingIndictorOffset,
simplePaddingFromMetadata,
simplePaddingFromStyleInfo,
} from '../../padding-utils'
import { useBoundingBox } from '../bounding-box-hooks'
import { CanvasOffsetWrapper } from '../canvas-offset-wrapper'
Expand All @@ -43,6 +43,7 @@ import type { CSSNumberWithRenderedValue } from './controls-common'
import { CanvasLabel, fallbackEmptyValue, PillHandle, useHoverWithDelay } from './controls-common'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { mapDropNulls } from '../../../../core/shared/array-utils'
import { getActivePlugin } from '../../plugins/style-plugins'

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

const styleInfoReaderRef = useRefEditorState((store) =>
getActivePlugin(store.editor).styleInfoFactory({
projectContents: store.editor.projectContents,
}),
)

const currentPadding = React.useMemo(() => {
return combinePaddings(
paddingFromSpecialSizeMeasurements(elementMetadata, selectedElements[0]),
simplePaddingFromMetadata(elementMetadata, selectedElements[0]),
simplePaddingFromStyleInfo(
elementMetadata,
selectedElements[0],
styleInfoReaderRef.current(selectedElements[0]),
),
)
}, [elementMetadata, selectedElements])
}, [elementMetadata, selectedElements, styleInfoReaderRef])

const shownByParent = selectedElementHovered || anyControlHovered

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React from 'react'
import { useColorTheme } from '../../../../uuiui'
import { Substores, useEditorState, useRefEditorState } from '../../../editor/store/store-hook'
import type { EdgePiece } from '../../canvas-types'
import { paddingPropForEdge, simplePaddingFromMetadata } from '../../padding-utils'
import { paddingPropForEdge, simplePaddingFromStyleInfo } from '../../padding-utils'
import { useBoundingBox } from '../bounding-box-hooks'
import { CanvasOffsetWrapper } from '../canvas-offset-wrapper'
import { getActivePlugin } from '../../plugins/style-plugins'

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

const styleInfoReaderRef = useRefEditorState((store) =>
getActivePlugin(store.editor).styleInfoFactory({
projectContents: store.editor.projectContents,
}),
)

// TODO Multiselect
const sideRef = useBoundingBox(targets, (ref, boundingBox) => {
const padding = simplePaddingFromMetadata(elementMetadata.current, targets[0])
const padding = simplePaddingFromStyleInfo(
elementMetadata.current,
targets[0],
styleInfoReaderRef.current(targets[0]),
)
const paddingValue = padding[paddingKey]?.renderedValuePx ?? 0

const { x, y, width, height } = boundingBox
Expand Down
Loading

0 comments on commit 35148f9

Please sign in to comment.