From 7db4069104e2832dae63cbe56690fcb3efce0ee6 Mon Sep 17 00:00:00 2001 From: Balint Gabor <127662+gbalint@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:18:27 +0100 Subject: [PATCH] Tailwind border-radius support (#6657) **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 --- ...t-border-radius-strategy.spec.browser2.tsx | 336 ++++++++++++++++++ .../strategies/set-border-radius-strategy.tsx | 96 ++--- editor/src/components/canvas/canvas-types.ts | 24 +- .../select-mode/border-radius-control.tsx | 15 +- .../canvas/plugins/inline-style-plugin.ts | 19 + .../canvas/plugins/style-plugins.ts | 28 +- .../canvas/plugins/tailwind-style-plugin.ts | 33 +- .../editor/store/store-hook-substore-types.ts | 12 + .../src/components/editor/store/store-hook.ts | 5 + .../components/inspector/common/css-utils.ts | 2 +- editor/src/core/layout/layout-helpers-new.ts | 1 + .../src/core/tailwind/tailwind-compilation.ts | 27 +- 12 files changed, 514 insertions(+), 84 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx index ac49bea9ee31..8e98bcb3f3b2 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.spec.browser2.tsx @@ -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' @@ -23,6 +31,7 @@ import { renderTestEditorWithCode, makeTestProjectCodeWithSnippet, getPrintedUiJsCode, + renderTestEditorWithModel, } from '../../ui-jsx.test-utils' describe('set border radius strategy', () => { @@ -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 = ( + + +
+ + + ) + + `, + [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 { diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx index c2e84920c45c..9d9e18ff297a 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx @@ -1,18 +1,13 @@ import { styleStringInArray } from '../../../../utils/common-constants' import type { Sides } from 'utopia-api/core' -import { getLayoutProperty } from '../../../../core/layout/getLayoutProperty' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' -import { defaultEither, foldEither, right } from '../../../../core/shared/either' -import type { - ElementInstanceMetadata, - JSXAttributes, -} from '../../../../core/shared/element-template' +import { foldEither } from '../../../../core/shared/either' +import type { ElementInstanceMetadata } from '../../../../core/shared/element-template' import { isIntrinsicElement, isJSXElement, jsxElementName, jsxElementNameEquals, - modifiableAttributeIsAttributeNotFound, } from '../../../../core/shared/element-template' import type { CanvasPoint, CanvasVector, Size } from '../../../../core/shared/math-utils' import { @@ -32,7 +27,7 @@ import type { CSSNumber, ParsedCSSPropertiesKeys, } from '../../../inspector/common/css-utils' -import { cssNumber, ParsedCSSProperties, printCSSNumber } from '../../../inspector/common/css-utils' +import { cssNumber, printCSSNumber } from '../../../inspector/common/css-utils' import { stylePropPathMappingFn } from '../../../inspector/common/property-path-hooks' import type { BorderRadiusAdjustMode, @@ -43,7 +38,7 @@ import { BorderRadiusControlMinimumForDisplay, maxBorderRadius, } from '../../border-radius-control-utils' -import { CSSCursor } from '../../canvas-types' +import { CSSCursor, maybePropertyValue, type StyleInfo } from '../../canvas-types' import type { CanvasCommand } from '../../commands/commands' import { setCursorCommand } from '../../commands/set-cursor-command' @@ -58,7 +53,6 @@ import { measurementBasedOnOtherMeasurement, precisionFromModifiers, shouldShowControls, - unitlessCSSNumberWithRenderedValue, } from '../../controls/select-mode/controls-common' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' @@ -70,11 +64,6 @@ import { } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' import { deleteProperties } from '../../commands/delete-properties-command' -import { allElemsEqual } from '../../../../core/shared/array-utils' -import * as PP from '../../../../core/shared/property-path' -import { withUnderlyingTarget } from '../../../editor/store/editor-state' -import type { ProjectContentTreeRoot } from '../../../assets' -import { getModifiableJSXAttributeAtPath } from '../../../../core/shared/jsx-attribute-utils' import { showToastCommand } from '../../commands/show-toast-command' import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' @@ -112,7 +101,10 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = ( return null } - const borderRadius = borderRadiusFromElement(element) + const borderRadius = borderRadiusFromElement( + element, + canvasState.styleInfoReader(selectedElement), + ) if (borderRadius == null) { return null } @@ -159,7 +151,10 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = ( [ setCursorCommand(CSSCursor.Radius), ...commands(selectedElement), - ...getAddOverflowHiddenCommands(selectedElement, canvasState.projectContents), + ...getAddOverflowHiddenCommands( + selectedElement, + canvasState.styleInfoReader(selectedElement), + ), setActiveFrames( selectedElements.map((path) => ({ action: 'set-radius', @@ -222,6 +217,7 @@ interface BorderRadiusData { export function borderRadiusFromElement( element: ElementInstanceMetadata, + styleInfo: StyleInfo | null, ): BorderRadiusData | null { return foldEither( () => null, @@ -232,7 +228,7 @@ export function borderRadiusFromElement( return null } - const fromProps = borderRadiusFromProps(jsxElement.props) + const fromStyleInfo = borderRadiusFromStyleInfo(styleInfo) const measurementsNonZero = AllSides.some((c) => { const measurement = renderedValueSides[c] if (measurement == null) { @@ -250,7 +246,7 @@ export function borderRadiusFromElement( if ( !( elementIsIntrinsicElementOrScene || - shouldShowControls(fromProps != null, measurementsNonZero) + shouldShowControls(fromStyleInfo != null, measurementsNonZero) ) ) { return null @@ -258,7 +254,7 @@ export function borderRadiusFromElement( const borderRadius = optionalMap( (radius) => measurementFromBorderRadius(renderedValueSides, radius), - fromProps, + fromStyleInfo, ) const defaultBorderRadiusSides = borderRadiusSidesFromValue( @@ -281,7 +277,7 @@ export function borderRadiusFromElement( const borderRadiusMinMax = { min: 0, max: borderRadiusUpperLimit } return { - mode: fromProps?.type === 'sides' ? 'individual' : 'all', + mode: fromStyleInfo?.type === 'sides' ? 'individual' : 'all', borderRadius: mapBorderRadiusSides( (n) => adjustBorderRadius(borderRadiusMinMax, n), borderRadius ?? defaultBorderRadiusSides, @@ -300,36 +296,20 @@ interface BorderRadiusFromProps { sides: BorderRadiusSides } -function borderRadiusFromProps(props: JSXAttributes): BorderRadiusFromProps | null { - const wrappedProps = right(props) +function borderRadiusFromStyleInfo(styleInfo: StyleInfo | null): BorderRadiusFromProps | null { + const borderRadius = optionalMap(maybePropertyValue, styleInfo?.borderRadius) - const borderRadius = getLayoutProperty('borderRadius', wrappedProps, styleStringInArray) - const simpleBorderRadius = foldEither( - () => null, - (radius) => { - if (radius == null) { - return null - } else { - return foldEither(borderRadiusSidesFromValue, (value) => value, radius) - } - }, + const simpleBorderRadius = optionalMap( + (radius) => foldEither(borderRadiusSidesFromValue, (value) => value, radius), borderRadius, ) - const borderTopLeftRadius = defaultEither( - null, - getLayoutProperty('borderTopLeftRadius', wrappedProps, styleStringInArray), - ) - const borderTopRightRadius = defaultEither( - null, - getLayoutProperty('borderTopRightRadius', wrappedProps, styleStringInArray), - ) - const borderBottomLeftRadius = defaultEither( - null, - getLayoutProperty('borderBottomLeftRadius', wrappedProps, styleStringInArray), - ) - const borderBottomRightRadius = defaultEither( - null, - getLayoutProperty('borderBottomRightRadius', wrappedProps, styleStringInArray), + + const borderTopLeftRadius = optionalMap(maybePropertyValue, styleInfo?.borderTopLeftRadius) + const borderTopRightRadius = optionalMap(maybePropertyValue, styleInfo?.borderTopRightRadius) + const borderBottomLeftRadius = optionalMap(maybePropertyValue, styleInfo?.borderBottomLeftRadius) + const borderBottomRightRadius = optionalMap( + maybePropertyValue, + styleInfo?.borderBottomRightRadius, ) if ( @@ -606,27 +586,15 @@ const setShorthandStylePropertyCommand = function getAddOverflowHiddenCommands( target: ElementPath, - projectContents: ProjectContentTreeRoot, + styleInfo: StyleInfo | null, ): Array { - const overflowProp = PP.create('style', 'overflow') - - const propertyExists = withUnderlyingTarget(target, projectContents, false, (_, element) => { - if (isJSXElement(element)) { - return foldEither( - () => false, - (value) => !modifiableAttributeIsAttributeNotFound(value), - getModifiableJSXAttributeAtPath(element.props, overflowProp), - ) - } else { - return false - } - }) - + const propertyExists = styleInfo?.overflow != null && styleInfo.overflow.type !== 'not-found' if (propertyExists) { return [] } + return [ showToastCommand('Element now hides overflowing content', 'NOTICE', 'property-added'), - setProperty('always', target, overflowProp, 'hidden'), + setProperty('always', target, StyleProp('overflow'), 'hidden'), ] } diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 218f168e7dec..d0e222a88ac3 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -27,7 +27,14 @@ 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, CSSPadding, FlexDirection } from '../inspector/common/css-utils' +import type { + CSSBorderRadius, + CSSBorderRadiusIndividual, + CSSNumber, + CSSOverflow, + CSSPadding, + FlexDirection, +} from '../inspector/common/css-utils' export const CanvasContainerID = 'canvas-container' @@ -594,7 +601,10 @@ export type HeightInfo = CSSStyleProperty export type FlexBasisInfo = CSSStyleProperty export type PaddingInfo = CSSStyleProperty export type PaddingSideInfo = CSSStyleProperty +export type BorderRadiusInfo = CSSStyleProperty +export type BorderRadiusCornerInfo = CSSStyleProperty export type ZIndexInfo = CSSStyleProperty +export type OverflowInfo = CSSStyleProperty export interface StyleInfo { gap: FlexGapInfo | null @@ -611,7 +621,13 @@ export interface StyleInfo { paddingRight: PaddingSideInfo | null paddingBottom: PaddingSideInfo | null paddingLeft: PaddingSideInfo | null + borderRadius: BorderRadiusInfo | null + borderTopLeftRadius: BorderRadiusCornerInfo | null + borderTopRightRadius: BorderRadiusCornerInfo | null + borderBottomRightRadius: BorderRadiusCornerInfo | null + borderBottomLeftRadius: BorderRadiusCornerInfo | null zIndex: ZIndexInfo | null + overflow: OverflowInfo | null } const emptyStyleInfo: StyleInfo = { @@ -629,7 +645,13 @@ const emptyStyleInfo: StyleInfo = { paddingRight: null, paddingBottom: null, paddingLeft: null, + borderRadius: null, + borderTopLeftRadius: null, + borderTopRightRadius: null, + borderBottomRightRadius: null, + borderBottomLeftRadius: null, zIndex: null, + overflow: null, } export const isStyleInfoKey = (key: string): key is keyof StyleInfo => key in emptyStyleInfo diff --git a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx index f1b733c9811e..eb0d98c4d493 100644 --- a/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/border-radius-control.tsx @@ -1,6 +1,5 @@ import createCachedSelector from 're-reselect' import React from 'react' -import { createSelector } from 'reselect' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import * as EP from '../../../../core/shared/element-path' import type { CanvasVector, Size } from '../../../../core/shared/math-utils' @@ -15,6 +14,7 @@ import { Substores, useEditorState, useRefEditorState } from '../../../editor/st import type { CanvasSubstate, MetadataSubstate, + StyleInfoSubstate, } from '../../../editor/store/store-hook-substore-types' import { printCSSNumber } from '../../../inspector/common/css-utils' import { metadataSelector } from '../../../inspector/inpector-selectors' @@ -24,7 +24,6 @@ import { BorderRadiusCorners, BorderRadiusHandleHitArea, BorderRadiusHandleSize, - BorderRadiusSides, handlePosition, } from '../../border-radius-control-utils' import CanvasActions from '../../canvas-actions' @@ -41,6 +40,8 @@ import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' import { isZeroSizedElement } from '../outline-utils' import type { CSSNumberWithRenderedValue } from './controls-common' import { CanvasLabel, fallbackEmptyValue } from './controls-common' +import { getActivePlugin } from '../../plugins/style-plugins' +import type { EditorStorePatched } from '../../../editor/store/editor-state' export const CircularHandleTestId = (corner: BorderRadiusCorner): string => `circular-handle-${corner}` @@ -59,13 +60,17 @@ const isDraggingSelector = (store: CanvasSubstate): boolean => { const borderRadiusSelector = createCachedSelector( metadataSelector, + (store: StyleInfoSubstate) => + getActivePlugin(store.editor).styleInfoFactory({ + projectContents: store.editor.projectContents, + }), (_: MetadataSubstate, x: ElementPath) => x, - (metadata, selectedElement) => { + (metadata, styleInfoReader, selectedElement) => { const element = MetadataUtils.findElementByElementPath(metadata, selectedElement) if (element == null) { return null } - return borderRadiusFromElement(element) + return borderRadiusFromElement(element, styleInfoReader(selectedElement)) }, )((_, x) => EP.toString(x)) @@ -117,7 +122,7 @@ export const BorderRadiusControl = controlForStrategyMemoized borderRadiusSelector(store, selectedElement), 'BorderRadiusControl borderRadius', ) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 226ff8a45401..a3d0c7c3376b 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -78,8 +78,21 @@ export const InlineStylePlugin: StylePlugin = { const paddingBottom = getPropertyFromInstance('paddingBottom', element.props) const paddingLeft = getPropertyFromInstance('paddingLeft', element.props) const paddingRight = getPropertyFromInstance('paddingRight', element.props) + const borderRadius = getPropertyFromInstance('borderRadius', element.props) + const borderTopLeftRadius = getPropertyFromInstance('borderTopLeftRadius', element.props) + const borderTopRightRadius = getPropertyFromInstance('borderTopRightRadius', element.props) + const borderBottomRightRadius = getPropertyFromInstance( + 'borderBottomRightRadius', + element.props, + ) + const borderBottomLeftRadius = getPropertyFromInstance( + 'borderBottomLeftRadius', + element.props, + ) const zIndex = getPropertyFromInstance('zIndex', element.props) + const overflow = getPropertyFromInstance('overflow', element.props) + return { gap: gap, flexDirection: flexDirection, @@ -95,7 +108,13 @@ export const InlineStylePlugin: StylePlugin = { paddingBottom: paddingBottom, paddingLeft: paddingLeft, paddingRight: paddingRight, + borderRadius: borderRadius, + borderTopLeftRadius: borderTopLeftRadius, + borderTopRightRadius: borderTopRightRadius, + borderBottomRightRadius: borderBottomRightRadius, + borderBottomLeftRadius: borderBottomLeftRadius, zIndex: zIndex, + overflow: overflow, } }, updateStyles: (editorState, elementPath, updates) => { diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index c23215963f48..42ecae83b215 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -20,6 +20,7 @@ import * as PP from '../../../core/shared/property-path' import { emptyComments, jsExpressionValue } from '../../../core/shared/element-template' import type { CSSStyleProperty } from '../canvas-types' import { isStyleInfoKey, type StyleInfo } from '../canvas-types' +import type { StyleInfoSubEditorState } from '../../editor/store/store-hook-substore-types' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' export interface UpdateCSSProp { @@ -64,7 +65,7 @@ export interface StylePlugin { ) => EditorStateWithPatch } -export function getActivePlugin(editorState: EditorState): StylePlugin { +export function getActivePlugin(editorState: StyleInfoSubEditorState): StylePlugin { if (isFeatureEnabled('Tailwind') && isTailwindEnabled()) { return TailwindPlugin(getTailwindConfigCached(editorState)) } @@ -177,6 +178,13 @@ const genericPropPatcher = const PaddingLonghands = ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'] +const BorderRadiusLonghands = [ + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', +] + const patchers: PropPatcher[] = [ { matches: (p) => p === 'gap', patch: genericPropPatcher('0px') }, { @@ -193,7 +201,25 @@ const patchers: PropPatcher[] = [ ) }, }, + { + matches: (p) => p === 'borderRadius', + patch: (_, styleInfo, updatedProperties) => { + const propIsSetOnElement = styleInfo?.padding != null + + if (!propIsSetOnElement) { + return [] + } + + return BorderRadiusLonghands.filter( + (p) => !updatedProperties.propertiesUpdated.includes(p), + ).map((p) => makeZeroProp(p)) + }, + }, { matches: (p) => PaddingLonghands.includes(p), patch: genericPropPatcher('0px') }, + { + matches: (p) => BorderRadiusLonghands.includes(p), + patch: genericPropPatcher('0px'), + }, ] function getPropertiesToZero( diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 768f66dd6207..dd9659424cfa 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -2,13 +2,13 @@ import * as TailwindClassParser from '@xengine/tailwindcss-class-parser' import { defaultEither, flatMapEither, isLeft } from '../../../core/shared/either' import { getClassNameAttribute } from '../../../core/tailwind/tailwind-options' import { getElementFromProjectContents } from '../../editor/store/editor-state' -import type { ParsedCSSProperties, Parser } from '../../inspector/common/css-utils' +import type { ParsedCSSProperties } from '../../inspector/common/css-utils' import { cssParsers } from '../../inspector/common/css-utils' import { mapDropNulls } from '../../../core/shared/array-utils' import type { StylePlugin } from './style-plugins' import type { Config } from 'tailwindcss/types/config' import type { StyleInfo } from '../canvas-types' -import { cssStyleProperty, isStyleInfoKey, type CSSStyleProperty } from '../canvas-types' +import { cssStyleProperty, type CSSStyleProperty } from '../canvas-types' import * as UCL from './tailwind-style-plugin-utils/update-class-list' import { assertNever } from '../../../core/shared/utils' import { @@ -45,6 +45,12 @@ const TailwindPropertyMapping: Record = { paddingBottom: 'paddingBottom', paddingLeft: 'paddingLeft', + borderRadius: 'borderRadius', + borderTopLeftRadius: 'borderTopLeftRadius', + borderTopRightRadius: 'borderTopRightRadius', + borderBottomRightRadius: 'borderBottomRightRadius', + borderBottomLeftRadius: 'borderBottomLeftRadius', + justifyContent: 'justifyContent', alignItems: 'alignItems', flex: 'flex', @@ -55,6 +61,8 @@ const TailwindPropertyMapping: Record = { flexWrap: 'flexWrap', gap: 'gap', + overflow: 'overflow', + zIndex: 'zIndex', } @@ -154,7 +162,28 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ mapping[TailwindPropertyMapping.paddingLeft], 'paddingLeft', ), + borderRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderRadius], + 'borderRadius', + ), + borderTopLeftRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderTopLeftRadius], + 'borderTopLeftRadius', + ), + borderTopRightRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderTopRightRadius], + 'borderTopRightRadius', + ), + borderBottomRightRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderBottomRightRadius], + 'borderBottomRightRadius', + ), + borderBottomLeftRadius: parseTailwindProperty( + mapping[TailwindPropertyMapping.borderBottomLeftRadius], + 'borderBottomLeftRadius', + ), zIndex: parseTailwindProperty(mapping[TailwindPropertyMapping.zIndex], 'zIndex'), + overflow: parseTailwindProperty(mapping[TailwindPropertyMapping.overflow], 'overflow'), } }, updateStyles: (editorState, elementPath, updates) => { diff --git a/editor/src/components/editor/store/store-hook-substore-types.ts b/editor/src/components/editor/store/store-hook-substore-types.ts index 178ab22a95d2..eea6a848a3f9 100644 --- a/editor/src/components/editor/store/store-hook-substore-types.ts +++ b/editor/src/components/editor/store/store-hook-substore-types.ts @@ -158,6 +158,18 @@ const propertyControlsInfoSubstate = { } as const export type PropertyControlsInfoSubstate = typeof propertyControlsInfoSubstate +// StyleInfoSubstate +export const styleInfoSubstateKeys = [ + ...metadataSubstateKeys, + ...projectContentsKeys, + 'codeResultCache', +] as const +const emptyStyleInfoSubstate = { + editor: pick(styleInfoSubstateKeys, EmptyEditorStateForKeysOnly), +} as const +export type StyleInfoSubstate = typeof emptyStyleInfoSubstate +export type StyleInfoSubEditorState = StyleInfoSubstate['editor'] + export type MetadataAndPropertyControlsInfoSubstate = MetadataSubstate & PropertyControlsInfoSubstate diff --git a/editor/src/components/editor/store/store-hook.ts b/editor/src/components/editor/store/store-hook.ts index 9ff6287b8a82..ea0824397cfc 100644 --- a/editor/src/components/editor/store/store-hook.ts +++ b/editor/src/components/editor/store/store-hook.ts @@ -40,6 +40,7 @@ import type { RestOfEditorState, SelectedViewsSubstate, StoreKey, + StyleInfoSubstate, Substates, ThemeSubstate, UserStateSubstate, @@ -58,6 +59,7 @@ import { restOfEditorStateKeys, restOfStoreKeys, selectedViewsSubstateKeys, + styleInfoSubstateKeys, variablesInScopeSubstateKeys, } from './store-hook-substore-types' import { Getter } from '../hook-utils' @@ -355,6 +357,9 @@ export const Substores = { b.editor, ) }, + styleInfo: (a: StyleInfoSubstate, b: StyleInfoSubstate) => { + return keysEquality(styleInfoSubstateKeys, a.editor, b.editor) + }, } as const export const SubstateEqualityFns: { diff --git a/editor/src/components/inspector/common/css-utils.ts b/editor/src/components/inspector/common/css-utils.ts index 4a381243fdc5..6147a5ce6441 100644 --- a/editor/src/components/inspector/common/css-utils.ts +++ b/editor/src/components/inspector/common/css-utils.ts @@ -2483,7 +2483,7 @@ function printTransformOrigin(transformOrigin: CSSTransformOrigin): JSExpression ) } -type CSSOverflow = boolean +export type CSSOverflow = boolean function parseOverflow(overflow: unknown): Either { if (typeof overflow === 'string') { diff --git a/editor/src/core/layout/layout-helpers-new.ts b/editor/src/core/layout/layout-helpers-new.ts index c3329258a2e6..a43d7eee3587 100644 --- a/editor/src/core/layout/layout-helpers-new.ts +++ b/editor/src/core/layout/layout-helpers-new.ts @@ -109,6 +109,7 @@ export type StyleLayoutProp = | 'zIndex' | 'rowGap' | 'columnGap' + | 'overflow' export function framePointForPinnedProp(pinnedProp: LayoutPinnedProp): FramePoint { switch (pinnedProp) { diff --git a/editor/src/core/tailwind/tailwind-compilation.ts b/editor/src/core/tailwind/tailwind-compilation.ts index b71edcc1b587..8a61463711ab 100644 --- a/editor/src/core/tailwind/tailwind-compilation.ts +++ b/editor/src/core/tailwind/tailwind-compilation.ts @@ -13,15 +13,16 @@ import { importDefault } from '../es-modules/commonjs-interop' import { rescopeCSSToTargetCanvasOnly } from '../shared/css-utils' import type { RequireFn } from '../shared/npm-dependency-types' import { TailwindConfigPath } from './tailwind-config' -import { ElementsToRerenderGLOBAL } from '../../components/canvas/ui-jsx-canvas' import { isFeatureEnabled } from '../../utils/feature-switches' import type { Config } from 'tailwindcss/types/config' -import type { EditorState } from '../../components/editor/store/editor-state' import { createSelector } from 'reselect' -import type { ProjectContentSubstate } from '../../components/editor/store/store-hook-substore-types' +import type { + ProjectContentSubstate, + StyleInfoSubEditorState, +} from '../../components/editor/store/store-hook-substore-types' const LatestConfig: { current: { code: string; config: Config } | null } = { current: null } -export function getTailwindConfigCached(editorState: EditorState): Config | null { +export function getTailwindConfigCached(editorState: StyleInfoSubEditorState): Config | null { const tailwindConfig = getProjectFileByFilePath(editorState.projectContents, TailwindConfigPath) if (tailwindConfig == null || tailwindConfig.type !== 'TEXT_FILE') { return null @@ -59,6 +60,8 @@ function ensureElementExists({ type, id }: { type: string; id: string }) { return tag } +const TailwindStylesElementID = 'utopia-tailwind-jit-styles' + async function generateTailwindStyles(tailwindCss: Tailwindcss, allCSSFiles: string) { const contentElement = document.getElementById(CanvasContainerID) if (contentElement == null) { @@ -66,10 +69,17 @@ async function generateTailwindStyles(tailwindCss: Tailwindcss, allCSSFiles: str } const content = contentElement.outerHTML const styleString = await tailwindCss.generateStylesFromContent(allCSSFiles, [content]) - const style = ensureElementExists({ type: 'style', id: 'utopia-tailwind-jit-styles' }) + const style = ensureElementExists({ type: 'style', id: TailwindStylesElementID }) style.textContent = rescopeCSSToTargetCanvasOnly(styleString) } +function removeTailwindStyles() { + const style = document.getElementById(TailwindStylesElementID) + if (style != null) { + style.remove() + } +} + function getCssFilesFromProjectContents(projectContents: ProjectContentTreeRoot) { let files: string[] = [] walkContentsTree(projectContents, (path, file) => { @@ -99,11 +109,7 @@ function runTailwindClassGenerationOnDOMMutation( m.addedNodes.length > 0 || // new DOM element was added with potentially new classes m.attributeName === 'class', // potentially new classes were added to the class attribute of an element ) - if ( - !updateHasNewTailwindData || - isInteractionActive || - ElementsToRerenderGLOBAL.current !== 'rerender-all-elements' // implies that an interaction is in progress) - ) { + if (!updateHasNewTailwindData || isInteractionActive) { return } generateTailwindClasses(projectContents, requireFn) @@ -161,6 +167,7 @@ export const useTailwindCompilation = () => { generateTailwindClasses(projectContentsRef.current, requireFnRef.current) return () => { + removeTailwindStyles() observer.disconnect() } }, [isInteractionActiveRef, projectContentsRef, requireFnRef, tailwindConfig])