@elementor/editor-ui 3.33.0-207 → 3.33.0-209

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.
package/dist/index.js CHANGED
@@ -676,13 +676,13 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
676
676
  setIsEditing(true);
677
677
  };
678
678
  const closeEditMode = () => {
679
- ref.current?.blur();
680
679
  setError(null);
681
680
  onError?.(null);
682
681
  setIsEditing(false);
683
682
  };
684
683
  const submit = (newValue) => {
685
684
  if (!isDirty(newValue)) {
685
+ closeEditMode();
686
686
  return;
687
687
  }
688
688
  if (!error) {
@@ -708,7 +708,9 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
708
708
  }
709
709
  if (["Enter"].includes(event.key)) {
710
710
  event.preventDefault();
711
- return submit(event.target.innerText);
711
+ if (!error) {
712
+ ref.current?.blur();
713
+ }
712
714
  }
713
715
  };
714
716
  const handleClick = (event) => {
@@ -717,11 +719,18 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
717
719
  }
718
720
  onClick?.(event);
719
721
  };
722
+ const handleBlur = () => {
723
+ if (error) {
724
+ closeEditMode();
725
+ return;
726
+ }
727
+ submit(ref.current.innerText);
728
+ };
720
729
  const listeners = {
721
730
  onClick: handleClick,
722
731
  onKeyDown: handleKeyDown,
723
732
  onInput: onChange,
724
- onBlur: closeEditMode
733
+ onBlur: handleBlur
725
734
  };
726
735
  const attributes = {
727
736
  value,
package/dist/index.mjs CHANGED
@@ -647,13 +647,13 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
647
647
  setIsEditing(true);
648
648
  };
649
649
  const closeEditMode = () => {
650
- ref.current?.blur();
651
650
  setError(null);
652
651
  onError?.(null);
653
652
  setIsEditing(false);
654
653
  };
655
654
  const submit = (newValue) => {
656
655
  if (!isDirty(newValue)) {
656
+ closeEditMode();
657
657
  return;
658
658
  }
659
659
  if (!error) {
@@ -679,7 +679,9 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
679
679
  }
680
680
  if (["Enter"].includes(event.key)) {
681
681
  event.preventDefault();
682
- return submit(event.target.innerText);
682
+ if (!error) {
683
+ ref.current?.blur();
684
+ }
683
685
  }
684
686
  };
685
687
  const handleClick = (event) => {
@@ -688,11 +690,18 @@ var useEditable = ({ value, onSubmit, validation, onClick, onError }) => {
688
690
  }
689
691
  onClick?.(event);
690
692
  };
693
+ const handleBlur = () => {
694
+ if (error) {
695
+ closeEditMode();
696
+ return;
697
+ }
698
+ submit(ref.current.innerText);
699
+ };
691
700
  const listeners = {
692
701
  onClick: handleClick,
693
702
  onKeyDown: handleKeyDown,
694
703
  onInput: onChange,
695
- onBlur: closeEditMode
704
+ onBlur: handleBlur
696
705
  };
