diff --git a/packages/block-library/src/legacy-widget/edit/form.js b/packages/block-library/src/legacy-widget/edit/form.js index 12cf58857f21da..ca2fd3baed575b 100644 --- a/packages/block-library/src/legacy-widget/edit/form.js +++ b/packages/block-library/src/legacy-widget/edit/form.js @@ -14,15 +14,13 @@ import { useRef, useState, useCallback, - forwardRef, RawHTML, } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { Button } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; export default function Form( { id, idBase, instance, setInstance } ) { - const ref = useRef(); - const { html, setFormData } = useForm( { id, idBase, @@ -30,44 +28,18 @@ export default function Form( { id, idBase, instance, setInstance } ) { setInstance, } ); - const onSubmit = useCallback( - ( event ) => { - event.preventDefault(); - if ( id ) { - setFormData( serializeForm( ref.current ) ); - } - }, - [ id ] - ); - - const onChange = useCallback( - debounce( () => { - if ( idBase ) { - setFormData( serializeForm( ref.current ) ); - } - }, 300 ), - [ idBase ] - ); + const setFormDataDebounced = useCallback( debounce( setFormData, 300 ), [ + setFormData, + ] ); return ( -
-
- - { html } - { id && ( - - ) } - -
-
+ ); } @@ -154,26 +126,124 @@ function useForm( { id, idBase, instance, setInstance } ) { return { html, setFormData }; } -function serializeForm( form ) { - return new window.URLSearchParams( - Array.from( new window.FormData( form ) ) - ).toString(); -} +function Control( { id, idBase, html, onChange, onSave } ) { + const controlRef = useRef(); + const formRef = useRef(); -const ObservableForm = forwardRef( ( { onChange, ...props }, ref ) => { - // React won't call the form's onChange handler because it doesn't know - // about the s that we add using dangerouslySetInnerHTML. We work - // around this by not using React's event system. + // Trigger 'widget-added' when widget is ready and 'widget-updated' when + // widget changes. This event is what widgets' scripts use to initialize, + // attach events, etc. The event must be fired using jQuery's event bus as + // this is what widget scripts expect. If jQuery is not loaded, do nothing - + // some widgets will still work regardless. + const hasBeenAdded = useRef( false ); + useEffect( () => { + if ( ! window.jQuery ) { + return; + } + + const { jQuery: $ } = window; + + if ( html ) { + $( document ).trigger( + hasBeenAdded.current ? 'widget-updated' : 'widget-added', + [ $( controlRef.current ) ] + ); + hasBeenAdded.current = true; + } + }, [ + html, + // Include id and idBase in the deps so that widget-updated is triggered + // if they change. + id, + idBase, + ] ); + // Prefer jQuery 'change' event instead of the native 'change' event because + // many widgets use jQuery's event bus to trigger an update. useEffect( () => { - const handler = () => onChange( ref.current ); - ref.current.addEventListener( 'change', handler ); - ref.current.addEventListener( 'input', handler ); - return () => { - ref.current.removeEventListener( 'change', handler ); - ref.current.removeEventListener( 'input', handler ); - }; + const handler = () => onChange( serializeForm( formRef.current ) ); + + if ( window.jQuery ) { + const { jQuery: $ } = window; + $( formRef.current ).on( 'change', null, handler ); + return () => $( formRef.current ).off( 'change', null, handler ); + } + + formRef.current.addEventListener( 'change', handler ); + return () => formRef.current.removeEventListener( 'change', handler ); }, [ onChange ] ); - return
; -} ); + // Non-multi widgets can be saved via a Save button. + const handleSubmit = ( event ) => { + event.preventDefault(); + onSave( serializeForm( event.target ) ); + }; + + // We can't use the real widget number as this is calculated by the server + // and we may not ever *actually* save this widget. Instead, use a fake but + // unique number. + const number = useInstanceId( Control ); + + return ( +
+
+ + { /* Many widgets expect these hidden inputs to exist in the DOM. */ } + + + + + + + { html } + { id && ( + + ) } + +
+
+ ); +} + +function serializeForm( form ) { + return new window.URLSearchParams( + Array.from( new window.FormData( form ) ) + ).toString(); +} diff --git a/packages/e2e-tests/specs/widgets/adding-widgets.test.js b/packages/e2e-tests/specs/widgets/adding-widgets.test.js index 07a3fe242754bb..e227452b39c544 100644 --- a/packages/e2e-tests/specs/widgets/adding-widgets.test.js +++ b/packages/e2e-tests/specs/widgets/adding-widgets.test.js @@ -461,7 +461,12 @@ describe( 'Widgets screen', () => { '[aria-label="Block: Widget Area"][role="group"]' ); - const legacyWidget = await page.waitForSelector( + // Wait for the widget's form to load. + await page.waitForSelector( + '[data-block][data-type="core/legacy-widget"] input' + ); + + const legacyWidget = await page.$( '[data-block][data-type="core/legacy-widget"]' ); @@ -491,8 +496,13 @@ describe( 'Widgets screen', () => { ); await editButton.click(); - // TODO: Should query this with role and label. - const titleInput = await legacyWidget.$( 'input' ); + const [ titleLabel ] = await legacyWidget.$x( + '//label[contains(text(), "Title")]' + ); + const titleInputId = await titleLabel.evaluate( ( node ) => + node.getAttribute( 'for' ) + ); + const titleInput = await page.$( `#${ titleInputId }` ); await titleInput.type( 'Search Title' ); // Trigger the toolbar to appear.