@elementor/editor-canvas 3.33.0-98 → 3.34.2

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 (59) hide show
  1. package/dist/index.d.mts +133 -10
  2. package/dist/index.d.ts +133 -10
  3. package/dist/index.js +1413 -212
  4. package/dist/index.mjs +1399 -180
  5. package/package.json +18 -14
  6. package/src/__tests__/settings-props-resolver.test.ts +0 -40
  7. package/src/__tests__/styles-prop-resolver.test.ts +13 -0
  8. package/src/components/__tests__/__snapshots__/style-renderer.test.tsx.snap +2 -6
  9. package/src/components/__tests__/elements-overlays.test.tsx +96 -12
  10. package/src/components/__tests__/inline-editor-overlay.test.tsx +245 -0
  11. package/src/components/__tests__/style-renderer.test.tsx +2 -2
  12. package/src/components/elements-overlays.tsx +33 -10
  13. package/src/components/inline-editor-overlay.tsx +79 -0
  14. package/src/components/interactions-renderer.tsx +33 -0
  15. package/src/components/{element-overlay.tsx → outline-overlay.tsx} +8 -7
  16. package/src/components/style-renderer.tsx +2 -4
  17. package/src/hooks/__tests__/use-has-overlapping.test.ts +187 -0
  18. package/src/hooks/use-floating-on-element.ts +11 -8
  19. package/src/hooks/use-has-overlapping.ts +21 -0
  20. package/src/hooks/use-interactions-items.ts +108 -0
  21. package/src/hooks/use-style-items.ts +34 -8
  22. package/src/index.ts +9 -0
  23. package/src/init-settings-transformers.ts +4 -0
  24. package/src/init.tsx +18 -0
  25. package/src/legacy/create-templated-element-type.ts +67 -42
  26. package/src/legacy/init-legacy-views.ts +27 -5
  27. package/src/legacy/types.ts +44 -4
  28. package/src/mcp/canvas-mcp.ts +17 -0
  29. package/src/mcp/mcp-description.ts +40 -0
  30. package/src/mcp/resources/widgets-schema-resource.ts +173 -0
  31. package/src/mcp/tools/build-composition/prompt.ts +128 -0
  32. package/src/mcp/tools/build-composition/schema.ts +31 -0
  33. package/src/mcp/tools/build-composition/tool.ts +163 -0
  34. package/src/mcp/tools/configure-element/prompt.ts +93 -0
  35. package/src/mcp/tools/configure-element/schema.ts +25 -0
  36. package/src/mcp/tools/configure-element/tool.ts +67 -0
  37. package/src/mcp/tools/get-element-config/tool.ts +69 -0
  38. package/src/mcp/utils/do-update-element-property.ts +129 -0
  39. package/src/mcp/utils/generate-available-tags.ts +23 -0
  40. package/src/renderers/__tests__/__snapshots__/create-styles-renderer.test.ts.snap +2 -0
  41. package/src/renderers/__tests__/create-styles-renderer.test.ts +25 -0
  42. package/src/renderers/create-props-resolver.ts +8 -1
  43. package/src/renderers/create-styles-renderer.ts +20 -9
  44. package/src/renderers/errors.ts +6 -0
  45. package/src/sync/drag-element-from-panel.ts +49 -0
  46. package/src/sync/types.ts +32 -1
  47. package/src/transformers/settings/__tests__/attributes-transformer.test.ts +15 -0
  48. package/src/transformers/settings/__tests__/classes-transformer.test.ts +83 -0
  49. package/src/transformers/settings/attributes-transformer.ts +1 -23
  50. package/src/transformers/settings/classes-transformer.ts +21 -21
  51. package/src/transformers/settings/date-time-transformer.ts +12 -0
  52. package/src/transformers/settings/query-transformer.ts +10 -0
  53. package/src/transformers/styles/__tests__/transform-origin-transformer.test.ts +24 -0
  54. package/src/transformers/styles/__tests__/transition-transformer.test.ts +52 -0
  55. package/src/transformers/styles/background-transformer.ts +3 -1
  56. package/src/transformers/styles/transform-origin-transformer.ts +12 -2
  57. package/src/transformers/styles/transition-transformer.ts +34 -4
  58. package/src/types/element-overlay.ts +18 -0
  59. package/src/utils/inline-editing-utils.ts +43 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "3.33.0-98",
