From 70626e00df84b7a15340285171169ef1f9304ea5 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:40:45 -0600 Subject: [PATCH] Fall back to first-error focus when the error summary isn't rendered js_validate's client-side validateFormSubmit() -> addAjaxFormErrors() never renders [data-frm-error-summary] - it only calls addFieldError() per field. checkForErrorsAndMaybeSetFocus() resolved focusErrorSummary true (the summary is "active" per filter, independent of js_validate) but focusFirstError false, so a failed submit on this path focused nothing at all - worse than the pre-#3408 behavior. Fall back to first-error-field focus when the summary was expected but isn't actually in the DOM, scoped to the submitting form's own container. Hand-patched js/formidable.min.js's matching function since SCRIPT_DEBUG is off in production/CI (same pattern PR#3408 used for this same file). Fixes Strategy11/formidable-pro#6763 --- js/formidable.js | 5 ++++- js/formidable.min.js | 2 +- tests/cypress/e2e/Forms/fieldsInFormBuilder-validation.cy.js | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/js/formidable.js b/js/formidable.js index 09e134947c..6cd973f4dd 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -1487,9 +1487,12 @@ function frmFrontFormJS() { summary.focus(); return; } + // Summary was resolved active server-side but isn't in the DOM on this path (e.g. + // js_validate's client-only validation never renders it) - fall back to the first + // error field instead of leaving focus nowhere. } - if ( ! config.focusFirstError ) { + if ( ! config.focusFirstError && ! config.focusErrorSummary ) { return; } diff --git a/js/formidable.min.js b/js/formidable.min.js index 264f898265..39ed008866 100644 --- a/js/formidable.min.js +++ b/js/formidable.min.js @@ -45,7 +45,7 @@ error.remove())}function scrollToFirstField(object){if("function"===typeof objec function addLoadingClass($object){const loadingClass=isGoingToPrevPage($object)?"frm_loading_prev":"frm_loading_form";$object.addClass(loadingClass)}function isGoingToPrevPage($object){return typeof frmProForm!=="undefined"&&frmProForm.goingToPreviousPage($object)}function removeSubmitLoading(_,enable,processesRunning){if(processesRunning>0)return;document.querySelectorAll(".frm_loading_form").forEach(function(form){form.classList.remove("frm_loading_form","frm_loading_prev");jQuery(form).trigger("frmEndFormLoading"); if(enable==="enable"){enableSubmitButton(form);enableSaveDraft(form)}})}function showFileLoading(object){const loading=document.getElementById("frm_loading");if(!loading)return;const fileInput=object.querySelector("input[type=file]");const fileval=fileInput?fileInput.value:"";if(fileval!=="")setTimeout(function(){jQuery(loading).fadeIn("slow")},2E3)}function confirmClick(){const message=this.dataset.frmconfirm;return confirm(message)}function onHoneypotFieldChange(){const css=window.getComputedStyle(this).boxShadow; if(css?.match(/inset/))this.remove()}function changeFocusWhenClickComboFieldLabel(){let label;const comboInputsContainer=document.querySelectorAll(".frm_combo_inputs_container");comboInputsContainer.forEach(function(inputsContainer){if(!inputsContainer.closest(".frm_form_field"))return;label=inputsContainer.closest(".frm_form_field").querySelector(".frm_primary_label");if(!label)return;label.addEventListener("click",function(){inputsContainer.querySelector(".frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea").focus()})})} -function maybeFocusOnComboSubField(element){if("FIELDSET"!==element.nodeName)return false;if(!element.querySelector(".frm_combo_inputs_container"))return false;const comboSubfield=element.querySelector('[aria-invalid="true"]');if(comboSubfield){focusInput(comboSubfield);return true}return false}function checkForErrorsAndMaybeSetFocus(){const errors=document.querySelectorAll(".frm_form_field .frm_error");if(!errors.length)return;const formContainer=errors[0].closest(".frm-show-form");const config=getErrorConfigForForm(formContainer);if(config.focusErrorSummary){const summary=formContainer?formContainer.querySelector("[data-frm-error-summary]"):null;if(summary){summary.focus();return}}if(!config.focusFirstError)return;let element=errors[0];let timeoutCallback; +function maybeFocusOnComboSubField(element){if("FIELDSET"!==element.nodeName)return false;if(!element.querySelector(".frm_combo_inputs_container"))return false;const comboSubfield=element.querySelector('[aria-invalid="true"]');if(comboSubfield){focusInput(comboSubfield);return true}return false}function checkForErrorsAndMaybeSetFocus(){const errors=document.querySelectorAll(".frm_form_field .frm_error");if(!errors.length)return;const formContainer=errors[0].closest(".frm-show-form");const config=getErrorConfigForForm(formContainer);if(config.focusErrorSummary){const summary=formContainer?formContainer.querySelector("[data-frm-error-summary]"):null;if(summary){summary.focus();return}}if(!config.focusFirstError&&!config.focusErrorSummary)return;let element=errors[0];let timeoutCallback; do{element=element.previousSibling;if(["input","select","textarea"].includes(element.nodeName.toLowerCase())){focusInput(element);break}if(maybeFocusOnComboSubField(element))break;if(element.classList!==undefined){if(element.classList.contains("html-active"))timeoutCallback=function(){const textarea=element.querySelector("textarea");if(null!==textarea)textarea.focus()};else if(element.classList.contains("tmce-active"))timeoutCallback=function(){tinyMCE.activeEditor.focus()};else if(element.classList.contains("frm_opt_container")){const firstInput= element.querySelector("input");if(firstInput){focusInput(firstInput);break}}if("function"===typeof timeoutCallback){setTimeout(timeoutCallback,0);break}}}while(element.previousSibling)}function focusInput(input){if(input.offsetParent!==null)input.focus();else triggerCustomEvent(document,"frmMaybeDelayFocus",{input})}function focusFieldFromErrorLink(event){const href=this.getAttribute("href");if(!href||!href.startsWith("#"))return;const container=document.getElementById(href.substring(1));if(!container)return; event.preventDefault();container.scrollIntoView({behavior:"smooth",block:"center"});const input=getFocusableInputInField(container);if(input){focusInput(input);return}container.setAttribute("tabindex","-1");focusInput(container)}function getFocusableInputInField(container){const inputs=Array.from(container.querySelectorAll(FOCUSABLE_FIELD_SELECTOR)).filter(inputCanTakeFocus);if(!inputs.length)return null;const invalidInput=inputs.find(input=>"true"===input.getAttribute("aria-invalid"));if(invalidInput)return invalidInput; diff --git a/tests/cypress/e2e/Forms/fieldsInFormBuilder-validation.cy.js b/tests/cypress/e2e/Forms/fieldsInFormBuilder-validation.cy.js index b3b3371654..f501d4a62b 100644 --- a/tests/cypress/e2e/Forms/fieldsInFormBuilder-validation.cy.js +++ b/tests/cypress/e2e/Forms/fieldsInFormBuilder-validation.cy.js @@ -180,6 +180,9 @@ describe( 'Fields in the form builder', () => { cy.get( `[id^="frm_error_field_"]` ).eq( 0 ).should( 'contain', `Text cannot be blank.` ); cy.get( `[id^="frm_error_field_"]` ).eq( 1 ).should( 'not.exist' ); cy.get( `[id^="frm_error_field_"]` ).eq( 2 ).should( 'not.exist' ); + // js_validate's client-side path never renders the error summary markup - focus must + // fall back to the first errored field instead of going nowhere (formidable-pro#6763). + cy.get( '[id^="field_"]' ).filter( 'input, textarea' ).eq( 0 ).should( 'have.focus' ); cy.get( '[id^="field_"]' ).filter( 'input, textarea' ).eq( 1 ).clear(); cy.get( '[id^="field_"]' ).filter( 'input, textarea' ).eq( 2 ).clear(); cy.get( "button[type='submit']" ).should( 'contain', 'Submit' ).click();