Commit abd1b70085 for woocommerce
commit abd1b7008512ed761c2bd2c1a083533376959145
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Wed Sep 17 16:23:13 2025 +0200
Add to Cart + Options block: trigger legacy mode only if PHP hooks are used to add form inner elements (#60915)
* Add changelog file
* Add to Cart + Options block: trigger legacy mode only if PHP hooks are used to add form inner elements
* Add tests
* Typo
diff --git a/plugins/woocommerce/changelog/fix-add-to-cart-with-options-legacy-mode-on-form-elements b/plugins/woocommerce/changelog/fix-add-to-cart-with-options-legacy-mode-on-form-elements
new file mode 100644
index 0000000000..2519b39212
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-add-to-cart-with-options-legacy-mode-on-form-elements
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Add to Cart + Options block: trigger legacy mode only if PHP hooks are used to add form inner elements
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
index 79fc7e944a..acb12f5192 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
@@ -82,6 +82,23 @@ class AddToCartWithOptions extends AbstractBlock {
return $context;
}
+ /**
+ * Check if HTML content has form elements.
+ *
+ * @param string $html_content The HTML content.
+ * @return bool True if the HTML content has form elements, false otherwise.
+ */
+ public function has_form_elements( $html_content ) {
+ $processor = new \WP_HTML_Tag_Processor( $html_content );
+ $form_elements = array( 'INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'FORM' );
+ while ( $processor->next_tag() ) {
+ if ( in_array( $processor->get_tag(), $form_elements, true ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Check if a child product is purchasable.
*
@@ -502,11 +519,11 @@ class AddToCartWithOptions extends AbstractBlock {
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' );
$form_attributes = '';
- $legacy_mode = $hooks_before || $hooks_after || 'yes' === $cart_redirect_after_add;
+ $legacy_mode = 'yes' === $cart_redirect_after_add || $this->has_form_elements( $hooks_before ) || $this->has_form_elements( $hooks_after );
if ( $legacy_mode ) {
$action_url = home_url( add_query_arg( null, null ) );
- // If an extension is hoooking into the form or we need to redirect to the cart,
+ // If an extension is hooking into the form or we need to redirect to the cart,
// we fall back to a regular HTML form.
$form_attributes = array(
'action' => esc_url(
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
index 161ac5fdd0..4eef7194a5 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/AddToCartWithOptions.php
@@ -72,13 +72,25 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
}
/**
- * Hook into the add to cart button action.
+ * Hook into the add to cart button action with a <select> element.
*
- * Outputs a test message when the `woocommerce_before_add_to_cart_button` action is triggered.
- * Used for testing that hooks are properly called during add to cart.
+ * Outputs a select element with an option.
+ * Used for testing that hooks are properly called during add to cart and
+ * fall back to a regular HTML form.
*/
public function hook_into_add_to_cart_button_action() {
- echo 'Hook into add to cart button action';
+ echo '<select><option>Hook into add to cart button action</option></select>';
+ }
+
+ /**
+ * Hook into the add to cart button action with a text element.
+ *
+ * Outputs a text element.
+ * Used for testing that text output doesn't trigger a fall back to a
+ * regular HTML form.
+ */
+ public function hook_into_add_to_cart_button_action_text() {
+ echo '<p>Hook into add to cart button action</p>';
}
/**
@@ -247,6 +259,7 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
$product->set_regular_price( 10 );
$product_id = $product->save();
+ // Test when cart redirect is enabled.
update_option( 'woocommerce_cart_redirect_after_add', 'yes' );
$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product_id . '} --><!-- wp:woocommerce/add-to-cart-with-options /--><!-- /wp:woocommerce/single-product -->' );
@@ -254,6 +267,7 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
$this->assertStringContainsString( 'action="https://example.com"', $markup, 'The form has an action that redirects to the page defined by the woocommerce_add_to_cart_form_action filter.' );
$this->assertStringNotContainsString( 'data-wp-on--submit', $markup, 'The form doesn\'t have an on submit event when redirect after add is enabled.' );
+ // Test when cart redirect is disabled.
update_option( 'woocommerce_cart_redirect_after_add', 'no' );
$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product_id . '} --><!-- wp:woocommerce/add-to-cart-with-options /--><!-- /wp:woocommerce/single-product -->' );
@@ -261,14 +275,26 @@ class AddToCartWithOptions extends \WP_UnitTestCase {
$this->assertStringNotContainsString( 'action="https://example.com"', $markup, 'The form doesn\'t have an action that redirects to the page defined by the woocommerce_add_to_cart_form_action filter when redirect after add is disabled.' );
$this->assertStringContainsString( 'data-wp-on--submit', $markup, 'The form has an on submit event when redirect after add is disabled.' );
+ // Test when an extension hooks into the form.
add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'hook_into_add_to_cart_button_action' ) );
$markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product_id . '} --><!-- wp:woocommerce/add-to-cart-with-options /--><!-- /wp:woocommerce/single-product -->' );
- $this->assertStringContainsString( 'action="https://example.com"', $markup, 'The form has an action that redirects to the page defined by the woocommerce_add_to_cart_form_action filter.' );
+ $this->assertStringContainsString( 'action="https://example.com"', $markup, 'The form has an action that redirects to the page defined by the woocommerce_add_to_cart_form_action filter when an extension hooks into the form.' );
$this->assertStringNotContainsString( 'data-wp-on--submit', $markup, 'The form doesn\'t have an on submit event when an extension hooks into the form.' );
remove_action( 'woocommerce_before_add_to_cart_button', array( $this, 'hook_into_add_to_cart_button_action' ) );
+
+ // Test when an extension hooks into the form but not adding a form element.
+ add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'hook_into_add_to_cart_button_action_text' ) );
+
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $product_id . '} --><!-- wp:woocommerce/add-to-cart-with-options /--><!-- /wp:woocommerce/single-product -->' );
+
+ $this->assertStringNotContainsString( 'action="https://example.com"', $markup, 'The form doesn\'t have an action that redirects to the page defined by the woocommerce_add_to_cart_form_action filter when an extension hooks into the form but not adding a form element.' );
+ $this->assertStringContainsString( 'data-wp-on--submit', $markup, 'The form has an on submit event when an extension hooks into the form but not adding a form element.' );
+
+ remove_action( 'woocommerce_before_add_to_cart_button', array( $this, 'hook_into_add_to_cart_button_action_text' ) );
+
remove_filter( 'woocommerce_add_to_cart_form_action', array( $this, 'hook_into_woocommerce_add_to_cart_form_action_filter' ) );
}