4
+ "version": "3.34.2",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,19 +37,23 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "3.33.0-98",
41
- "@elementor/editor-notifications": "3.33.0-98",
42
- "@elementor/editor-documents": "3.33.0-98",
43
- "@elementor/editor-elements": "3.33.0-98",
44
- "@elementor/editor-props": "3.33.0-98",
45
- "@elementor/editor-responsive": "3.33.0-98",
46
- "@elementor/editor-styles": "3.33.0-98",
47
- "@elementor/editor-styles-repository": "3.33.0-98",
48
- "@elementor/editor-v1-adapters": "3.33.0-98",
49
- "@elementor/twing": "3.33.0-98",
50
- "@elementor/ui": "1.36.12",
51
- "@elementor/utils": "3.33.0-98",
52
- "@elementor/wp-media": "3.33.0-98",
40
+ "@elementor/editor": "3.34.2",
41
+ "@elementor/editor-controls": "3.34.2",
42
+ "@elementor/editor-documents": "3.34.2",
43
+ "@elementor/editor-elements": "3.34.2",
44
+ "@elementor/editor-interactions": "3.34.2",
45
+ "@elementor/editor-notifications": "3.34.2",
46
+ "@elementor/editor-props": "3.34.2",
47
+ "@elementor/editor-responsive": "3.34.2",
48
+ "@elementor/editor-styles": "3.34.2",
49
+ "@elementor/editor-styles-repository": "3.34.2",
50
+ "@elementor/editor-v1-adapters": "3.34.2",
51
+ "@elementor/editor-mcp": "3.34.2",
52
+ "@elementor/schema": "3.34.2",
53
+ "@elementor/twing": "3.34.2",
54
+ "@elementor/ui": "1.36.17",
55
+ "@elementor/utils": "3.34.2",
56
+ "@elementor/wp-media": "3.34.2",
53
57
  "@floating-ui/react": "^0.27.5",
54
58
  "@wordpress/i18n": "^5.13.0"
55
59
  },
