diff --git a/projects/packages/forms/changelog/add-core-button-submit b/projects/packages/forms/changelog/add-core-button-submit new file mode 100644 index 0000000000000..4d4cc73ceb6ca --- /dev/null +++ b/projects/packages/forms/changelog/add-core-button-submit @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Forms: allow using the Gutenberg Core Button block as the submit control in Contact Form. The block gets the same interactivity bindings and loading spinner as the Jetpack Button, and all form variations now insert a core button by default. diff --git a/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php b/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php index 9b62c58348e0e..75d9a65a9df2c 100644 --- a/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php +++ b/projects/packages/forms/src/blocks/contact-form/class-contact-form-block.php @@ -47,6 +47,7 @@ public static function register_block() { add_filter( 'render_block_data', array( __CLASS__, 'find_nested_html_block' ), 10, 3 ); add_filter( 'render_block_core/html', array( __CLASS__, 'render_wrapped_html_block' ), 10, 2 ); + add_filter( 'render_block_core/button', array( __CLASS__, 'render_submit_button' ), 10, 2 ); add_filter( 'jetpack_block_editor_feature_flags', array( __CLASS__, 'register_feature' ) ); add_filter( 'pre_render_block', array( __CLASS__, 'pre_render_contact_form' ), 10, 3 ); @@ -101,6 +102,35 @@ public static function render_wrapped_html_block( $content, $parsed_block ) { return $content; } + /** + * Add Jetpack Forms interactivity attributes to core/buttons blocks that live inside a contact form. + * + * @param string $content Rendered HTML of the core/button block. + * @param array $parsed_block Parsed block array. + * @return string Possibly modified HTML. + */ + public static function render_submit_button( $content, $parsed_block ) { + $class = $parsed_block['attrs']['className'] ?? ''; + if ( ! str_contains( $class, 'jetpack-form-submit-button' ) ) { + return $content; + } + + if ( ! class_exists( \WP_HTML_Tag_Processor::class ) ) { + return $content; + } + + $p = new \WP_HTML_Tag_Processor( $content ); + if ( ! $p->next_tag( array( 'tag_name' => 'button' ) ) ) { + return $content; + } + + $p->set_attribute( 'data-wp-class--is-submitting', 'state.isSubmitting' ); + $p->set_attribute( 'data-wp-bind--aria-disabled', 'state.isAriaDisabled' ); + $p->set_attribute( 'data-wp-bind--disabled', 'state.isAriaDisabled' ); + + return $p->get_updated_html(); + } + /** * Register the Child blocks of Contact Form * We are registering child blocks only when Contact Form plugin is Active diff --git a/projects/packages/forms/src/blocks/contact-form/edit.tsx b/projects/packages/forms/src/blocks/contact-form/edit.tsx index f2c25a3ea9546..2555388ba2274 100644 --- a/projects/packages/forms/src/blocks/contact-form/edit.tsx +++ b/projects/packages/forms/src/blocks/contact-form/edit.tsx @@ -197,11 +197,13 @@ function JetpackContactFormEdit( { [ clientId, steps ] ); - const submitButton = useFindBlockRecursively( - clientId, - block => block.name === 'jetpack/button' + const findButtonsBlock = useCallback( + block => block.name === 'core/buttons' || block.name === 'jetpack/button', + [] ); + const submitButton = useFindBlockRecursively( clientId, findButtonsBlock ); + const { postTitle, hasAnyInnerBlocks, postAuthorEmail, selectedBlockClientId, onlySubmitBlock } = useSelect( select => { @@ -222,6 +224,11 @@ function JetpackContactFormEdit( { const { getUser } = select( coreStore ); const innerBlocksData = getBlocks( clientId ); + const isSingleButtonBlock = + innerBlocksData.length === 1 && + ( innerBlocksData[ 0 ].name === 'core/buttons' || + innerBlocksData[ 0 ].name === 'jetpack/button' ); + const title = getEditedPostAttribute( 'title' ); const authorId = getEditedPostAttribute( 'author' ); const authorEmail = authorId && getUser( authorId )?.email; @@ -231,8 +238,7 @@ function JetpackContactFormEdit( { hasAnyInnerBlocks: innerBlocksData.length > 0, postAuthorEmail: authorEmail, selectedBlockClientId: selectedStepBlockId, - onlySubmitBlock: - innerBlocksData.length === 1 && innerBlocksData[ 0 ].name === 'jetpack/button', + onlySubmitBlock: isSingleButtonBlock, }; }, [ clientId ] @@ -330,7 +336,7 @@ function JetpackContactFormEdit( { // Find the submit button const submitButtonIndex = currentInnerBlocks.findIndex( block => - block.name === 'jetpack/button' && + ( block.name === 'core/buttons' || block.name === 'jetpack/button' ) && ( block.attributes?.customVariant === 'submit' || block.attributes?.element === 'button' ) ); @@ -465,7 +471,9 @@ function JetpackContactFormEdit( { // Helper functions const findButtonBlock = () => { - const buttonIndex = currentInnerBlocks.findIndex( block => block.name === 'jetpack/button' ); + const buttonIndex = currentInnerBlocks.findIndex( + block => block.name === 'core/buttons' || block.name === 'jetpack/button' + ); return buttonIndex !== -1 ? { block: currentInnerBlocks[ buttonIndex ], @@ -706,10 +714,14 @@ function JetpackContactFormEdit( { // Ensure we have a submit button at the end of the form. if ( ! finalSubmitButton ) { // Create a fresh submit button if none was found. - finalSubmitButton = createBlock( 'jetpack/button', { - element: 'button', - text: __( 'Submit', 'jetpack-forms' ), - } ); + finalSubmitButton = createBlock( 'core/buttons', {}, [ + createBlock( 'core/button', { + text: __( 'Submit', 'jetpack-forms' ), + type: 'submit', + tagName: 'button', + className: 'wp-block-jetpack-button jetpack-form-submit-button', + } ), + ] ); } const finalBlocks = [ ...flattenedInnerBlocks, finalSubmitButton ]; diff --git a/projects/packages/forms/src/blocks/contact-form/editor.scss b/projects/packages/forms/src/blocks/contact-form/editor.scss index 458bd5b86256c..053a4e3635495 100644 --- a/projects/packages/forms/src/blocks/contact-form/editor.scss +++ b/projects/packages/forms/src/blocks/contact-form/editor.scss @@ -54,8 +54,8 @@ flex: 1 1 100%; } - .wp-block { - flex: 1 1 100%; + .wp-block:not(.wp-block-buttons):not(.wp-block-button) { + flex: 0 0 100%; margin-top: 0; margin-bottom: 0; max-width: 100%; diff --git a/projects/packages/forms/src/blocks/contact-form/variations.js b/projects/packages/forms/src/blocks/contact-form/variations.js index ee870c0bfcaf1..1126fba5e5cf4 100644 --- a/projects/packages/forms/src/blocks/contact-form/variations.js +++ b/projects/packages/forms/src/blocks/contact-form/variations.js @@ -66,12 +66,20 @@ const variations = [ ], ], [ - 'jetpack/button', - { - text: __( 'Contact Us', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - }, + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Contact us', 'jetpack-forms' ), + tagName: 'button', + className: 'jetpack-form-submit-button', + type: 'submit', + element: 'button', + }, + ], + ], ], ], attributes: { @@ -145,12 +153,20 @@ const variations = [ ], ], [ - 'jetpack/button', - { - text: __( 'Send RSVP', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - }, + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Send RSVP', 'jetpack-forms' ), + tagName: 'button', + className: 'jetpack-form-submit-button', + type: 'submit', + element: 'button', + }, + ], + ], ], ], attributes: { @@ -298,12 +314,20 @@ const variations = [ ], ], [ - 'jetpack/button', - { - text: __( 'Send', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - }, + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Send', 'jetpack-forms' ), + tagName: 'button', + className: 'jetpack-form-submit-button', + type: 'submit', + element: 'button', + }, + ], + ], ], ], attributes: { @@ -487,12 +511,20 @@ const variations = [ ], ], [ - 'jetpack/button', - { - text: __( 'Book appointment', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - }, + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Book appointment', 'jetpack-forms' ), + tagName: 'button', + className: 'jetpack-form-submit-button', + type: 'submit', + element: 'button', + }, + ], + ], ], ], attributes: { @@ -633,12 +665,20 @@ const variations = [ ], ], [ - 'jetpack/button', - { - text: __( 'Send Feedback', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - }, + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Send feedback', 'jetpack-forms' ), + tagName: 'button', + className: 'jetpack-form-submit-button', + type: 'submit', + element: 'button', + }, + ], + ], ], ], attributes: { @@ -956,12 +996,20 @@ const variations = [ [ [ 'jetpack/label' ], [ 'jetpack/input', { type: 'checkbox' } ] ], ], [ - 'jetpack/button', - { - text: __( 'Subscribe', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - }, + 'core/buttons', + {}, + [ + [ + 'core/button', + { + text: __( 'Subscribe', 'jetpack-forms' ), + tagName: 'button', + className: 'jetpack-form-submit-button', + type: 'submit', + element: 'button', + }, + ], + ], ], ], attributes: {}, diff --git a/projects/packages/forms/src/blocks/shared/hooks/use-form-wrapper.js b/projects/packages/forms/src/blocks/shared/hooks/use-form-wrapper.js index ef4b599663420..9eec92b14bee9 100644 --- a/projects/packages/forms/src/blocks/shared/hooks/use-form-wrapper.js +++ b/projects/packages/forms/src/blocks/shared/hooks/use-form-wrapper.js @@ -25,11 +25,14 @@ export default function useFormWrapper( { attributes, clientId, name } ) { clientId, createBlock( FORM_BLOCK_NAME, {}, [ createBlock( name, attributes, getBlocks( clientId ) ), - createBlock( 'jetpack/button', { - text: __( 'Submit', 'jetpack-forms' ), - element: 'button', - lock: { remove: true }, - } ), + createBlock( 'core/buttons', { lock: { remove: true } }, [ + createBlock( 'core/button', { + text: __( 'Submit', 'jetpack-forms' ), + type: 'submit', + tagName: 'button', + className: 'wp-block-jetpack-button jetpack-form-submit-button', + } ), + ] ), ] ) ); } diff --git a/projects/packages/forms/src/contact-form/class-contact-form.php b/projects/packages/forms/src/contact-form/class-contact-form.php index ec4f9b23928de..ee687e5ee690f 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form.php +++ b/projects/packages/forms/src/contact-form/class-contact-form.php @@ -1049,7 +1049,7 @@ class='{$container_classes_string}' * @param int $id Contact Form ID. */ $url = apply_filters( 'grunion_contact_form_form_action', $url, $GLOBALS['post'], $id, $page ); - $has_submit_button_block = str_contains( $content, 'wp-block-jetpack-button' ); + $has_submit_button_block = str_contains( $content, 'wp-block-jetpack-button' ) || str_contains( $content, 'wp-block-buttons' ); $form_classes = 'contact-form commentsblock'; if ( $submission_success ) { $form_classes .= ' submission-success'; @@ -1084,8 +1084,11 @@ class='" . esc_attr( $form_classes ) . "' $form_aria_label $r = preg_replace( '/
- + +
+ +
+
+
', ), @@ -69,7 +74,12 @@ public static function register_pattern() { - + +
+ +
+
+ ', ), @@ -83,7 +93,12 @@ public static function register_pattern() { - + +
+ +
+
+ ', ), @@ -98,7 +113,12 @@ public static function register_pattern() { - + +
+ +
+
+ ', ), @@ -114,7 +134,12 @@ public static function register_pattern() { - + +
+ +
+
+ ', ), @@ -131,7 +156,12 @@ public static function register_pattern() { - + +
+ +
+
+ ', ), @@ -147,7 +177,12 @@ public static function register_pattern() { - + +
+ +
+
+ ', ), diff --git a/projects/packages/forms/src/contact-form/css/grunion.scss b/projects/packages/forms/src/contact-form/css/grunion.scss index a51cd8a566fa7..27b27f2fef8ab 100644 --- a/projects/packages/forms/src/contact-form/css/grunion.scss +++ b/projects/packages/forms/src/contact-form/css/grunion.scss @@ -1307,3 +1307,22 @@ on production builds, the attributes are being reordered, causing side-effects .contact-form-ajax-submission:not(.submission-success) { display: none; } + +.jetpack-form-submit-button button.is-submitting::after { + content: ""; + display: inline-block; + width: 1em; + height: 1em; + margin-left: 0.5em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: jp-spinner 0.75s linear infinite; +} + +@keyframes jp-spinner { + + to { + transform: rotate(360deg); + } +} diff --git a/projects/packages/forms/tests/php/contact-form/Contact_Form_Block_Test.php b/projects/packages/forms/tests/php/contact-form/Contact_Form_Block_Test.php index cb458e0a4a65f..5d1d768b22ce1 100644 --- a/projects/packages/forms/tests/php/contact-form/Contact_Form_Block_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Contact_Form_Block_Test.php @@ -150,4 +150,283 @@ public static function data_provider_test_register_child_blocks() { ), ); } + + /** + * Test that ::render_submit_button adds interactivity attributes to submit buttons. + * + * @dataProvider data_provider_test_render_submit_button + */ + #[DataProvider( 'data_provider_test_render_submit_button' )] + public function test_render_submit_button( $content, $parsed_block, $expected ) { + $result = Contact_Form_Block::render_submit_button( $content, $parsed_block ); + $this->assertEquals( $expected, $result ); + } + + /** + * Data provider for test_render_submit_button. + */ + public static function data_provider_test_render_submit_button() { + // Skip HTML Tag Processor tests if the class doesn't exist (WordPress < 6.2) + if ( ! class_exists( \WP_HTML_Tag_Processor::class ) ) { + return array( + 'no html tag processor' => array( + '', + array( 'attrs' => array( 'className' => 'jetpack-form-submit-button' ) ), + '', + ), + ); + } + + return array( + 'submit button with jetpack class' => array( + '', + array( 'attrs' => array( 'className' => 'jetpack-form-submit-button' ) ), + '', + ), + 'button without jetpack class' => array( + '', + array( 'attrs' => array( 'className' => 'wp-block-button__link' ) ), + '', + ), + 'submit button with multiple classes including jetpack' => array( + '', + array( 'attrs' => array( 'className' => 'my-custom-class jetpack-form-submit-button another-class' ) ), + '', + ), + 'block with no className attribute' => array( + '', + array( 'attrs' => array() ), + '', + ), + 'block with empty className' => array( + '', + array( 'attrs' => array( 'className' => '' ) ), + '', + ), + 'content without button tag' => array( + '
Not a button
', + array( 'attrs' => array( 'className' => 'jetpack-form-submit-button' ) ), + '
Not a button
', + ), + ); + } + + /** + * Test that ::render_wrapped_html_block wraps HTML blocks with jetpack form parent. + */ + public function test_render_wrapped_html_block() { + $content = '

Some HTML content

'; + + // Test with hasJPFormParent flag + $parsed_block_with_parent = array( 'hasJPFormParent' => true ); + $result = Contact_Form_Block::render_wrapped_html_block( $content, $parsed_block_with_parent ); + $this->assertEquals( '

Some HTML content

', $result ); + + // Test without hasJPFormParent flag + $parsed_block_without_parent = array(); + $result = Contact_Form_Block::render_wrapped_html_block( $content, $parsed_block_without_parent ); + $this->assertEquals( '

Some HTML content

', $result ); + + // Test with hasJPFormParent set to false + $parsed_block_false_parent = array( 'hasJPFormParent' => false ); + $result = Contact_Form_Block::render_wrapped_html_block( $content, $parsed_block_false_parent ); + $this->assertEquals( '

Some HTML content

', $result ); + } + + /** + * Test that ::register_feature adds multistep-form feature. + */ + public function test_register_feature() { + $input_features = array( 'existing-feature' => true ); + + // We can't easily mock static methods, so we'll test the structure + $result = Contact_Form_Block::register_feature( $input_features ); + + // Should preserve existing features + $this->assertTrue( $result['existing-feature'] ); + + // Should add multistep-form feature + $this->assertArrayHasKey( 'multistep-form', $result ); + $this->assertIsBool( $result['multistep-form'] ); + } + + /** + * Test form step counting functionality. + * + * @dataProvider data_provider_test_form_step_counting + */ + #[DataProvider( 'data_provider_test_form_step_counting' )] + public function test_form_step_counting( $block_structure, $expected_steps ) { + // Use reflection to access private method + $reflection = new \ReflectionClass( Contact_Form_Block::class ); + $count_method = $reflection->getMethod( 'count_form_steps_in_block' ); + $count_method->setAccessible( true ); + + $result = $count_method->invoke( null, $block_structure ); + $this->assertEquals( $expected_steps, $result ); + } + + /** + * Data provider for form step counting tests. + */ + public static function data_provider_test_form_step_counting() { + return array( + 'no inner blocks' => array( + array( + 'blockName' => 'jetpack/contact-form', + 'innerBlocks' => array(), + ), + 0, + ), + 'single form step' => array( + array( + 'blockName' => 'jetpack/contact-form', + 'innerBlocks' => array( + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + ), + ), + 1, + ), + 'multiple form steps' => array( + array( + 'blockName' => 'jetpack/contact-form', + 'innerBlocks' => array( + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + ), + ), + 3, + ), + 'nested form steps in container' => array( + array( + 'blockName' => 'jetpack/contact-form', + 'innerBlocks' => array( + array( + 'blockName' => 'jetpack/form-step-container', + 'innerBlocks' => array( + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + ), + ), + ), + ), + 2, + ), + 'mixed blocks with form steps' => array( + array( + 'blockName' => 'jetpack/contact-form', + 'innerBlocks' => array( + array( + 'blockName' => 'jetpack/field-text', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/field-email', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + ), + ), + 2, + ), + ); + } + + /** + * Test pre_render_contact_form hook processing. + */ + public function test_pre_render_contact_form() { + $contact_form_block = array( + 'blockName' => 'jetpack/contact-form', + 'innerBlocks' => array( + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + array( + 'blockName' => 'jetpack/form-step', + 'innerBlocks' => array(), + ), + ), + ); + + $other_block = array( + 'blockName' => 'core/paragraph', + 'innerBlocks' => array(), + ); + + // Test that it returns null for non-contact-form blocks + $result = Contact_Form_Block::pre_render_contact_form( null, $other_block ); + $this->assertNull( $result ); + + // Test that it processes contact form blocks and returns null (lets normal rendering continue) + $result = Contact_Form_Block::pre_render_contact_form( null, $contact_form_block ); + $this->assertNull( $result ); + + // Test that step count is updated after processing + $step_count = Contact_Form_Block::get_form_step_count(); + $this->assertEquals( 2, $step_count ); + } + + /** + * Test get_form_step_count method. + */ + public function test_get_form_step_count() { + // Use reflection to set the private static property for testing + $reflection = new \ReflectionClass( Contact_Form_Block::class ); + $step_count_property = $reflection->getProperty( 'form_step_count' ); + $step_count_property->setAccessible( true ); + $step_count_property->setValue( null, 5 ); + + $result = Contact_Form_Block::get_form_step_count(); + $this->assertEquals( 5, $result ); + + // Reset to default + $step_count_property->setValue( null, 1 ); + } + + /** + * Test can_manage_block method behavior. + */ + public function test_can_manage_block() { + // Test the filter override + add_filter( 'jetpack_contact_form_can_manage_block', '__return_true' ); + $this->assertTrue( Contact_Form_Block::can_manage_block() ); + remove_filter( 'jetpack_contact_form_can_manage_block', '__return_true' ); + + add_filter( 'jetpack_contact_form_can_manage_block', '__return_false' ); + + // When not in Jetpack context (class doesn't exist), should return true + if ( ! class_exists( 'Jetpack' ) ) { + $this->assertTrue( Contact_Form_Block::can_manage_block() ); + } + + remove_filter( 'jetpack_contact_form_can_manage_block', '__return_false' ); + } }