@elementor/editor-canvas 3.35.0-351 → 3.35.0-353

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.
@@ -1,245 +0,0 @@
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
- } );
@@ -1,79 +0,0 @@
1
- import * as React from 'react';
2
- import { InlineEditor } from '@elementor/editor-controls';
3
- import { getContainer, updateElementSettings, useElementSetting } from '@elementor/editor-elements';
4
- import { htmlPropTypeUtil } from '@elementor/editor-props';
5
- import { Box } from '@elementor/ui';
6
- import { debounce } from '@elementor/utils';
7
- import { FloatingPortal } from '@floating-ui/react';
8
-
9
- import { useFloatingOnElement } from '../hooks/use-floating-on-element';
10
- import type { ElementOverlayProps } from '../types/element-overlay';
11
- import { getInlineEditablePropertyName } from '../utils/inline-editing-utils';
12
- import { CANVAS_WRAPPER_ID } from './outline-overlay';
13
-
14
- const OVERLAY_Z_INDEX = 1000;
15
- const DEBOUNCE_DELAY = 100;
16
-
17
- export const InlineEditorOverlay = ( { element, isSelected, id }: ElementOverlayProps ): React.ReactElement | null => {
18
- const { floating, isVisible } = useFloatingOnElement( { element, isSelected } );
19
-
20
- const propertyName = React.useMemo( () => {
21
- const container = getContainer( id );
22
- return getInlineEditablePropertyName( container );
23
- }, [ id ] );
24
-
25
- const contentProp = useElementSetting( id, propertyName );
26
- const value = React.useMemo( () => htmlPropTypeUtil.extract( contentProp ) || '', [ contentProp ] );
27
-
28
- const debouncedUpdateRef = React.useRef< ReturnType< typeof debounce > | null >( null );
29
- const lastValueRef = React.useRef< string >( '' );
30
-
31
- React.useEffect( () => {
32
- debouncedUpdateRef.current = debounce( ( newValue: string ) => {
33
- const textContent = newValue.replace( /<[^>]*>/g, '' ).trim();
34
- const valueToSave = textContent === '' ? '&nbsp;' : newValue;
35
-
36
- updateElementSettings( {
37
- id,
38
- props: {
39
- [ propertyName ]: htmlPropTypeUtil.create( valueToSave ),
40
- },
41
- withHistory: true,
42
- } );
43
- }, DEBOUNCE_DELAY );
44
-
45
- return () => {
46
- debouncedUpdateRef.current?.cancel?.();
47
- };
48
- }, [ id, propertyName ] );
49
-
50
- const handleValueChange = React.useCallback( ( newValue: string ) => {
51
- lastValueRef.current = newValue;
52
- debouncedUpdateRef.current?.( newValue );
53
- }, [] );
54
-
55
- React.useEffect( () => {
56
- if ( ! isVisible && debouncedUpdateRef.current?.pending?.() ) {
57
- debouncedUpdateRef.current.flush( lastValueRef.current );
58
- }
59
- }, [ isVisible ] );
60
-
61
- if ( ! isVisible ) {
62
- return null;
63
- }
64
-
65
- return (
66
- <FloatingPortal id={ CANVAS_WRAPPER_ID }>
67
- <Box
68
- ref={ floating.setRef }
69
- style={ {
70
- ...floating.styles,
71
- zIndex: OVERLAY_Z_INDEX,
72
- pointerEvents: 'auto',
73
- } }
74
- >
75
- <InlineEditor value={ value } setValue={ handleValueChange } showToolbar={ isSelected } />
76
- </Box>
77
- </FloatingPortal>
78
- );
79
- };