@@ -1,7 +1,5 @@
1
- import { createMockStyleDefinition, createMockStylesProvider } from 'test-utils';
2
1
  import {
3
2
  booleanPropTypeUtil,
4
- classesPropTypeUtil,
5
3
  imageAttachmentIdPropType,
6
4
  imagePropTypeUtil,
7
5
  imageSrcPropTypeUtil,
@@ -12,7 +10,6 @@ import {
12
10
  stringPropTypeUtil,
13
11
  urlPropTypeUtil,
14
12
  } from '@elementor/editor-props';
15
- import { stylesRepository } from '@elementor/editor-styles-repository';
16
13
  import { getMediaAttachment } from '@elementor/wp-media';
17
14
 
18
15
  import { initSettingsTransformers } from '../init-settings-transformers';
@@ -21,7 +18,6 @@ import { settingsTransformersRegistry } from '../settings-transformers-registry'
21
18
  import { mockAttachmentData } from './mock-attachment-data';
22
19
  import {
23
20
  booleanPropType,
24
- classesPropType,
25
21
  imagePropType,
26
22
  linkPropType,
27
23
  numberPropType,
@@ -69,42 +65,6 @@ describe( 'settings props resolver', () => {
69
65
  boolean: true,
70
66
  },
71
67
  },
72
- {
73
- name: 'classes',
74
- props: {
75
- classes: classesPropTypeUtil.create( [
76
- 'test-1',
77
- 'test-2-suffix',
78
- 'without-provider',
79
- '',
80
- null as unknown as string,
81
- undefined as unknown as string,
82
- ] ),
83
- },
84
- prepare: () => {
85
- jest.mocked( stylesRepository.getProviders ).mockReturnValue( [
86
- createMockStylesProvider( {
87
- key: 'test-1-provider',
88
- actions: {
89
- all: () => [ createMockStyleDefinition( { id: 'test-1' } ) ],
90
- },
91
- } ),
92
- createMockStylesProvider( {
93
- key: 'test-2-provider',
94
- actions: {
95
- resolveCssName: ( id ) => `${ id }-suffix`,
96
- all: () => [ createMockStyleDefinition( { id: 'test-2' } ) ],
97
- },
98
- } ),
99
- ] );
100
- },
101
- schema: {
102
- classes: classesPropType(),
103
- },
104
- expected: {
105
- classes: [ 'test-1', 'test-2-suffix', 'without-provider' ],
106
- },
107
- },
108
68
  {
109
69
  name: 'link',
110
70
  props: {
@@ -316,6 +316,7 @@ describe( 'styles prop resolver', () => {
316
316
  props: {
317
317
  background: backgroundPropTypeUtil.create( {
318
318
  color: colorPropTypeUtil.create( '#000' ),
319
+ clip: stringPropTypeUtil.create( 'text' ),
319
320
  'background-overlay': backgroundOverlayPropTypeUtil.create( [
320
321
  backgroundColorOverlayPropTypeUtil.create( {
321
322
  color: colorPropTypeUtil.create( 'blue' ),
@@ -370,6 +371,7 @@ describe( 'styles prop resolver', () => {
370
371
  },
371
372
  expected: {
372
373
  'background-color': '#000',
374
+ 'background-clip': 'text',
373
375
  'background-attachment': 'scroll,scroll,scroll,fixed',
374
376
  'background-image':
375
377
  'linear-gradient(blue, blue),linear-gradient(yellow, yellow),url(thumbnail-image-url-123),url(medium_large-image-url-123)',
@@ -378,6 +380,15 @@ describe( 'styles prop resolver', () => {
378
380
  'background-size': 'auto auto,auto auto,1400px auto,auto',
379
381
  },
380
382
  },
383
+ {
384
+ name: 'mix-blend-mode',
385
+ props: {
386
+ 'mix-blend-mode': stringPropTypeUtil.create( 'multiply' ),
387
+ },
388
+ expected: {
389
+ 'mix-blend-mode': 'multiply',
390
+ },
391
+ },
381
392
  {
382
393
  name: 'filter',
383
394
  props: {
@@ -560,6 +571,7 @@ describe( 'styles prop resolver', () => {
560
571
  props: {
561
572
  background: backgroundPropTypeUtil.create( {
562
573
  color: colorPropTypeUtil.create( '#000' ),
574
+ clip: stringPropTypeUtil.create( 'border-box' ),
563
575
  'background-overlay': backgroundOverlayPropTypeUtil.create( [
564
576
  backgroundImageOverlayPropTypeUtil.create( {
565
577
  image: imagePropTypeUtil.create( {
@@ -581,6 +593,7 @@ describe( 'styles prop resolver', () => {
581
593
  },
582
594
  expected: {
583
595
  'background-color': '#000',
596
+ 'background-clip': 'border-box',
584
597
  'background-image': 'url(original-image-url-123)',
585
598
  'background-repeat': 'repeat',
586
599
  'background-size': '1400px auto',
@@ -4,14 +4,10 @@ exports[`<StyleRenderer /> should render styles and links in portal when contain
4
4
  <div
5
5
  data-testid="portal"
6
6
  >
7
- <style
8
- data-e-style-id="style1"
9
- >
7
+ <style>
10
8
  .test { color: red; }
11
9
  </style>
12
- <style
13
- data-e-style-id="style2"
14
- >
10
+ <style>
15
11
  .test2 { color: blue; }
16
12
  </style>
17
13
  <link
@@ -1,18 +1,25 @@
1
1
  import * as React from 'react';
2
2
  import { createDOMElement, createMockElement, createMockElementType, renderWithTheme } from 'test-utils';
3
3
  import { getElements, useSelectedElement } from '@elementor/editor-elements';
4
- import { __privateUseIsRouteActive as useIsRouteActive, useEditMode } from '@elementor/editor-v1-adapters';
5
- import { act, screen } from '@testing-library/react';
6
-
7
- import { CANVAS_WRAPPER_ID } from '../element-overlay';
4
+ import {
5
+ __privateUseIsRouteActive as useIsRouteActive,
6
+ isExperimentActive,
7
+ useEditMode,
8
+ } from '@elementor/editor-v1-adapters';
9
+ import { screen, waitFor } from '@testing-library/react';
10
+
11
+ import { hasInlineEditableProperty } from '../../utils/inline-editing-utils';
8
12
  import { ElementsOverlays } from '../elements-overlays';
13
+ import { CANVAS_WRAPPER_ID } from '../outline-overlay';
9
14
 
10
15
  jest.mock( '@elementor/editor-elements' );
11
16
  jest.mock( '@elementor/editor-v1-adapters', () => ( {
12
17
  ...jest.requireActual( '@elementor/editor-v1-adapters' ),
13
18
  useEditMode: jest.fn(),
14
19
  __privateUseIsRouteActive: jest.fn(),
20
+ isExperimentActive: jest.fn(),
15
21
  } ) );
22
+ jest.mock( '../../utils/inline-editing-utils' );
16
23
 
17
24
  describe( '<ElementsOverlays />', () => {
18
25
  beforeEach( () => {
@@ -23,6 +30,8 @@ describe( '<ElementsOverlays />', () => {
23
30
 
24
31
  jest.mocked( useEditMode ).mockReturnValue( 'edit' );
25
32
  jest.mocked( useIsRouteActive ).mockReturnValue( false );
33
+ jest.mocked( hasInlineEditableProperty ).mockReturnValue( false );
34
+ jest.mocked( isExperimentActive ).mockReturnValue( true );
26
35
 
27
36
  jest.mocked( getElements ).mockReturnValue( [
28
37
  createMockElement( {
@@ -68,16 +77,15 @@ describe( '<ElementsOverlays />', () => {
68
77
  } );
69
78
 
70
79
  // Act.
71
- // eslint-disable-next-line testing-library/no-unnecessary-act
72
- await act( () => renderWithTheme( <ElementsOverlays /> ) );
80
+ renderWithTheme( <ElementsOverlays /> );
73
81
 
74
82
  // Assert.
75
- const overlay = screen.getByRole( 'presentation' );
76
-
77
- expect( overlay ).toHaveAttribute( 'data-element-overlay', 'atomic2' );
78
-
79
- // eslint-disable-next-line testing-library/no-test-id-queries
80
- expect( screen.getByTestId( CANVAS_WRAPPER_ID ) ).toContainElement( overlay );
83
+ await waitFor( () => {
84
+ const overlay = screen.getByRole( 'presentation' );
85
+ expect( overlay ).toHaveAttribute( 'data-element-overlay', 'atomic2' );
86
+ // eslint-disable-next-line testing-library/no-test-id-queries
87
+ expect( screen.getByTestId( CANVAS_WRAPPER_ID ) ).toContainElement( overlay );
88
+ } );
81
89
  } );
82
90
 
83
91
  it.each( [
@@ -128,4 +136,80 @@ describe( '<ElementsOverlays />', () => {
128
136
  // Assert.
129
137
  expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
130
138
  } );
139
+
140
+ it( 'should return null when editor is not in edit mode', () => {
141
+ // Arrange
142
+ jest.mocked( useEditMode ).mockReturnValue( 'preview' );
143
+ jest.mocked( useIsRouteActive ).mockReturnValue( false );
144
+ jest.mocked( useSelectedElement ).mockReturnValue( {
145
+ element: { id: 'atomic1', type: 'widget' },
146
+ elementType: createMockElementType(),
147
+ } );
148
+
149
+ // Act
150
+ const { container } = renderWithTheme( <ElementsOverlays /> );
151
+
152
+ // Assert
153
+ expect( container ).toBeEmptyDOMElement();
154
+ expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
155
+ } );
156
+
157
+ it( 'should return null when panel/global route is active', () => {
158
+ // Arrange
159
+ jest.mocked( useEditMode ).mockReturnValue( 'edit' );
160
+ jest.mocked( useIsRouteActive ).mockReturnValue( true );
161
+ jest.mocked( useSelectedElement ).mockReturnValue( {
162
+ element: { id: 'atomic1', type: 'widget' },
163
+ elementType: createMockElementType(),
164
+ } );
165
+
166
+ // Act
167
+ const { container } = renderWithTheme( <ElementsOverlays /> );
168
+
169
+ // Assert
170
+ expect( container ).toBeEmptyDOMElement();
171
+ expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
172
+ } );
173
+
174
+ it( 'should render OutlineOverlay for atomic elements', async () => {
175
+ // Arrange
176
+ jest.mocked( useSelectedElement ).mockReturnValue( {
177
+ element: { id: 'atomic1', type: 'widget' },
178
+ elementType: createMockElementType(),
179
+ } );
180
+
181
+ // Act
182
+ renderWithTheme( <ElementsOverlays /> );
183
+
184
+ // Assert
185
+ const overlay = await screen.findByRole( 'presentation' );
186
+ expect( overlay ).toBeInTheDocument();
187
+ expect( overlay ).toHaveAttribute( 'data-element-overlay', 'atomic1' );
188
+ } );
189
+
190
+ it( 'should render InlineEditorOverlay only for selected elements that support inline editing', async () => {
191
+ // Arrange
192
+ const headingEl = createDOMElement( { tag: 'div', attrs: { 'data-atomic': '', id: '50' } } );
193
+
194
+ jest.mocked( getElements ).mockReturnValue( [
195
+ createMockElement( {
196
+ model: { id: 'heading-element' },
197
+ view: { el: headingEl, getDomElement: () => ( { get: () => headingEl } ) },
198
+ } ),
199
+ ] );
200
+
201
+ jest.mocked( useSelectedElement ).mockReturnValue( {
202
+ element: { id: 'heading-element', type: 'widget' },
203
+ elementType: createMockElementType(),
204
+ } );
205
+
206
+ // Act
207
+ renderWithTheme( <ElementsOverlays /> );
208
+
209
+ // Assert
210
+ await waitFor( () => {
211
+ const overlay = screen.getByRole( 'presentation' );
212
+ expect( overlay ).toHaveAttribute( 'data-element-overlay', 'heading-element' );
213
+ } );
214
+ } );
131
215
  } );
@@ -0,0 +1,245 @@
1
+ import * as React from 'react';
2
+ import { renderWithTheme } from 'test-utils';
3
+ import { getContainer, updateElementSettings, useElementSetting } from '@elementor/editor-elements';
4
+ import { htmlPropTypeUtil } from '@elementor/editor-props';
5
+ import { debounce } from '@elementor/utils';
6
+ import { act, screen } from '@testing-library/react';
7
+
8
+ import { useFloatingOnElement } from '../../hooks/use-floating-on-element';
9
+ import { getInlineEditablePropertyName } from '../../utils/inline-editing-utils';
10
+ import { InlineEditorOverlay } from '../inline-editor-overlay';
11
+
12
+ jest.mock( '@elementor/editor-elements' );
13
+ jest.mock( '@elementor/editor-props' );
14
+ jest.mock( '@elementor/utils' );
15
+ jest.mock( '@elementor/editor-controls', () => ( {
16
+ InlineEditor: jest.fn( ( { value, setValue } ) => (
17
+ <div aria-label="inline editor container">
18
+ <input aria-label="inline editor" value={ value } onChange={ ( e ) => setValue( e.target.value ) } />
19
+ </div>
20
+ ) ),
21
+ } ) );
22
+ jest.mock( '../../hooks/use-floating-on-element' );
23
+ jest.mock( '../../utils/inline-editing-utils' );
24
+
25
+ describe( '<InlineEditorOverlay />', () => {
26
+ const mockElement = document.createElement( 'div' );
27
+ const mockId = 'test-element-id';
28
+ const mockPropertyName = 'title';
29
+ const mockValue = '<p>Test content</p>';
30
+
31
+ beforeEach( () => {
32
+ jest.mocked( useFloatingOnElement ).mockReturnValue( {
33
+ floating: {
34
+ setRef: jest.fn(),
35
+ ref: { current: null },
36
+ styles: { position: 'absolute', top: 0, left: 0 },
37
+ },
38
+ isVisible: true,
39
+ context: {} as never,
40
+ } );
41
+
42
+ jest.mocked( getContainer ).mockReturnValue( {
43
+ model: {
44
+ get: jest.fn().mockReturnValue( 'e-heading' ),
45
+ },
46
+ } as unknown as ReturnType< typeof getContainer > );
47
+
48
+ jest.mocked( getInlineEditablePropertyName ).mockReturnValue( mockPropertyName );
49
+
50
+ jest.mocked( useElementSetting ).mockReturnValue( {
51
+ $$type: 'html',
52
+ value: mockValue,
53
+ } );
54
+
55
+ jest.mocked( htmlPropTypeUtil.extract ).mockReturnValue( mockValue );
56
+
57
+ jest.mocked( htmlPropTypeUtil.create ).mockImplementation( ( value ) => ( {
58
+ $$type: 'html',
59
+ value: typeof value === 'function' ? value( null ) : value,
60
+ } ) );
61
+
62
+ const mockDebouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
63
+ const debouncedFn = ( ...args: unknown[] ) => fn( ...args );
64
+ (
65
+ debouncedFn as typeof debouncedFn & {
66
+ cancel: jest.Mock;
67
+ flush: jest.Mock;
68
+ pending: jest.Mock;
69
+ }
70
+ ).cancel = jest.fn();
71
+ (
72
+ debouncedFn as typeof debouncedFn & {
73
+ cancel: jest.Mock;
74
+ flush: jest.Mock;
75
+ pending: jest.Mock;
76
+ }
77
+ ).flush = jest.fn( ( ...args: unknown[] ) => fn( ...args ) );
78
+ (
79
+ debouncedFn as typeof debouncedFn & {
80
+ cancel: jest.Mock;
81
+ flush: jest.Mock;
82
+ pending: jest.Mock;
83
+ }
84
+ ).pending = jest.fn().mockReturnValue( false );
85
+ return debouncedFn;
86
+ } );
87
+ jest.mocked( debounce ).mockImplementation( mockDebouncedFn as unknown as typeof debounce );
88
+ } );
89
+
90
+ afterEach( () => {
91
+ jest.clearAllMocks();
92
+ } );
93
+
94
+ it( 'should render InlineEditor when visible', () => {
95
+ renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
96
+
97
+ const input = screen.getByRole( 'textbox', { name: 'inline editor' } );
98
+ expect( input ).toBeInTheDocument();
99
+ expect( input ).toHaveValue( mockValue );
100
+ } );
101
+
102
+ it( 'should not render when not visible', () => {
103
+ jest.mocked( useFloatingOnElement ).mockReturnValue( {
104
+ floating: {
105
+ setRef: jest.fn(),
106
+ ref: { current: null },
107
+ styles: {},
108
+ },
109
+ isVisible: false,
110
+ context: {} as never,
111
+ } );
112
+
113
+ renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ false } id={ mockId } /> );
114
+
115
+ expect( screen.queryByRole( 'textbox', { name: 'inline editor' } ) ).not.toBeInTheDocument();
116
+ } );
117
+
118
+ it( 'should get container and property name on mount', () => {
119
+ renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
120
+
121
+ expect( getContainer ).toHaveBeenCalledWith( mockId );
122
+ expect( getInlineEditablePropertyName ).toHaveBeenCalled();
123
+ } );
124
+
125
+ it( 'should extract value from contentProp using htmlPropTypeUtil', () => {
126
+ renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
127
+
128
+ expect( htmlPropTypeUtil.extract ).toHaveBeenCalledWith( {
129
+ $$type: 'html',
130
+ value: mockValue,
131
+ } );
132
+ } );
133
+
134
+ it( 'should call updateElementSettings when value changes', async () => {
135
+ const newValue = '<p>New content</p>';
136
+ const mockDebounceFn = jest.fn();
137
+ const debouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
138
+ mockDebounceFn.mockImplementation( fn );
139
+ return mockDebounceFn;
140
+ } );
141
+ jest.mocked( debounce ).mockImplementation( debouncedFn as unknown as typeof debounce );
142
+
143
+ renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
144
+
145
+ const input = screen.getByRole( 'textbox', { name: 'inline editor' } ) as HTMLInputElement;
146
+
147
+ await act( async () => {
148
+ input.dispatchEvent( new Event( 'change', { bubbles: true } ) );
149
+ Object.defineProperty( input, 'value', { value: newValue, writable: true } );
150
+ } );
151
+
152
+ await act( async () => {
153
+ mockDebounceFn( newValue );
154
+ } );
155
+
156
+ expect( updateElementSettings ).toHaveBeenCalledWith( {
157
+ id: mockId,
158
+ props: {
159
+ [ mockPropertyName ]: {
160
+ $$type: 'html',
161
+ value: newValue,
162
+ },
163
+ },
164
+ withHistory: true,
165
+ } );
166
+ } );
167
+
168
+ it( 'should save &nbsp; when content is empty', async () => {
169
+ const emptyValue = '';
170
+ const mockDebounceFn = jest.fn();
171
+ const debouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
172
+ mockDebounceFn.mockImplementation( fn );
173
+ return mockDebounceFn;
174
+ } );
175
+ jest.mocked( debounce ).mockImplementation( debouncedFn as unknown as typeof debounce );
176
+
177
+ renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
178
+
179
+ await act( async () => {
180
+ mockDebounceFn( emptyValue );
181
+ } );
182
+
183
+ expect( updateElementSettings ).toHaveBeenCalledWith( {
184
+ id: mockId,
185
+ props: {
186
+ [ mockPropertyName ]: {
187
+ $$type: 'html',
188
+ value: '&nbsp;',
189
+ },
190
+ },
191
+ withHistory: true,
192
+ } );
193
+ } );
194
+
195
+ it( 'should update and display new value after editing', async () => {
196
+ const initialValue = '<p>Initial content</p>';
197
+ const newValue = '<p>Updated content</p>';
198
+
199
+ jest.mocked( useElementSetting ).mockReturnValue( {
200
+ $$type: 'html',
201
+ value: initialValue,
202
+ } );
203
+ jest.mocked( htmlPropTypeUtil.extract ).mockReturnValue( initialValue );
204
+
205
+ const mockDebounceFn = jest.fn();
206
+ const debouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
207
+ mockDebounceFn.mockImplementation( fn );
208
+ return mockDebounceFn;
209
+ } );
210
+ jest.mocked( debounce ).mockImplementation( debouncedFn as unknown as typeof debounce );
211
+
212
+ const { rerender } = renderWithTheme(
213
+ <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } />
214
+ );
215
+
216
+ const input = screen.getByRole( 'textbox', { name: 'inline editor' } ) as HTMLInputElement;
217
+ expect( input ).toHaveValue( initialValue );
218
+
219
+ await act( async () => {
220
+ mockDebounceFn( newValue );
221
+ } );
222
+
223
+ expect( updateElementSettings ).toHaveBeenCalledWith( {
224
+ id: mockId,
225
+ props: {
226
+ [ mockPropertyName ]: {
227
+ $$type: 'html',
228
+ value: newValue,
229
+ },
230
+ },
231
+ withHistory: true,
232
+ } );
233
+
234
+ jest.mocked( useElementSetting ).mockReturnValue( {
235
+ $$type: 'html',
236
+ value: newValue,
237
+ } );
238
+ jest.mocked( htmlPropTypeUtil.extract ).mockReturnValue( newValue );
239
+
240
+ rerender( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
241
+
242
+ const updatedInput = screen.getByRole( 'textbox', { name: 'inline editor' } ) as HTMLInputElement;
243
+ expect( updatedInput ).toHaveValue( newValue );
244
+ } );
245
+ } );
@@ -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; }', breakpoint: 'desktop' },
50
- { id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop' },
49
+ { id: 'style1', value: '.test { color: red; }', breakpoint: 'desktop', state: null },
50
+ { id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop', state: null },
51
51
  ];
52
52
 
53
53
  const mockLinkAttrs = [
@@ -3,11 +3,29 @@ import { getElements, useSelectedElement } from '@elementor/editor-elements';
3
3
  import {
4
4
  __privateUseIsRouteActive as useIsRouteActive,
5
5
  __privateUseListenTo as useListenTo,
6
+ isExperimentActive,
6
7
  useEditMode,
7
8
  windowEvent,
8
9
  } from '@elementor/editor-v1-adapters';
9
10
 
10
- import { ElementOverlay } from './element-overlay';
11
+ import type { ElementOverlayConfig } from '../types/element-overlay';
12
+ import { hasInlineEditableProperty } from '../utils/inline-editing-utils';
13
+ import { InlineEditorOverlay } from './inline-editor-overlay';
14
+ import { OutlineOverlay } from './outline-overlay';
15
+
16
+ const ELEMENTS_DATA_ATTR = 'atomic';
17
+
18
+ const overlayRegistry: ElementOverlayConfig[] = [
19
+ {
20
+ component: OutlineOverlay,
21
+ shouldRender: () => true,
22
+ },
23
+ {
24
+ component: InlineEditorOverlay,
25
+ shouldRender: ( { id, isSelected } ) =>
26
+ isSelected && hasInlineEditableProperty( id ) && isExperimentActive( 'v4-inline-text-editing' ),
27
+ },
28
+ ];
11
29
 
12
30
  export function ElementsOverlays() {
13
31
  const selected = useSelectedElement();
@@ -16,18 +34,23 @@ export function ElementsOverlays() {
16
34
 
17
35
  const isEditMode = currentEditMode === 'edit';
18
36
  const isKitRouteActive = useIsRouteActive( 'panel/global' );
19
-
20
37
  const isActive = isEditMode && ! isKitRouteActive;
21
38
 
22
- return (
23
- isActive &&
24
- elements.map( ( [ id, element ] ) => (
25
- <ElementOverlay key={ id } id={ id } element={ element } isSelected={ selected.element?.id === id } />
26
- ) )
27
- );
28
- }
39
+ if ( ! isActive ) {
40
+ return null;
41
+ }
29
42
 
30
- const ELEMENTS_DATA_ATTR = 'atomic';
43
+ return elements.map( ( [ id, element ] ) => {
44
+ const isSelected = selected.element?.id === id;
45
+
46
+ return overlayRegistry.map(
47
+ ( { shouldRender, component: Overlay }, index ) =>
48
+ shouldRender( { id, element, isSelected } ) && (
49
+ <Overlay key={ `${ id }-${ index }` } id={ id } element={ element } isSelected={ isSelected } />
50
+ )
51
+ );
52
+ } );
53
+ }
31
54
 
32
55
  type IdElementTuple = [ string, HTMLElement ];
33
56