Skip to content

Commit

Permalink
Tailwind border-radius support (#6657)
Browse files Browse the repository at this point in the history
**Problem:**
Make border-radius controls work in tailwind projects.

**Fix:**

- Add the border-radius shorthand/longhand props to the StyleInfo
interface
- Add overflow prop to the StyleInfo interface (necessary because when
we set the border-radius we set the element to overflow: hidden)
- Update InlineStylePlugin and TailwindStylePlugin to support the new
props in StyleInfo
- Refactor the set-border-radius strategy and the border-radius control
handle control to read element styles through a StyleInfoReader instance
- Add a new property patcher in style-plugins@patchers to take care of
patching removed border-radius props
- Add a new EditorState substate so we can get a StyleInfoReader in a
useEditorState hook without using the full store
- Remove the condition in tailwind-compilation that
`ElementsToRerenderGLOBAL.current` has to be `'rerender-all-elements'`
for the tailwind classes to be regenerated. For some reason this blocked
the class generation after a border-radius interaction. We are still
protected against tailwind class generation during the interaction (we
check `isInteractionActive`)
- Add tests with a tailwind project to the set-border-radius strategy
test suite
- removeTailwindClasses guarantees that subsequent tests do not have the
tailwind css on

**Todo:**
- [x] Check why the ``ElementsToRerenderGLOBAL.current` check caused
problems.

**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
gbalint authored Nov 21, 2024
1 parent a4cf331 commit 7db4069
Show file tree
Hide file tree
Showing 12 changed files with 514 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import { fromString } from '../../../../core/shared/element-path'
import type { CanvasVector, Size, WindowPoint } from '../../../../core/shared/math-utils'
import { canvasVector, size, windowPoint } from '../../../../core/shared/math-utils'
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, emptyModifiers } from '../../../../utils/modifiers'
import {
selectComponentsForTest,
setFeatureForBrowserTestsUseInDescribeBlockOnly,
wait,
} from '../../../../utils/utils.test-utils'
import { selectComponents, setFocusedElement } from '../../../editor/actions/action-creators'
import { StoryboardFilePath } from '../../../editor/store/editor-state'
import type { BorderRadiusCorner } from '../../border-radius-control-utils'
import { BorderRadiusCorners } from '../../border-radius-control-utils'
import type { EdgePosition } from '../../canvas-types'
Expand All @@ -23,6 +31,7 @@ import {
renderTestEditorWithCode,
makeTestProjectCodeWithSnippet,
getPrintedUiJsCode,
renderTestEditorWithModel,
} from '../../ui-jsx.test-utils'

describe('set border radius strategy', () => {
Expand Down Expand Up @@ -424,6 +433,333 @@ describe('set border radius 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-28 left-28 w-28 h-28 bg-black absolute ${classes}'
/>
</Scene>
</Storyboard>
)
`,
[TailwindConfigPath]: `
const TailwindConfig = { }
export default TailwindConfig
`,
'app.css': `
@tailwind base;
@tailwind components;
@tailwind utilities;`,
})

it('border radius controls show up for elements that have tailwind border radius set', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await selectComponentsForTest(editor, [fromString('sb/scene/mydiv')])

const borderRadiusControls = BorderRadiusCorners.flatMap((corner) =>
editor.renderedDOM.queryAllByTestId(CircularHandleTestId(corner)),
)

expect(borderRadiusControls.length).toEqual(4)
})

it('border radius controls show up for elements that dont have tailwind border radius set', async () => {
const editor = await renderTestEditorWithModel(TailwindProject(''), 'await-first-dom-report')
await selectComponentsForTest(editor, [fromString('sb/scene/mydiv')])

const borderRadiusControls = BorderRadiusCorners.flatMap((corner) =>
editor.renderedDOM.queryAllByTestId(CircularHandleTestId(corner)),
)

expect(borderRadiusControls.length).toEqual(4)
})

describe('adjust border radius via handles', () => {
it('top left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
)
})

it('top right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tr', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
)
})

it('bottom left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
)
})

it('bottom right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[22px] overflow-hidden',
)
})
})
describe('adjust border radius via handles with non-arbitrary tailwind classes', () => {
it('top left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
)
})

it('top right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'tr', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
)
})

it('bottom left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
)
})

it('bottom right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-[2rem] overflow-hidden',
)
})
})
describe('adjust border radius via handles, individually', () => {
it('top left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[22px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px] overflow-hidden',
)
})

it('top right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tr', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[10px] rounded-tr-[22px] rounded-br-[10px] rounded-bl-[10px] overflow-hidden',
)
})

it('bottom left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'bl', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[10px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[22px] overflow-hidden',
)
})

it('bottom right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'br', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[10px] rounded-tr-[10px] rounded-br-[22px] rounded-bl-[10px] overflow-hidden',
)
})
})
describe('adjust border radius via handles, individually, with non-arbitrary tailwind classes', () => {
it('top left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[2rem] rounded-tr-2xl rounded-br-2xl rounded-bl-2xl overflow-hidden',
)
})

it('top right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'tr', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-2xl rounded-tr-[2rem] rounded-br-2xl rounded-bl-2xl overflow-hidden',
)
})

it('bottom left', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'bl', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-2xl rounded-tr-2xl rounded-br-2xl rounded-bl-[2rem] overflow-hidden',
)
})

it('bottom right', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-2xl'),
'await-first-dom-report',
)
await doDragTest(editor, 'br', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-2xl rounded-tr-2xl rounded-br-[2rem] rounded-bl-2xl overflow-hidden',
)
})
})

describe('Overflow property handling', () => {
it('does not overwrite existing overflow property', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px] overflow-visible'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, cmdModifier)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute overflow-visible rounded-tl-[22px] rounded-tr-[10px] rounded-br-[10px] rounded-bl-[10px]',
)
})

it('shows toast message', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject('rounded-[10px]'),
'await-first-dom-report',
)
await doDragTest(editor, 'tl', 10, cmdModifier)
expect(editor.getEditorState().editor.toasts).toHaveLength(1)
expect(editor.getEditorState().editor.toasts[0]).toEqual({
id: 'property-added',
level: 'NOTICE',
message: 'Element now hides overflowing content',
persistent: false,
})
})
})

it('can handle 4-value syntax', async () => {
const editor = await renderTestEditorWithModel(
TailwindProject(
'rounded-tl-[14px] rounded-tr-[15px] rounded-br-[16px] rounded-bl-[17px] overflow-visible',
),
'await-first-dom-report',
)

await doDragTest(editor, 'tl', 10, emptyModifiers)
await editor.getDispatchFollowUpActionsFinished()
const div = editor.renderedDOM.getByTestId('mydiv')
expect(div.className).toEqual(
'top-28 left-28 w-28 h-28 bg-black absolute rounded-tl-[24px] rounded-tr-[15px] rounded-br-[16px] rounded-bl-[17px] overflow-visible',
)
})
})
})

function codeForDragTest(borderRadius: string): string {
Expand Down
Loading

0 comments on commit 7db4069

Please sign in to comment.