697
706
  const attributes = {
698
707
  value,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-ui",
3
3
  "description": "Elementor Editor UI",
4
- "version": "3.33.0-207",
4
+ "version": "3.33.0-209",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,7 +37,7 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor-v1-adapters": "3.33.0-207",
40
+ "@elementor/editor-v1-adapters": "3.33.0-209",
41
41
  "@elementor/icons": "1.53.0",
42
42
  "@elementor/ui": "1.36.15",
43
43
  "@tanstack/react-virtual": "^3.13.3",
@@ -0,0 +1,255 @@
1
+ import * as React from 'react';
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+
4
+ import { EditableField } from '../../components/editable-field';
5
+ import { useEditable } from '../use-editable';
6
+
7
+ // Test utility component that uses the hook with EditableField
8
+ const TestComponent = ( {
9
+ value,
10
+ onSubmit,
11
+ validation,
12
+ onClick,
13
+ onError,
14
+ }: {
15
+ value: string;
16
+ onSubmit: ( value: string ) => unknown;
17
+ validation?: ( value: string ) => string | null;
18
+ onClick?: ( event: React.MouseEvent< HTMLDivElement > ) => void;
19
+ onError?: ( error: string | null ) => void;
20
+ } ) => {
21
+ const { ref, isEditing, openEditMode, error, getProps } = useEditable( {
22
+ value,
23
+ onSubmit,
24
+ validation,
25
+ onClick,
26
+ onError,
27
+ } );
28
+
29
+ return (
30
+ <div>
31
+ <button onClick={ openEditMode } data-testid="open-edit">
32
+ Open Edit
33
+ </button>
34
+ <EditableField ref={ ref } { ...getProps() } data-testid="editable-field" />
35
+ <div data-testid="editing-state">{ isEditing ? 'editing' : 'not-editing' }</div>
36
+ <div data-testid="error-state">{ error || 'no-error' }</div>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ describe( 'useEditable', () => {
42
+ it( 'should return editable content attributes and handlers', () => {
43
+ // Arrange & Act.
44
+ render( <TestComponent value="" onSubmit={ jest.fn() } /> );
45
+
46
+ // Assert.
47
+ const editableField = screen.getByRole( 'textbox' );
48
+
49
+ expect( editableField ).toHaveAttribute( 'role', 'textbox' );
50
+ expect( editableField ).toHaveAttribute( 'contentEditable', 'false' );
51
+ expect( editableField ).toHaveTextContent( '' );
52
+ } );
53
+
54
+ it( 'should set editable to true', () => {
55
+ // Arrange & Act.
56
+ render( <TestComponent value="" onSubmit={ jest.fn() } /> );
57
+
58
+ // Assert.
59
+ const editableField = screen.getByRole( 'textbox' );
60
+ const editingState = screen.getByText( 'not-editing' );
61
+ const openEditButton = screen.getByRole( 'button', { name: 'Open Edit' } );
62
+
63
+ expect( editingState ).toHaveTextContent( 'not-editing' );
64
+ expect( editableField ).toHaveAttribute( 'contentEditable', 'false' );
65
+
66
+ // Act.
67
+ fireEvent.click( openEditButton );
68
+
69
+ // Assert.
70
+ expect( screen.getByText( 'editing' ) ).toBeInTheDocument();
71
+ expect( editableField ).toHaveAttribute( 'contentEditable', 'true' );
72
+ } );
73
+
74
+ it( 'should call onSubmit with the new value on enter', async () => {
75
+ // Arrange.
76
+ const onSubmit = jest.fn();
77
+ const newValue = 'New value';
78
+ const validation = jest.fn().mockReturnValue( null );
79
+
80
+ render( <TestComponent value={ 'Some value' } onSubmit={ onSubmit } validation={ validation } /> );
81
+
82
+ const editableField = screen.getByRole( 'textbox' );
83
+ const openEditButton = screen.getByRole( 'button', { name: 'Open Edit' } );
84
+
85
+ // Mock the blur method to trigger onBlur event
86
+ mockBlur( editableField );
87
+
88
+ // Act.
89
+ fireEvent.click( openEditButton );
90
+
91
+ fireEvent.input( editableField, { target: { innerText: newValue } } );
92
+
93
+ // Assert.
94
+ expect( validation ).toHaveBeenCalledWith( newValue );
95
+
96
+ // Act.
97
+ fireEvent.keyDown( editableField, { key: 'Enter' } );
98
+
99
+ // Assert.
100
+ await waitFor( () => {
101
+ expect( onSubmit ).toHaveBeenCalledWith( newValue );
102
+ } );
103
+ } );
104
+
105
+ it( 'should remove the editable content attribute on blur', () => {
106
+ // Arrange & Act.
107
+ render( <TestComponent value="" onSubmit={ jest.fn() } /> );
108
+
109
+ fireEvent.click( screen.getByRole( 'button', { name: 'Open Edit' } ) );
110
+
111
+ // Assert.
112
+ expect( screen.getByText( 'editing' ) ).toBeInTheDocument();
113
+
114
+ // Act.
115
+ fireEvent.blur( screen.getByRole( 'textbox' ) );
116
+
117
+ // Assert.
118
+ expect( screen.getByText( 'not-editing' ) ).toBeInTheDocument();
119
+ } );
120
+
121
+ it( 'should call onSubmit with the new value on blur', () => {
122
+ // Arrange.
123
+ const onSubmit = jest.fn();
124
+ const newValue = 'New value';
125
+ const validation = jest.fn().mockReturnValue( null );
126
+
127
+ render( <TestComponent value={ 'Some value' } onSubmit={ onSubmit } validation={ validation } /> );
128
+
129
+ const editableField = screen.getByRole( 'textbox' );
130
+
131
+ // Act.
132
+ fireEvent.click( screen.getByRole( 'button', { name: 'Open Edit' } ) );
133
+
134
+ fireEvent.input( editableField, { target: { innerText: newValue } } );
135
+
136
+ // Assert.
137
+ expect( validation ).toHaveBeenCalledWith( newValue );
138
+
139
+ // Act.
140
+ fireEvent.blur( editableField );
141
+
142
+ // Assert.
143
+ expect( onSubmit ).toHaveBeenCalledWith( newValue );
144
+ } );
145
+
146
+ it( 'should set error message id validation fails on enter, and keep the edit mode open', () => {
147
+ // Arrange.
148
+ const newValue = 'invalid-value';
149
+ const onSubmit = jest.fn();
150
+
151
+ const validation = ( v: string ) => {
152
+ if ( v === newValue ) {
153
+ return 'Nope';
154
+ }
155
+
156
+ return null;
157
+ };
158
+
159
+ render( <TestComponent value={ 'Some value' } onSubmit={ onSubmit } validation={ validation } /> );
160
+
161
+ // Act.
162
+ fireEvent.click( screen.getByRole( 'button', { name: 'Open Edit' } ) );
163
+
164
+ // Assert.
165
+ expect( screen.getByText( 'no-error' ) ).toBeInTheDocument();
166
+
167
+ // Act.
168
+ const editableField = screen.getByRole( 'textbox' );
169
+ fireEvent.input( editableField, { target: { innerText: newValue } } );
170
+
171
+ // Assert.
172
+ expect( screen.getByText( 'Nope' ) ).toBeInTheDocument();
173
+
174
+ // Act.
175
+ fireEvent.keyDown( editableField, { key: 'Enter' } );
176
+
177
+ // Assert.
178
+ expect( onSubmit ).not.toHaveBeenCalled();
179
+ expect( editableField ).toHaveAttribute( 'contentEditable', 'true' );
180
+ } );
181
+
182
+ it( 'should not run validation & submit if the value has not changed', () => {
183
+ // Arrange.
184
+ const value = 'initial value';
185
+ const onSubmit = jest.fn();
186
+
187
+ const validation = () => {
188
+ return 'test-error';
189
+ };
190
+
191
+ render( <TestComponent value={ value } onSubmit={ onSubmit } validation={ validation } /> );
192
+
193
+ const editableField = screen.getByRole( 'textbox' );
194
+ const openEditButton = screen.getByRole( 'button', { name: 'Open Edit' } );
195
+
196
+ // Act.
197
+ fireEvent.click( openEditButton );
198
+
199
+ // Assert.
200
+ expect( screen.getByText( 'no-error' ) ).toBeInTheDocument();
201
+
202
+ // Act.
203
+ fireEvent.input( editableField, { target: { innerText: 'new value' } } );
204
+
205
+ // Assert.
206
+ expect( screen.getByText( 'test-error' ) ).toBeInTheDocument();
207
+
208
+ // Act.
209
+ fireEvent.input( editableField, { target: { innerText: value } } );
210
+
211
+ // Assert.
212
+ expect( screen.getByText( 'no-error' ) ).toBeInTheDocument();
213
+
214
+ // Act.
215
+ fireEvent.keyDown( editableField, { key: 'Enter' } );
216
+
217
+ // Assert.
218
+ expect( onSubmit ).not.toHaveBeenCalled();
219
+ } );
220
+
221
+ it( 'should not submit, and only close the edit mode on blur if there is an error', () => {
222
+ // Arrange.
223
+ const onSubmit = jest.fn();
224
+ const newValue = 'invalid-value';
225
+ const validation = jest.fn().mockReturnValue( 'Nope' );
226
+
227
+ render( <TestComponent value={ 'Some value' } onSubmit={ onSubmit } validation={ validation } /> );
228
+
229
+ const editableField = screen.getByRole( 'textbox' );
230
+ const openEditButton = screen.getByRole( 'button', { name: 'Open Edit' } );
231
+
232
+ // Act.
233
+ fireEvent.click( openEditButton );
234
+
235
+ fireEvent.input( editableField, { target: { innerText: newValue } } );
236
+
237
+ // Assert.
238
+ expect( validation ).toHaveBeenCalledWith( newValue );
239
+
240
+ // Act.
241
+ fireEvent.blur( editableField );
242
+
243
+ // Assert.
244
+ expect( onSubmit ).not.toHaveBeenCalled();
245
+ expect( screen.getByText( 'not-editing' ) ).toBeInTheDocument();
246
+ } );
247
+ } );
248
+
249
+ function mockBlur( element: HTMLElement ) {
250
+ const originalBlur = element.blur;
251
+ element.blur = jest.fn( () => {
252
+ originalBlur.call( element );
253
+ fireEvent.blur( element );
254
+ } );
255
+ }
@@ -21,8 +21,6 @@ export const useEditable = ( { value, onSubmit, validation, onClick, onError }:
21
21
  };
22
22
 
23
23
  const closeEditMode = () => {
24
- ref.current?.blur();
25
-
26
24
  setError( null );
27
25
  onError?.( null );
28
26
  setIsEditing( false );
@@ -30,6 +28,7 @@ export const useEditable = ( { value, onSubmit, validation, onClick, onError }:
30
28
 
31
29
  const submit = ( newValue: string ) => {
32
30
  if ( ! isDirty( newValue ) ) {
31
+ closeEditMode();
33
32
  return;
34
33
  }
35
34
 
@@ -62,7 +61,10 @@ export const useEditable = ( { value, onSubmit, validation, onClick, onError }:
62
61
 
63
62
  if ( [ 'Enter' ].includes( event.key ) ) {
64
63
  event.preventDefault();
65
- return submit( ( event.target as HTMLElement ).innerText );
64
+ // submission is invoked only on blur, to avoid issues with double-submission in certain cases
65
+ if ( ! error ) {
66
+ ref.current?.blur();
67
+ }
66
68
  }
67
69
  };
68
70
 
@@ -74,11 +76,20 @@ export const useEditable = ( { value, onSubmit, validation, onClick, onError }:
74
76
  onClick?.( event );
75
77
  };
76
78
 
79
+ const handleBlur = () => {
80
+ if ( error ) {
81
+ closeEditMode();
82
+ return;
83
+ }
84
+
85
+ submit( ( ref.current as HTMLElement ).innerText );
86
+ };
87
+
77
88
  const listeners = {
78
89
  onClick: handleClick,
79
90
  onKeyDown: handleKeyDown,
80
91
  onInput: onChange,
81
- onBlur: closeEditMode,
92
+ onBlur: handleBlur,
82
93
  } as const;
83
94
 
84
95
  const attributes = {
@@ -1,209 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react';
2
-
3
- import { useEditable } from '../use-editable';
4
-
5
- describe( 'useEditable', () => {
6
- it( 'should return editable content attributes and handlers', () => {
7
- // Arrange & Act.
8
- const { result } = renderHook( () =>
9
- useEditable( {
10
- value: '',
11
- onSubmit: jest.fn(),
12
- } )
13
- );
14
-
15
- const props = result.current.getProps();
16
-
17
- // Assert.
18
- expect( props ).toEqual( {
19
- value: '',
20
- role: 'textbox',
21
- contentEditable: false,
22
- suppressContentEditableWarning: undefined,
23
- onBlur: expect.any( Function ),
24
- onClick: expect.any( Function ),
25
- onInput: expect.any( Function ),
26
- onKeyDown: expect.any( Function ),
27
- } );
28
- } );
29
-
30
- it( 'should set editable to true', () => {
31
- // Arrange & Act.
32
- const { result } = renderHook( () => useEditable( { value: '', onSubmit: jest.fn() } ) );
33
-
34
- // Assert.
35
- expect( result.current.isEditing ).toBe( false );
36
-
37
- // Act.
38
- act( result.current.openEditMode );
39
-
40
- // Assert.
41
- expect( result.current.isEditing ).toBe( true );
42
- expect( result.current.getProps() ).toMatchObject( {
43
- contentEditable: true,
44
- suppressContentEditableWarning: true,
45
- } );
46
- } );
47
-
48
- it( 'should call onSubmit with the new value on enter', async () => {
49
- // Arrange.
50
- const onSubmit = jest.fn();
51
- const value = 'Some value';
52
- const newValue = 'New value';
53
- const validation = jest.fn();
54
-
55
- // Act.
56
- const { result } = renderHook( () =>
57
- useEditable( {
58
- value,
59
- onSubmit,
60
- validation,
61
- } )
62
- );
63
-
64
- act( () => {
65
- result.current.openEditMode();
66
- result.current.getProps().onInput( { target: { innerText: newValue } } as never );
67
- } );
68
-
69
- // Assert.
70
- expect( validation ).toHaveBeenCalledWith( newValue );
71
-
72
- // Act.
73
- const { onKeyDown } = result.current.getProps();
74
-
75
- act( () => {
76
- onKeyDown( {
77
- key: 'Enter',
78
- stopPropagation: jest.fn(),
79
- preventDefault: jest.fn(),
80
- target: { innerText: newValue },
81
- } as never );
82
- } );
83
-
84
- expect( onSubmit ).toHaveBeenCalledWith( newValue );
85
- } );
86
-
87
- it( 'should remove the editable content attribute on blur', () => {
88
- // Arrange
89
- const { result } = renderHook( () =>
90
- useEditable( {
91
- value: '',
92
- onSubmit: jest.fn(),
93
- } )
94
- );
95
-
96
- // Act.
97
- act( result.current.openEditMode );
98
-
99
- // Assert.
100
- expect( result.current.isEditing ).toBe( true );
101
-
102
- // Act.
103
- act( result.current.getProps().onBlur );
104
-
105
- // Assert.
106
- expect( result.current.isEditing ).toBe( false );
107
- } );
108
-
109
- it( 'should set error message id validation fails', () => {
110
- // Arrange.
111
- const newValue = 'invalid-value';
112
- const value = 'Some value';
113
- const onSubmit = jest.fn();
114
-
115
- const validation = ( v: string ) => {
116
- if ( v === newValue ) {
117
- return 'Nope';
118
- }
119
-
120
- return null;
121
- };
122
-
123
- const { result } = renderHook( () =>
124
- useEditable( {
125
- value,
126
- onSubmit,
127
- validation,
128
- } )
129
- );
130
-
131
- // Act.
132
- act( result.current.openEditMode );
133
-
134
- // Assert.
135
- expect( result.current.error ).toBeNull();
136
-
137
- // Act.
138
- act( () => {
139
- result.current.getProps().onInput( { target: { innerText: newValue } } as never );
140
- } );
141
-
142
- // Assert.
143
- expect( result.current.error ).toBe( 'Nope' );
144
-
145
- // Act.
146
- act( () => {
147
- result.current.getProps().onKeyDown( {
148
- key: 'Enter',
149
- stopPropagation: jest.fn(),
150
- preventDefault: jest.fn(),
151
- target: { innerText: newValue },
152
- } as never );
153
- } );
154
-
155
- // Assert.
156
- expect( onSubmit ).not.toHaveBeenCalled();
157
- } );
158
-
159
- it( 'should not run validation & submit if the value has not changed', () => {
160
- // Arrange.
161
- const value = 'initial value';
162
- const onSubmit = jest.fn();
163
-
164
- const validation = () => {
165
- return 'test-error';
166
- };
167
-
168
- const { result } = renderHook( () =>
169
- useEditable( {
170
- value,
171
- onSubmit,
172
- validation,
173
- } )
174
- );
175
-
176
- // Act.
177
- act( result.current.openEditMode );
178
-
179
- // Assert.
180
- expect( result.current.error ).toBeNull();
181
-
182
- // Act.
183
- act( () => {
184
- result.current.getProps().onInput( { target: { innerText: 'new value' } } as never );
185
- } );
186
-
187
- // Assert.
188
- expect( result.current.error ).toBe( 'test-error' );
189
-
190
- act( () => {
191
- result.current.getProps().onInput( { target: { innerText: value } } as never );
192
- } );
193
-
194
- expect( result.current.error ).toBe( null );
195
-
196
- // Act.
197
- act( () => {
198
- result.current.getProps().onKeyDown( {
199
- key: 'Enter',
200
- stopPropagation: jest.fn(),
201
- preventDefault: jest.fn(),
202
- target: { innerText: value },
203
- } as never );
204
- } );
205
-
206
- // Assert.
207
- expect( onSubmit ).not.toHaveBeenCalled();
208
- } );
209
- } );