@elementor/editor-canvas 0.26.0 → 3.32.0-20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +170 -31
  3. package/dist/index.mjs +166 -27
  4. package/package.json +15 -15
  5. package/src/__tests__/flex-transformer.test.ts +272 -0
  6. package/src/__tests__/styles-prop-resolver.test.ts +89 -14
  7. package/src/components/__tests__/style-renderer.test.tsx +2 -2
  8. package/src/components/style-renderer.tsx +1 -1
  9. package/src/hooks/__tests__/use-style-items.test.ts +106 -7
  10. package/src/hooks/use-style-items.ts +47 -5
  11. package/src/init-settings-transformers.ts +2 -0
  12. package/src/init-style-transformers.ts +11 -0
  13. package/src/renderers/__tests__/__snapshots__/create-styles-renderer.test.ts.snap +2 -0
  14. package/src/renderers/__tests__/create-styles-renderer.test.ts +98 -0
  15. package/src/renderers/create-styles-renderer.ts +26 -2
  16. package/src/style-commands/__tests__/paste-style.test.ts +30 -0
  17. package/src/style-commands/__tests__/reset-style.test.ts +4 -0
  18. package/src/style-commands/undoable-actions/paste-element-style.ts +2 -1
  19. package/src/transformers/settings/attributes-transformer.ts +25 -0
  20. package/src/transformers/styles/background-overlay-transformer.ts +1 -10
  21. package/src/transformers/styles/create-combine-array-transformer.ts +3 -1
  22. package/src/transformers/styles/filter-transformer.ts +13 -10
  23. package/src/transformers/styles/flex-transformer.ts +55 -0
  24. package/src/transformers/styles/transform-move-transformer.ts +3 -1
  25. package/src/transformers/styles/transform-rotate-transformer.ts +19 -0
  26. package/src/transformers/styles/transform-scale-transformer.ts +11 -0
  27. package/src/transformers/styles/transform-skew-transformer.ts +15 -0
  28. package/src/transformers/styles/transition-transformer.ts +25 -0
  29. package/.turbo/turbo-build.log +0 -22
  30. package/dist/index.js.map +0 -1
  31. package/dist/index.mjs.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { mockStylesSchema } from 'test-utils';
2
2
  import { styleTransformersRegistry } from '@elementor/editor-canvas';
