Commit ed6d5ba2497 for woocommerce
commit ed6d5ba2497c3927445fdbabdb6e2a48708d431e
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date: Wed Jun 24 15:52:27 2026 +0300
Fix custom place order button never submitting on shortcode checkout (#65933)
Fix custom place order button never submitting order with shipping block present
On the classic (shortcode) checkout, a gateway using a custom place order
button never submitted the order when the cart needed shipping. The collapsed
"Ship to a different address?" block renders hidden, empty required shipping
fields, and the button's client-side validate() counted all .woocommerce-invalid
fields regardless of visibility, so those hidden rows falsely blocked submission
with no error notice.
Gate submission (and the scroll-to-error target) on .woocommerce-invalid:visible,
matching the :visible filter already used by the sibling required-field check.
Fixes WOOPMNT-6250.
diff --git a/plugins/woocommerce/changelog/woopmnt-6250-custom-place-order-button b/plugins/woocommerce/changelog/woopmnt-6250-custom-place-order-button
new file mode 100644
index 00000000000..1ce7806c490
--- /dev/null
+++ b/plugins/woocommerce/changelog/woopmnt-6250-custom-place-order-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix custom place order button (shortcode checkout) never submitting the order when a shipping address block is present. Client-side validation now only counts visible invalid fields, so the collapsed "Ship to a different address?" shipping fields no longer block submission.
diff --git a/plugins/woocommerce/client/legacy/js/frontend/checkout.js b/plugins/woocommerce/client/legacy/js/frontend/checkout.js
index 4fdbe0b393e..8b54c351fca 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/checkout.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/checkout.js
@@ -43,8 +43,14 @@ jQuery( function ( $ ) {
// Trigger field-level validation (which adds `.woocommerce-invalid` to invalid fields)
$form.find( '.input-text, select, input:checkbox' ).trigger( 'validate' );
- // Check for validation errors (from validate_field handler)
- if ( $form.find( '.woocommerce-invalid' ).length > 0 ) {
+ // Check for validation errors (from validate_field handler).
+ // Only consider visible fields: `validate_field` flags any empty
+ // required field regardless of visibility, so hidden fields (e.g.
+ // the collapsed "Ship to a different address?" shipping fields)
+ // would otherwise block submission even when the visible form is
+ // valid. The `.woocommerce-invalid` class lives on the `.form-row`,
+ // which is hidden when its section is collapsed.
+ if ( $form.find( '.woocommerce-invalid:visible' ).length > 0 ) {
hasError = true;
}
@@ -79,7 +85,7 @@ jQuery( function ( $ ) {
// Scroll to the first invalid field in DOM order
if ( hasError ) {
- var $firstInvalidField = $form.find( '.woocommerce-invalid' ).first();
+ var $firstInvalidField = $form.find( '.woocommerce-invalid:visible' ).first();
if ( $firstInvalidField.length ) {
$( 'html, body' ).animate(
{
diff --git a/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js b/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js
index a3e0a1c942f..f39a76a85de 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js
@@ -7,9 +7,17 @@ describe( 'createCheckoutPlaceOrderApi', () => {
let $termsCheckbox;
let $termsRow;
let capturedApi;
+ // Set the number of invalid `.form-row` elements that are hidden (e.g. the
+ // collapsed "Ship to a different address?" shipping fields). These must never
+ // block submission, so `validate()` should only count visible invalid fields.
+ let setHiddenInvalidCount;
beforeEach( () => {
capturedApi = null;
+ let hiddenInvalidCount = 0;
+ setHiddenInvalidCount = ( count ) => {
+ hiddenInvalidCount = count;
+ };
// used to track whether terms checkbox is checked
let termsChecked = false;
@@ -61,7 +69,9 @@ describe( 'createCheckoutPlaceOrderApi', () => {
if ( selector === '.input-text, select, input:checkbox' ) {
return { trigger: jest.fn() };
}
- if ( selector === '.woocommerce-invalid' ) {
+ if ( selector === '.woocommerce-invalid:visible' ) {
+ // Visible invalid fields only (e.g. the terms row). Hidden
+ // invalid fields are deliberately excluded.
return {
length: formInvalidElements.size,
first: jest.fn( () => ( {
@@ -70,6 +80,19 @@ describe( 'createCheckoutPlaceOrderApi', () => {
} ) ),
};
}
+ if ( selector === '.woocommerce-invalid' ) {
+ // Unfiltered query (includes hidden fields). The implementation
+ // must NOT use this to gate submission; counting hidden invalid
+ // fields here is the regression these tests guard against.
+ const total = formInvalidElements.size + hiddenInvalidCount;
+ return {
+ length: total,
+ first: jest.fn( () => ( {
+ length: total > 0 ? 1 : 0,
+ offset: jest.fn( () => ( { top: 100 } ) ),
+ } ) ),
+ };
+ }
if ( selector === '.validate-required:visible' ) {
return { each: jest.fn() };
}
@@ -270,4 +293,32 @@ describe( 'createCheckoutPlaceOrderApi', () => {
expect( secondResult.hasError ).toBe( false );
} );
} );
+
+ describe( 'Hidden field validation', () => {
+ test( 'should ignore invalid fields that are hidden (e.g. collapsed shipping address)', async () => {
+ // A shippable cart renders the "Ship to a different address?" block,
+ // whose required shipping fields are present but hidden when the option
+ // is unchecked. Field-level validation flags them as invalid regardless
+ // of visibility, so validate() must only count *visible* invalid fields
+ // or the order is never submitted even when the visible form is valid.
+ $termsCheckbox.setChecked( true );
+ setHiddenInvalidCount( 5 );
+
+ const result = await capturedApi.validate();
+
+ expect( result.hasError ).toBe( false );
+ } );
+
+ test( 'should only count visible invalid fields', async () => {
+ $termsCheckbox.setChecked( true );
+ setHiddenInvalidCount( 5 );
+
+ await capturedApi.validate();
+
+ expect( $form.find ).toHaveBeenCalledWith(
+ '.woocommerce-invalid:visible'
+ );
+ expect( $form.find ).not.toHaveBeenCalledWith( '.woocommerce-invalid' );
+ } );
+ } );
} );