From a6336015121971756764e6c4ebf01624ac2c26f5 Mon Sep 17 00:00:00 2001
From: "vivi-the-going-merry[bot]"
<308115520+vivi-the-going-merry[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 14:47:34 -0600
Subject: [PATCH 1/2] Focus success message after AJAX submit
Screen reader users get no notification when the success/error message
replaces the form after an AJAX submit, since focus stays wherever it
was before the submit button was clicked.
- tabindex="-1" on the message div (FrmFormsHelper::get_success_message)
makes it programmatically focusable without adding it to the tab order.
- js/formidable.js focuses the message once it's live in the document.
The old .frm_forms wrapper is already detached by the time the message
needs to be looked up, so it's queried fresh from the still-attached
parent captured before replaceWith() ran, not from the replaced-away
subtree.
Closes Strategy11/formidable-pro#6702
---
classes/helpers/FrmFormsHelper.php | 4 +++-
js/formidable.js | 21 +++++++++++++++++++++
tests/cypress/e2e/admin.cy.js | 3 +++
3 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index b9da117e58..97671eceec 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -532,7 +532,9 @@ public static function get_success_message( $atts ) {
$message = do_shortcode( $message );
$role = $atts['role'] ?? 'status';
- return '
' . $message . '
';
+ // tabindex="-1" makes the message programmatically focusable without adding it to the tab order,
+ // so it can receive focus after an AJAX submit without a screen reader user having to tab to it.
+ return '' . $message . '
';
}
/**
diff --git a/js/formidable.js b/js/formidable.js
index 41c018a4bd..5fbc4180c2 100644
--- a/js/formidable.js
+++ b/js/formidable.js
@@ -939,12 +939,14 @@ function frmFrontFormJS() {
const replaceContent = jQuery( object ).closest( '.frm_forms' ); // eslint-disable-line no-jquery/no-closest
removeAddedScripts( replaceContent, formID );
const delay = maybeSlideOut( replaceContent, response.content );
+ const formParent = replaceContent.parent();
setTimeout(
function() {
afterFormSubmittedBeforeReplace( object, response );
replaceContent.replaceWith( response.content );
+ focusFormMessage( formParent );
addUrlParam( response );
@@ -1070,6 +1072,25 @@ function frmFrontFormJS() {
jQuery.ajax( ajaxParams ); // eslint-disable-line no-jquery/no-ajax
}
+ /**
+ * Move focus to the top-level success message after an AJAX submit, so screen reader
+ * users are notified it appeared. `replaceContent` (the old `.frm_forms` wrapper) is
+ * already detached from the document by the time this runs, so the message has to be
+ * looked up fresh from `formParent` (still live in the document) rather than from the
+ * replaced-away subtree or from a detached `tempDiv` copy of `response.content`.
+ *
+ * @since x.x
+ *
+ * @param {jQuery} formParent The element that contained the form before it was replaced.
+ * @return {void}
+ */
+ function focusFormMessage( formParent ) {
+ const message = formParent[ 0 ] && formParent[ 0 ].querySelector( '.frm_message' );
+ if ( message ) {
+ message.focus();
+ }
+ }
+
function afterFormSubmitted( object, response ) {
const tempDiv = document.createElement( 'div' );
tempDiv.innerHTML = response.content;
diff --git a/tests/cypress/e2e/admin.cy.js b/tests/cypress/e2e/admin.cy.js
index 80941a8a93..e25df9e43e 100644
--- a/tests/cypress/e2e/admin.cy.js
+++ b/tests/cypress/e2e/admin.cy.js
@@ -38,6 +38,9 @@ describe( 'Run some basic Formidale tests', function() {
cy.visit( `/wp-admin/admin-ajax.php?action=frm_forms_preview&form=${ formKey }` );
cy.get( '.frm_button_submit' ).should( 'contain.text', 'Submit' ).click();
cy.get( '.frm_message' ).should( 'contain.text', 'Your responses were successfully submitted. Thank you!' );
+
+ // Focus should move to the success message so screen reader users are notified it appeared.
+ cy.focused().should( 'have.class', 'frm_message' );
} );
} );
} );
From 4281096655d97e1a4d77ac155072d165728bcb72 Mon Sep 17 00:00:00 2001
From: "vivi-the-going-merry[bot]"
<308115520+vivi-the-going-merry[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 14:53:42 -0600
Subject: [PATCH 2/2] Self-review: reuse focusInput(), scope focus lookup to
inserted content
- Reuse the existing focusInput() helper (already used by the error-summary
a11y work) instead of a bare .focus() call, so a message that appears
mid slide-in animation is handled the same way error focus already is.
- Scope the .frm_message lookup to the markup that was just inserted,
not the form's whole parent container, so an unrelated element
elsewhere on the page can never be focused instead.
- Tighten the Cypress assertion to check focus on the message element
itself, and the PHP comment to not overclaim beyond the success case.
---
classes/helpers/FrmFormsHelper.php | 5 +++--
js/formidable.js | 21 ++++++++++-----------
tests/cypress/e2e/admin.cy.js | 5 +++--
3 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index 97671eceec..d858d58bc1 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -532,8 +532,9 @@ public static function get_success_message( $atts ) {
$message = do_shortcode( $message );
$role = $atts['role'] ?? 'status';
- // tabindex="-1" makes the message programmatically focusable without adding it to the tab order,
- // so it can receive focus after an AJAX submit without a screen reader user having to tab to it.
+ // tabindex="-1" makes the success message programmatically focusable without adding it
+ // to the tab order, so js/formidable.js can focus it after an AJAX submit without a
+ // screen reader user having to tab to it.
return '' . $message . '
';
}
diff --git a/js/formidable.js b/js/formidable.js
index 5fbc4180c2..4b57e6a1b4 100644
--- a/js/formidable.js
+++ b/js/formidable.js
@@ -939,14 +939,14 @@ function frmFrontFormJS() {
const replaceContent = jQuery( object ).closest( '.frm_forms' ); // eslint-disable-line no-jquery/no-closest
removeAddedScripts( replaceContent, formID );
const delay = maybeSlideOut( replaceContent, response.content );
- const formParent = replaceContent.parent();
setTimeout(
function() {
afterFormSubmittedBeforeReplace( object, response );
- replaceContent.replaceWith( response.content );
- focusFormMessage( formParent );
+ const insertedContent = jQuery( response.content );
+ replaceContent.replaceWith( insertedContent );
+ focusFormMessage( insertedContent );
addUrlParam( response );
@@ -1074,20 +1074,19 @@ function frmFrontFormJS() {
/**
* Move focus to the top-level success message after an AJAX submit, so screen reader
- * users are notified it appeared. `replaceContent` (the old `.frm_forms` wrapper) is
- * already detached from the document by the time this runs, so the message has to be
- * looked up fresh from `formParent` (still live in the document) rather than from the
- * replaced-away subtree or from a detached `tempDiv` copy of `response.content`.
+ * users are notified it appeared. Scoped to `insertedContent` (the markup that just
+ * replaced the form) rather than a wider ancestor, so an unrelated `.frm_message`-classed
+ * element elsewhere on the page can never be focused instead.
*
* @since x.x
*
- * @param {jQuery} formParent The element that contained the form before it was replaced.
+ * @param {jQuery} insertedContent The markup that just replaced the form.
* @return {void}
*/
- function focusFormMessage( formParent ) {
- const message = formParent[ 0 ] && formParent[ 0 ].querySelector( '.frm_message' );
+ function focusFormMessage( insertedContent ) {
+ const message = insertedContent.filter( '.frm_message' ).add( insertedContent.find( '.frm_message' ) ).get( 0 );
if ( message ) {
- message.focus();
+ focusInput( message );
}
}
diff --git a/tests/cypress/e2e/admin.cy.js b/tests/cypress/e2e/admin.cy.js
index e25df9e43e..f5557f80fa 100644
--- a/tests/cypress/e2e/admin.cy.js
+++ b/tests/cypress/e2e/admin.cy.js
@@ -37,10 +37,11 @@ describe( 'Run some basic Formidale tests', function() {
cy.get( '#frm_form_key' ).invoke( 'val' ).then( formKey => {
cy.visit( `/wp-admin/admin-ajax.php?action=frm_forms_preview&form=${ formKey }` );
cy.get( '.frm_button_submit' ).should( 'contain.text', 'Submit' ).click();
- cy.get( '.frm_message' ).should( 'contain.text', 'Your responses were successfully submitted. Thank you!' );
// Focus should move to the success message so screen reader users are notified it appeared.
- cy.focused().should( 'have.class', 'frm_message' );
+ cy.get( '.frm_message' )
+ .should( 'contain.text', 'Your responses were successfully submitted. Thank you!' )
+ .and( 'be.focused' );
} );
} );
} );