3
3
  import {
4
+ backdropFilterPropTypeUtil,
4
5
  backgroundColorOverlayPropTypeUtil,
5
6
  backgroundGradientOverlayPropTypeUtil,
6
7
  backgroundImageOverlayPropTypeUtil,
@@ -8,13 +9,12 @@ import {
8
9
  backgroundImageSizeScalePropTypeUtil,
9
10
  backgroundOverlayPropTypeUtil,
10
11
  backgroundPropTypeUtil,
11
- blurFilterPropTypeUtil,
12
12
  borderRadiusPropTypeUtil,
13
13
  borderWidthPropTypeUtil,
14
14
  boxShadowPropTypeUtil,
15
- brightnessFilterPropTypeUtil,
16
15
  colorPropTypeUtil,
17
16
  colorStopPropTypeUtil,
17
+ cssFilterFunctionPropUtil,
18
18
  dimensionsPropTypeUtil,
19
19
  filterPropTypeUtil,
20
20
  gradientColorStopPropTypeUtil,
@@ -29,7 +29,6 @@ import {
29
29
  stringPropTypeUtil,
30
30
  strokePropTypeUtil,
31
31
  } from '@elementor/editor-props';
32
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
33
32
  import { getMediaAttachment } from '@elementor/wp-media';
34
33
 
35
34
  import { initStyleTransformers } from '../init-style-transformers';
@@ -48,6 +47,76 @@ type Payload = {
48
47
  expected: Record< string, unknown >;
49
48
  };
50
49
 
50
+ const filters = filterPropTypeUtil.create( [
51
+ cssFilterFunctionPropUtil.create( {
52
+ func: stringPropTypeUtil.create( 'blur' ),
53
+ args: sizePropTypeUtil.create( { size: 1, unit: 'px' } ),
54
+ } ),
55
+ cssFilterFunctionPropUtil.create( {
56
+ func: stringPropTypeUtil.create( 'brightness' ),
57
+ args: sizePropTypeUtil.create( { size: 90, unit: '%' } ),
58
+ } ),
59
+ cssFilterFunctionPropUtil.create( {
60
+ func: stringPropTypeUtil.create( 'contrast' ),
61
+ args: sizePropTypeUtil.create( { size: 50, unit: '%' } ),
62
+ } ),
63
+ cssFilterFunctionPropUtil.create( {
64
+ func: stringPropTypeUtil.create( 'grayscale' ),
65
+ args: sizePropTypeUtil.create( { size: 70, unit: '%' } ),
66
+ } ),
67
+ cssFilterFunctionPropUtil.create( {
68
+ func: stringPropTypeUtil.create( 'invert' ),
69
+ args: sizePropTypeUtil.create( { size: 60, unit: '%' } ),
70
+ } ),
71
+ cssFilterFunctionPropUtil.create( {
72
+ func: stringPropTypeUtil.create( 'sepia' ),
73
+ args: sizePropTypeUtil.create( { size: 30, unit: '%' } ),
74
+ } ),
75
+ cssFilterFunctionPropUtil.create( {
76
+ func: stringPropTypeUtil.create( 'saturate' ),
77
+ args: sizePropTypeUtil.create( { size: 25, unit: '%' } ),
78
+ } ),
79
+ cssFilterFunctionPropUtil.create( {
80
+ func: stringPropTypeUtil.create( 'hue-rotate' ),
81
+ args: sizePropTypeUtil.create( { size: 10, unit: 'deg' } ),
82
+ } ),
83
+ ] );
84
+
85
+ const backDropFilters = backdropFilterPropTypeUtil.create( [
86
+ cssFilterFunctionPropUtil.create( {
87
+ func: stringPropTypeUtil.create( 'blur' ),
88
+ args: sizePropTypeUtil.create( { size: 2, unit: 'rem' } ),
89
+ } ),
90
+ cssFilterFunctionPropUtil.create( {
91
+ func: stringPropTypeUtil.create( 'brightness' ),
92
+ args: sizePropTypeUtil.create( { size: 80, unit: '%' } ),
93
+ } ),
94
+ cssFilterFunctionPropUtil.create( {
95
+ func: stringPropTypeUtil.create( 'contrast' ),
96
+ args: sizePropTypeUtil.create( { size: 50, unit: '%' } ),
97
+ } ),
98
+ cssFilterFunctionPropUtil.create( {
99
+ func: stringPropTypeUtil.create( 'grayscale' ),
100
+ args: sizePropTypeUtil.create( { size: 70, unit: '%' } ),
101
+ } ),
102
+ cssFilterFunctionPropUtil.create( {
103
+ func: stringPropTypeUtil.create( 'invert' ),
104
+ args: sizePropTypeUtil.create( { size: 60, unit: '%' } ),
105
+ } ),
106
+ cssFilterFunctionPropUtil.create( {
107
+ func: stringPropTypeUtil.create( 'sepia' ),
108
+ args: sizePropTypeUtil.create( { size: 30, unit: '%' } ),
109
+ } ),
110
+ cssFilterFunctionPropUtil.create( {
111
+ func: stringPropTypeUtil.create( 'saturate' ),
112
+ args: sizePropTypeUtil.create( { size: 25, unit: '%' } ),
113
+ } ),
114
+ cssFilterFunctionPropUtil.create( {
115
+ func: stringPropTypeUtil.create( 'hue-rotate' ),
116
+ args: sizePropTypeUtil.create( { size: 10, unit: 'deg' } ),
117
+ } ),
118
+ ] );
119
+
51
120
  describe( 'styles prop resolver', () => {
52
121
  it.each< Payload >( [
53
122
  {
@@ -177,7 +246,7 @@ describe( 'styles prop resolver', () => {
177
246
  },
178
247
  },
179
248
  {
180
- name: 'background (only image url)',
249
+ name: 'background (image url and default values)',
181
250
  props: {
182
251
  background: backgroundPropTypeUtil.create( {
183
252
  'background-overlay': backgroundOverlayPropTypeUtil.create( [
@@ -195,12 +264,15 @@ describe( 'styles prop resolver', () => {
195
264
  },
196
265
  expected: {
197
266
  'background-image': 'url(https://localhost.test/test-image.png)',
267
+ 'background-repeat': 'repeat',
268
+ 'background-attachment': 'scroll',
269
+ 'background-size': 'auto auto',
270
+ 'background-position': '0% 0%',
198
271
  },
199
272
  },
200
273
  {
201
274
  name: 'background (full)',
202
275
  prepare: () => {
203
- jest.mocked( isExperimentActive ).mockReturnValue( true );
204
276
  jest.mocked( getMediaAttachment ).mockImplementation(
205
277
  ( args ) => Promise.resolve( mockAttachmentData( args.id ) ) as never
206
278
  );
@@ -273,17 +345,20 @@ describe( 'styles prop resolver', () => {
273
345
  {
274
346
  name: 'filter',
275
347
  props: {
276
- filter: filterPropTypeUtil.create( [
277
- blurFilterPropTypeUtil.create( {
278
- radius: sizePropTypeUtil.create( { size: 1, unit: 'px' } ),
279
- } ),
280
- brightnessFilterPropTypeUtil.create( {
281
- amount: sizePropTypeUtil.create( { size: 90, unit: '%' } ),
282
- } ),
283
- ] ),
348
+ filter: filters,
349
+ },
350
+ expected: {
351
+ filter: 'blur(1px) brightness(90%) contrast(50%) grayscale(70%) invert(60%) sepia(30%) saturate(25%) hue-rotate(10deg)',
352
+ },
353
+ },
354
+ {
355
+ name: 'backdrop-filter',
356
+ props: {
357
+ 'backdrop-filter': backDropFilters,
284
358
  },
285
359
  expected: {
286
- filter: 'blur(1px) brightness(90%)',
360
+ 'backdrop-filter':
361
+ 'blur(2rem) brightness(80%) contrast(50%) grayscale(70%) invert(60%) sepia(30%) saturate(25%) hue-rotate(10deg)',
287
362
  },
288
363
  },
289
364
  {
@@ -46,8 +46,8 @@ describe( '<StyleRenderer />', () => {
46
46
  const mockContainer = document.createElement( 'div' );
47
47
 
48
48
  const mockCssItems = [
49
- { id: 'style1', value: '.test { color: red; }' },
50
- { id: 'style2', value: '.test2 { color: blue; }' },
49
+ { id: 'style1', value: '.test { color: red; }', breakpoint: 'desktop' },
50
+ { id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop' },
51
51
  ];
52
52
 
53
53
  const mockLinkAttrs = [
@@ -19,7 +19,7 @@ export function StyleRenderer() {
19
19
  return (
20
20
  <Portal container={ container }>
21
21
  { styleItems.map( ( item ) => (
22
- <style data-e-style-id={ item.id } key={ item.id }>
22
+ <style data-e-style-id={ item.id } key={ `${ item.id }-${ item.breakpoint }` }>
23
23
  { item.value }
24
24
  </style>
25
25
  ) ) }
@@ -1,4 +1,5 @@
1
- import { createMockStyleDefinition, createMockStylesProvider } from 'test-utils';
1
+ import { createMockStyleDefinition, createMockStyleDefinitionWithVariants, createMockStylesProvider } from 'test-utils';
2
+ import { type StyleDefinition } from '@elementor/editor-styles';
2
3
  import { stylesRepository } from '@elementor/editor-styles-repository';
3
4
  import { registerDataHook } from '@elementor/editor-v1-adapters';
4
5
  import { act, renderHook } from '@testing-library/react';
@@ -26,12 +27,23 @@ jest.mock( '../use-style-renderer', () => ( {
26
27
  useStyleRenderer: jest.fn(),
27
28
  } ) );
28
29
 
30
+ jest.mock( '@elementor/editor-responsive', () => ( {
31
+ getBreakpoints: jest.fn().mockReturnValue( [
32
+ { id: 'desktop', label: 'Desktop' },
33
+ { id: 'tablet', label: 'Tablet' },
34
+ { id: 'mobile', label: 'Mobile' },
35
+ ] ),
36
+ } ) );
37
+
29
38
  describe( 'useStyleItems', () => {
30
39
  beforeEach( () => {
31
40
  jest.mocked( useStyleRenderer ).mockReturnValue(
32
- jest
33
- .fn()
34
- .mockImplementation( ( { styles } ) => styles.map( ( style: { id: string } ) => ( { id: style.id } ) ) )
41
+ jest.fn().mockImplementation( ( { styles } ) =>
42
+ styles.map( ( style: StyleDefinition ) => ( {
43
+ id: style.id,
44
+ breakpoint: style?.variants[ 0 ]?.meta.breakpoint || 'desktop',
45
+ } ) )
46
+ )
35
47
  );
36
48
  } );
37
49
 
@@ -71,7 +83,10 @@ describe( 'useStyleItems', () => {
71
83
  } );
72
84
 
73
85
  // Assert.
74
- expect( result.current ).toEqual( [ { id: 'style2' }, { id: 'style1' } ] );
86
+ expect( result.current ).toEqual( [
87
+ { id: 'style2', breakpoint: 'desktop' },
88
+ { id: 'style1', breakpoint: 'desktop' },
89
+ ] );
75
90
 
76
91
  // Act.
77
92
  await act( async () => {
@@ -83,7 +98,12 @@ describe( 'useStyleItems', () => {
83
98
  } );
84
99
 
85
100
  // Assert.
86
- expect( result.current ).toEqual( [ { id: 'style4' }, { id: 'style3' }, { id: 'style2' }, { id: 'style1' } ] );
101
+ expect( result.current ).toEqual( [
102
+ { id: 'style4', breakpoint: 'desktop' },
103
+ { id: 'style3', breakpoint: 'desktop' },
104
+ { id: 'style2', breakpoint: 'desktop' },
105
+ { id: 'style1', breakpoint: 'desktop' },
106
+ ] );
87
107
  } );
88
108
 
89
109
  it( 'should return style items when attach-preview command is triggered', async () => {
@@ -128,6 +148,85 @@ describe( 'useStyleItems', () => {
128
148
  } );
129
149
 
130
150
  // Assert.
131
- expect( result.current ).toEqual( [ { id: 'style4' }, { id: 'style3' }, { id: 'style2' }, { id: 'style1' } ] );
151
+ expect( result.current ).toEqual( [
152
+ { id: 'style4', breakpoint: 'desktop' },
153
+ { id: 'style3', breakpoint: 'desktop' },
154
+ { id: 'style2', breakpoint: 'desktop' },
155
+ { id: 'style1', breakpoint: 'desktop' },
156
+ ] );
157
+ } );
158
+
159
+ it( 'should return style items ordered by provider priority and breakpoint', async () => {
160
+ // Arrange.
161
+ const mockProvider1 = createMockStylesProvider(
162
+ {
163
+ key: 'provider1',
164
+ priority: 2,
165
+ },
166
+ [
167
+ createMockStyleDefinitionWithVariants( {
168
+ id: 'style1',
169
+ variants: [
170
+ {
171
+ meta: { breakpoint: 'mobile', state: null },
172
+ props: {
173
+ padding: '10px',
174
+ },
175
+ custom_css: null,
176
+ },
177
+ {
178
+ meta: { breakpoint: 'mobile', state: 'hover' },
179
+ props: {
180
+ padding: '20px',
181
+ },
182
+ custom_css: null,
183
+ },
184
+ ],
185
+ } ),
186
+ createMockStyleDefinition( { id: 'style2' } ),
187
+ ]
188
+ );
189
+
190
+ const mockProvider2 = createMockStylesProvider(
191
+ {
192
+ key: 'provider2',
193
+ priority: 1,
194
+ },
195
+ [
196
+ createMockStyleDefinition( { id: 'style3', meta: { breakpoint: 'tablet', state: null } } ),
197
+ createMockStyleDefinition( { id: 'style4' } ),
198
+ ]
199
+ );
200
+
201
+ jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ mockProvider1, mockProvider2 ] );
202
+
203
+ let attachPreviewCallback: () => Promise< void >;
204
+
205
+ jest.mocked( registerDataHook ).mockImplementation( ( position, command, callback ) => {
206
+ if ( command === 'editor/documents/attach-preview' && position === 'after' ) {
207
+ attachPreviewCallback = callback as never;
208
+ }
209
+
210
+ return null as never;
211
+ } );
212
+
213
+ // Act.
214
+ const { result } = renderHook( () => useStyleItems() );
215
+
216
+ // Assert.
217
+ expect( result.current ).toEqual( [] );
218
+
219
+ // Act.
220
+ await act( async () => {
221
+ await attachPreviewCallback?.();
222
+ } );
223
+
224
+ // Assert.
225
+ expect( result.current ).toEqual( [
226
+ { id: 'style4', breakpoint: 'desktop' },
227
+ { id: 'style2', breakpoint: 'desktop' },
228
+ { id: 'style3', breakpoint: 'tablet' },
229
+ { id: 'style1', breakpoint: 'mobile' },
230
+ ] );
132
231
  } );
133
232
  } );
@@ -1,8 +1,9 @@
1
1
  import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
2
+ import { type BreakpointId, getBreakpoints } from '@elementor/editor-responsive';
2
3
  import { type StylesProvider, stylesRepository } from '@elementor/editor-styles-repository';
3
4
  import { registerDataHook } from '@elementor/editor-v1-adapters';
4
5
 
5
- import { type StyleItem, type StyleRenderer } from '../renderers/create-styles-renderer';
6
+ import { type RendererStyleDefinition, type StyleItem, type StyleRenderer } from '../renderers/create-styles-renderer';
6
7
  import { abortPreviousRuns } from '../utils/abort-previous-runs';
7
8
  import { signalizedProcess } from '../utils/signalized-process';
8
9
  import { useOnMount } from './use-on-mount';
@@ -52,9 +53,23 @@ export function useStyleItems() {
52
53
  } );
53
54
  } );
54
55
 
55
- return Object.values( styleItems )
56
- .sort( ( { provider: providerA }, { provider: providerB } ) => providerA.priority - providerB.priority )
57
- .flatMap( ( { items } ) => items );
56
+ const breakpointsOrder = getBreakpoints().map( ( breakpoint ) => breakpoint.id );
57
+
58
+ return useMemo(
59
+ () =>
60
+ Object.values( styleItems )
61
+ .sort( ( { provider: providerA }, { provider: providerB } ) => providerA.priority - providerB.priority )
62
+ .flatMap( ( { items } ) => items )
63
+ .sort( ( { breakpoint: breakpointA }, { breakpoint: breakpointB } ) => {
64
+ return (
65
+ breakpointsOrder.indexOf( breakpointA as BreakpointId ) -
66
+ breakpointsOrder.indexOf( breakpointB as BreakpointId )
67
+ );
68
+ } ),
69
+ // eslint-disable-next-line
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ [ styleItems, breakpointsOrder.join( '-' ) ]
72
+ );
58
73
  }
59
74
 
60
75
  type CreateProviderSubscriberArgs = {
@@ -78,7 +93,7 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems }: Cr
78
93
  };
79
94
  } );
80
95
 
81
- return renderStyles( { styles, signal } );
96
+ return renderStyles( { styles: breakToBreakpoints( styles ), signal } );
82
97
  } )
83
98
  .then( ( items ) => {
84
99
  setStyleItems( ( prev ) => ( {
@@ -88,4 +103,31 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems }: Cr
88
103
  } )
89
104
  .execute()
90
105
  );
106
+
107
+ function breakToBreakpoints( styles: RendererStyleDefinition[] ) {
108
+ return Object.values(
109
+ styles.reduce(
110
+ ( acc, style ) => {
111
+ style.variants.forEach( ( variant ) => {
112
+ const breakpoint = variant.meta.breakpoint || 'desktop';
113
+
114
+ if ( ! acc[ style.id ] ) {
115
+ acc[ style.id ] = {};
116
+ }
117
+
118
+ if ( ! acc[ style.id ][ breakpoint ] ) {
119
+ acc[ style.id ][ breakpoint ] = {
120
+ ...style,
121
+ variants: [],
122
+ };
123
+ }
124
+
125
+ acc[ style.id ][ breakpoint ].variants.push( variant );
126
+ } );
127
+ return acc;
128
+ },
129
+ {} as Record< string, Record< string, RendererStyleDefinition > >
130
+ )
131
+ ).flatMap( ( breakpointMap ) => Object.values( breakpointMap ) );
132
+ }
91
133
  }
@@ -1,4 +1,5 @@
1
1
  import { settingsTransformersRegistry } from './settings-transformers-registry';
2
+ import { attributesTransformer } from './transformers/settings/attributes-transformer';
2
3
  import { createClassesTransformer } from './transformers/settings/classes-transformer';
3
4
  import { linkTransformer } from './transformers/settings/link-transformer';
4
5
  import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
@@ -11,5 +12,6 @@ export function initSettingsTransformers() {
11
12
  .register( 'link', linkTransformer )
12
13
  .register( 'image', imageTransformer )
13
14
  .register( 'image-src', imageSrcTransformer )
15
+ .register( 'key-value-array', attributesTransformer )
14
16
  .registerFallback( plainTransformer );
15
17
  }
@@ -12,12 +12,17 @@ import { colorStopTransformer } from './transformers/styles/color-stop-transform
12
12
  import { createCombineArrayTransformer } from './transformers/styles/create-combine-array-transformer';
13
13
  import { createMultiPropsTransformer } from './transformers/styles/create-multi-props-transformer';
14
14
  import { filterTransformer } from './transformers/styles/filter-transformer';
15
+ import { flexTransformer } from './transformers/styles/flex-transformer';
15
16
  import { positionTransformer } from './transformers/styles/position-transformer';
16
17
  import { shadowTransformer } from './transformers/styles/shadow-transformer';
17
18
  import { sizeTransformer } from './transformers/styles/size-transformer';
18
19
  import { strokeTransformer } from './transformers/styles/stroke-transformer';
19
20
  import { transformMoveTransformer } from './transformers/styles/transform-move-transformer';
21
+ import { transformRotateTransformer } from './transformers/styles/transform-rotate-transformer';
22
+ import { transformScaleTransformer } from './transformers/styles/transform-scale-transformer';
23
+ import { transformSkewTransformer } from './transformers/styles/transform-skew-transformer';
20
24
  import { transformTransformer } from './transformers/styles/transform-transformer';
25
+ import { transitionTransformer } from './transformers/styles/transition-transformer';
21
26
 
22
27
  export function initStyleTransformers() {
23
28
  styleTransformersRegistry
@@ -32,6 +37,7 @@ export function initStyleTransformers() {
32
37
  )
33
38
  )
34
39
  .register( 'filter', filterTransformer )
40
+ .register( 'backdrop-filter', filterTransformer )
35
41
  .register( 'box-shadow', createCombineArrayTransformer( ',' ) )
36
42
  .register( 'background', backgroundTransformer )
37
43
  .register( 'background-overlay', backgroundOverlayTransformer )
@@ -46,11 +52,16 @@ export function initStyleTransformers() {
46
52
  .register( 'image', imageTransformer )
47
53
  .register( 'object-position', positionTransformer )
48
54
  .register( 'transform-move', transformMoveTransformer )
55
+ .register( 'transform-scale', transformScaleTransformer )
56
+ .register( 'transform-rotate', transformRotateTransformer )
57
+ .register( 'transform-skew', transformSkewTransformer )
49
58
  .register( 'transform', transformTransformer )
59
+ .register( 'transition', transitionTransformer )
50
60
  .register(
51
61
  'layout-direction',
52
62
  createMultiPropsTransformer( [ 'row', 'column' ], ( { propKey, key } ) => `${ key }-${ propKey }` )
53
63
  )
64
+ .register( 'flex', flexTransformer )
54
65
  .register(
55
66
  'border-width',
56
67
  createMultiPropsTransformer(
@@ -3,10 +3,12 @@
3
3
  exports[`renderStyles should render styles 1`] = `
4
4
  [
5
5
  {
6
+ "breakpoint": "desktop",
6
7
  "id": "test",
7
8
  "value": ".test{font-size:10px;}.test:hover{font-size:20px;}@media(max-width:992px){.test{font-size:30px;}}@media(max-width:768px){.test:focus{font-size:40px;}}",
8
9
  },
9
10
  {
11
+ "breakpoint": "desktop",
10
12
  "id": "test-2",
11
13
  "value": ".custom-name{font-size:50px;}",
12
14
  },
@@ -1,8 +1,14 @@
1
1
  /* eslint-disable testing-library/render-result-naming-convention */
2
2
  import type { BreakpointsMap } from '@elementor/editor-responsive';
3
+ import { encodeString } from '@elementor/utils';
3
4
 
4
5
  import { createStylesRenderer, type RendererStyleDefinition } from '../create-styles-renderer';
5
6
 
7
+ jest.mock( '@elementor/editor-v1-adapters', () => ( {
8
+ ...jest.requireActual( '@elementor/editor-v1-adapters' ),
9
+ isExperimentActive: jest.fn().mockReturnValue( true ),
10
+ } ) );
11
+
6
12
  describe( 'renderStyles', () => {
7
13
  it( 'should render styles', async () => {
8
14
  // Arrange.
@@ -15,18 +21,22 @@ describe( 'renderStyles', () => {
15
21
  {
16
22
  meta: { breakpoint: null, state: null },
17
23
  props: { 'font-size': '10px' },
24
+ custom_css: null,
18
25
  },
19
26
  {
20
27
  meta: { breakpoint: null, state: 'hover' },
21
28
  props: { 'font-size': '20px' },
29
+ custom_css: null,
22
30
  },
23
31
  {
24
32
  meta: { breakpoint: 'tablet', state: null },
25
33
  props: { 'font-size': '30px' },
34
+ custom_css: null,
26
35
  },
27
36
  {
28
37
  meta: { breakpoint: 'mobile', state: 'focus' },
29
38
  props: { 'font-size': '40px' },
39
+ custom_css: null,
30
40
  },
31
41
  ],
32
42
  };
@@ -40,6 +50,7 @@ describe( 'renderStyles', () => {
40
50
  {
41
51
  meta: { breakpoint: null, state: null },
42
52
  props: { 'font-size': '50px' },
53
+ custom_css: null,
43
54
  },
44
55
  ],
45
56
  };
@@ -79,6 +90,7 @@ describe( 'renderStyles', () => {
79
90
  {
80
91
  meta: { breakpoint: null, state: null },
81
92
  props: { 'font-size': '24px' },
93
+ custom_css: null,
82
94
  },
83
95
  ],
84
96
  };
@@ -97,9 +109,95 @@ describe( 'renderStyles', () => {
97
109
  // Assert.
98
110
  expect( result ).toEqual( [
99
111
  {
112
+ breakpoint: 'desktop',
100
113
  id: 'test',
101
114
  value: '.elementor-prefix .test{font-size:24px;}',
102
115
  },
103
116
  ] );
104
117
  } );
105
118
  } );
119
+
120
+ describe( 'custom_css rendering', () => {
121
+ it( 'should not render custom_css if raw is empty', async () => {
122
+ // Arrange.
123
+ const styleDef: RendererStyleDefinition = {
124
+ id: 'test',
125
+ type: 'class',
126
+ cssName: 'test',
127
+ label: 'Test',
128
+ variants: [ { meta: { breakpoint: null, state: null }, props: {}, custom_css: { raw: '' } } ],
129
+ };
130
+
131
+ // Act.
132
+ const renderStyles = createStylesRenderer( { breakpoints: {} as BreakpointsMap, resolve: async () => ( {} ) } );
133
+ const result = await renderStyles( { styles: [ styleDef ] } );
134
+
135
+ // Assert.
136
+ expect( result[ 0 ].value ).not.toContain( '{;}' );
137
+ } );
138
+
139
+ it( 'should not render custom_css if raw is whitespace', async () => {
140
+ // Arrange.
141
+ const styleDef: RendererStyleDefinition = {
142
+ id: 'test',
143
+ type: 'class',
144
+ cssName: 'test',
145
+ label: 'Test',
146
+ variants: [
147
+ { meta: { breakpoint: null, state: null }, props: {}, custom_css: { raw: encodeString( ' \n\t' ) } },
148
+ ],
149
+ };
150
+
151
+ // Act.
152
+ const renderStyles = createStylesRenderer( { breakpoints: {} as BreakpointsMap, resolve: async () => ( {} ) } );
153
+ const result = await renderStyles( { styles: [ styleDef ] } );
154
+
155
+ // Assert.
156
+ expect( result[ 0 ].value ).not.toContain( '{;}' );
157
+ } );
158
+
159
+ it( 'should not render custom_css if raw is invalid encoded string', async () => {
160
+ // Arrange.
161
+ const styleDef: RendererStyleDefinition = {
162
+ id: 'test',
163
+ type: 'class',
164
+ cssName: 'test',
165
+ label: 'Test',
166
+ variants: [
167
+ { meta: { breakpoint: null, state: null }, props: {}, custom_css: { raw: 'I cannot be decoded' } },
168
+ ],
169
+ };
170
+
171
+ // Act.
172
+ const renderStyles = createStylesRenderer( { breakpoints: {} as BreakpointsMap, resolve: async () => ( {} ) } );
173
+ const result = await renderStyles( { styles: [ styleDef ] } );
174
+
175
+ // Assert.
176
+ expect( result[ 0 ].value ).not.toContain( '{;}' );
177
+ } );
178
+
179
+ it( 'should render custom_css if raw is valid base64 encoded string', async () => {
180
+ // Arrange.
181
+ const css = 'transition: 100s; \n .inner-selector { color: red; }';
182
+ const styleDef: RendererStyleDefinition = {
183
+ id: 'test',
184
+ type: 'class',
185
+ cssName: 'test',
186
+ label: 'Test',
187
+ variants: [
188
+ {
189
+ meta: { breakpoint: null, state: null },
190
+ props: {},
191
+ custom_css: { raw: encodeString( css ) },
192
+ },
193
+ ],
194
+ };
195
+
196
+ // Act.
197
+ const renderStyles = createStylesRenderer( { breakpoints: {} as BreakpointsMap, resolve: async () => ( {} ) } );
198
+ const result = await renderStyles( { styles: [ styleDef ] } );
199
+
200
+ // Assert.
201
+ expect( result[ 0 ].value ).toContain( css );
202
+ } );
203
+ } );
@@ -1,6 +1,13 @@
1
1
  import type { Props } from '@elementor/editor-props';
2
2
  import { type Breakpoint, type BreakpointsMap } from '@elementor/editor-responsive';
3
- import { type StyleDefinition, type StyleDefinitionState, type StyleDefinitionType } from '@elementor/editor-styles';
3
+ import {
4
+ type CustomCss,
5
+ type StyleDefinition,
6
+ type StyleDefinitionState,
7
+ type StyleDefinitionType,
8
+ } from '@elementor/editor-styles';
9
+ import { EXPERIMENTAL_FEATURES, isExperimentActive } from '@elementor/editor-v1-adapters';
10
+ import { decodeString } from '@elementor/utils';
4
11
 
5
12
  import { type PropsResolver } from './create-props-resolver';
6
13
  import { UnknownStyleTypeError } from './errors';
@@ -8,6 +15,7 @@ import { UnknownStyleTypeError } from './errors';
8
15
  export type StyleItem = {
9
16
  id: string;
10
17
  value: string;
18
+ breakpoint: string;
11
19
  };
12
20
 
13
21
  export type StyleRenderer = ReturnType< typeof createStylesRenderer >;
@@ -42,19 +50,21 @@ export function createStylesRenderer( { resolve, breakpoints, selectorPrefix = '
42
50
  const stylesCssPromises = styles.map( async ( style ) => {
43
51
  const variantCssPromises = Object.values( style.variants ).map( async ( variant ) => {
44
52
  const css = await propsToCss( { props: variant.props, resolve, signal } );
53
+ const customCss = customCssToString( variant.custom_css );
45
54
 
46
55
  return createStyleWrapper()
47
56
  .for( style.cssName, style.type )
48
57
  .withPrefix( selectorPrefix )
49
58
  .withState( variant.meta.state )
50
59
  .withMediaQuery( variant.meta.breakpoint ? breakpoints[ variant.meta.breakpoint ] : null )
51
- .wrap( css );
60
+ .wrap( css + customCss );
52
61
  } );
53
62
 
54
63
  const variantsCss = await Promise.all( variantCssPromises );
55
64
 
56
65
  return {
57
66
  id: style.id,
67
+ breakpoint: style?.variants[ 0 ]?.meta?.breakpoint || 'desktop',
58
68
  value: variantsCss.join( '' ),
59
69
  };
60
70
  } );
@@ -118,3 +128,17 @@ async function propsToCss( { props, resolve, signal }: PropsToCssArgs ) {
118
128
  }, [] )
119
129
  .join( '' );
120
130
  }
131
+
132
+ function customCssToString( customCss: CustomCss | null ): string {
133
+ if ( ! isExperimentActive( EXPERIMENTAL_FEATURES.CUSTOM_CSS ) || ! customCss?.raw ) {
134
+ return '';
135
+ }
136
+
137
+ const decoded = decodeString( customCss.raw );
138
+
139
+ if ( ! decoded.trim() ) {
140
+ return '';
141
+ }
142
+
143
+ return decoded + '\n';
144
+ }