From b9abb5c6ef5b090414524e2fff39d6b1e6453c0e Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Mon, 3 Jul 2023 17:21:13 +0200
Subject: [PATCH] [BUGFIX] Show validation errors for fields with legend in
 form engine

Since #97330, a form engine elements label
might not be represented by a <label> tag,
but by using a <legend> tag.

This patch now ensures that those elements
are also correctly considered within form
engine validation.

Resolves: #101221
Related: #97330
Releases: main, 12.4
Change-Id: Iac4ef9b198bd7a7b8c75e53b83e45117be348c98
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79636
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
---
 Build/Sources/Sass/typo3/_main_form.scss                   | 3 ++-
 Build/Sources/TypeScript/backend/form-engine-review.ts     | 7 ++++++-
 .../backend/Classes/Form/Element/AbstractFormElement.php   | 2 +-
 typo3/sysext/backend/Resources/Public/Css/backend.css      | 2 +-
 .../Resources/Public/JavaScript/form-engine-review.js      | 2 +-
 5 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/Build/Sources/Sass/typo3/_main_form.scss b/Build/Sources/Sass/typo3/_main_form.scss
index 91b8922560fc..1f925ed791b8 100644
--- a/Build/Sources/Sass/typo3/_main_form.scss
+++ b/Build/Sources/Sass/typo3/_main_form.scss
@@ -127,7 +127,8 @@ fieldset[disabled] .form-control {
 // Form group validation states
 //
 .form-group.has-error {
-    > .form-label:before {
+    > .form-label:before,
+    > fieldset > .form-legend:before {
         @include icon-background($icon-actions-exclamation-circle, $danger);
         border-radius: 50%;
         width: 14px;
diff --git a/Build/Sources/TypeScript/backend/form-engine-review.ts b/Build/Sources/TypeScript/backend/form-engine-review.ts
index f4c97af2af87..d4038629fc49 100644
--- a/Build/Sources/TypeScript/backend/form-engine-review.ts
+++ b/Build/Sources/TypeScript/backend/form-engine-review.ts
@@ -36,6 +36,11 @@ class FormEngineReview {
    */
   private readonly labelSelector: string = '.t3js-formengine-label';
 
+  /**
+   * Class of FormEngine legends
+   */
+  private readonly legendSelector: string = '.t3js-formengine-legend';
+
   /**
    * The constructor, set the class properties default values
    */
@@ -108,7 +113,7 @@ class FormEngineReview {
         const link = document.createElement('a');
         link.classList.add('list-group-item');
         link.href = '#';
-        link.textContent = $field.find(me.labelSelector).text();
+        link.textContent = $field.find(me.labelSelector).text() || $field.find(me.legendSelector).text();
         link.addEventListener('click', (e: Event) => me.switchToField(e, $input));
 
         $list.append(link);
diff --git a/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php b/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
index 992bd6f0bce6..ed9c97987ce0 100644
--- a/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
+++ b/typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
@@ -150,7 +150,7 @@ abstract class AbstractFormElement extends AbstractNode
         }
         $html = [];
         $html[] = '<fieldset>';
-        $html[] =     '<legend class="form-legend">' . $legend . '</legend>';
+        $html[] =     '<legend class="form-legend t3js-formengine-legend">' . $legend . '</legend>';
         $html[] =     $innerHTML;
         $html[] = '</fieldset>';
         return implode(LF, $html);
diff --git a/typo3/sysext/backend/Resources/Public/Css/backend.css b/typo3/sysext/backend/Resources/Public/Css/backend.css
index e42d18bdd972..916fbf18b131 100644
--- a/typo3/sysext/backend/Resources/Public/Css/backend.css
+++ b/typo3/sysext/backend/Resources/Public/Css/backend.css
@@ -4182,7 +4182,7 @@ label .icon img{pointer-events:none}
 .formengine-field-item>.t3js-charcounter-wrapper .t3js-charcounter-min{margin:0 2px}
 .form-group .panel,.form-group .panel-group{overflow:visible}
 .form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{color:#737373}
-.form-group.has-error>.form-label:before{content:"";vertical-align:middle;-webkit-mask:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 16 16'%3e%3cg class='icon-color'%3e%3cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7z'/%3e%3ccircle cx='8' cy='11' r='1'/%3e%3cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55L8.5 9z'/%3e%3c/g%3e%3c/svg%3e");mask:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 16 16'%3e%3cg class='icon-color'%3e%3cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7z'/%3e%3ccircle cx='8' cy='11' r='1'/%3e%3cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55L8.5 9z'/%3e%3c/g%3e%3c/svg%3e");background-color:#c83c3c;background-size:contain;display:inline-block;border-radius:50%;width:14px;height:14px}
+.form-group.has-error>.form-label:before,.form-group.has-error>fieldset>.form-legend:before{content:"";vertical-align:middle;-webkit-mask:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 16 16'%3e%3cg class='icon-color'%3e%3cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7z'/%3e%3ccircle cx='8' cy='11' r='1'/%3e%3cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55L8.5 9z'/%3e%3c/g%3e%3c/svg%3e");mask:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 16 16'%3e%3cg class='icon-color'%3e%3cpath d='M8 2c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6m0-1C4.1 1 1 4.1 1 8s3.1 7 7 7 7-3.1 7-7-3.1-7-7-7z'/%3e%3ccircle cx='8' cy='11' r='1'/%3e%3cpath d='M8.5 9h-1l-.445-4.45A.5.5 0 0 1 7.552 4h.896a.5.5 0 0 1 .497.55L8.5 9z'/%3e%3c/g%3e%3c/svg%3e");background-color:#c83c3c;background-size:contain;display:inline-block;border-radius:50%;width:14px;height:14px}
 .form-group.has-error .btn-toolbar label:before{font-family:inherit;font-size:inherit;margin-right:inherit;text-align:inherit;content:"";color:inherit;display:block}
 select.form-select[multiple],select.form-select[size]:not([size="1"]){min-height:156px}
 select.form-select>optgroup{margin-top:9px}
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js
index 26c7c29a1f9f..a41f338f10dc 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import"bootstrap";import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import FormEngine from"@typo3/backend/form-engine.js";import"@typo3/backend/element/icon-element.js";import Popover from"@typo3/backend/popover.js";class FormEngineReview{constructor(){this.toggleButtonClass="t3js-toggle-review-panel",this.labelSelector=".t3js-formengine-label",this.checkForReviewableField=()=>{const e=this,t=FormEngineReview.findInvalidField(),o=document.querySelector("."+this.toggleButtonClass);if(null!==o)if(t.length>0){const i=$("<div />",{class:"list-group"});t.each((function(){const t=$(this),o=t.find("[data-formengine-validation-rules]"),n=document.createElement("a");n.classList.add("list-group-item"),n.href="#",n.textContent=t.find(e.labelSelector).text(),n.addEventListener("click",(t=>e.switchToField(t,o))),i.append(n)})),o.classList.remove("hidden"),Popover.setOptions(o,{html:!0,content:i[0]})}else o.classList.add("hidden"),Popover.hide(o)},this.switchToField=(e,t)=>{e.preventDefault(),t.parents('[id][role="tabpanel"]').each((function(){$('[aria-controls="'+$(this).attr("id")+'"]').tab("show")})),t.focus()},this.initialize()}static findInvalidField(){return $(document).find(".tab-content ."+FormEngine.Validation.errorClass)}static attachButtonToModuleHeader(e){const t=document.querySelector(".t3js-module-docheader-bar-buttons").lastElementChild.querySelector('[role="toolbar"]'),o=document.createElement("typo3-backend-icon");o.setAttribute("identifier","actions-info"),o.setAttribute("size","small");const i=document.createElement("button");i.type="button",i.classList.add("btn","btn-danger","btn-sm","hidden",e.toggleButtonClass),i.title=TYPO3.lang["buttons.reviewFailedValidationFields"],i.appendChild(o),Popover.popover(i),t.prepend(i)}initialize(){const e=this,t=$(document);DocumentService.ready().then((()=>{FormEngineReview.attachButtonToModuleHeader(e)})),t.on("t3-formengine-postfieldvalidation",this.checkForReviewableField)}}export default new FormEngineReview;
\ No newline at end of file
+import"bootstrap";import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import FormEngine from"@typo3/backend/form-engine.js";import"@typo3/backend/element/icon-element.js";import Popover from"@typo3/backend/popover.js";class FormEngineReview{constructor(){this.toggleButtonClass="t3js-toggle-review-panel",this.labelSelector=".t3js-formengine-label",this.legendSelector=".t3js-formengine-legend",this.checkForReviewableField=()=>{const e=this,t=FormEngineReview.findInvalidField(),o=document.querySelector("."+this.toggleButtonClass);if(null!==o)if(t.length>0){const n=$("<div />",{class:"list-group"});t.each((function(){const t=$(this),o=t.find("[data-formengine-validation-rules]"),i=document.createElement("a");i.classList.add("list-group-item"),i.href="#",i.textContent=t.find(e.labelSelector).text()||t.find(e.legendSelector).text(),i.addEventListener("click",(t=>e.switchToField(t,o))),n.append(i)})),o.classList.remove("hidden"),Popover.setOptions(o,{html:!0,content:n[0]})}else o.classList.add("hidden"),Popover.hide(o)},this.switchToField=(e,t)=>{e.preventDefault(),t.parents('[id][role="tabpanel"]').each((function(){$('[aria-controls="'+$(this).attr("id")+'"]').tab("show")})),t.focus()},this.initialize()}static findInvalidField(){return $(document).find(".tab-content ."+FormEngine.Validation.errorClass)}static attachButtonToModuleHeader(e){const t=document.querySelector(".t3js-module-docheader-bar-buttons").lastElementChild.querySelector('[role="toolbar"]'),o=document.createElement("typo3-backend-icon");o.setAttribute("identifier","actions-info"),o.setAttribute("size","small");const n=document.createElement("button");n.type="button",n.classList.add("btn","btn-danger","btn-sm","hidden",e.toggleButtonClass),n.title=TYPO3.lang["buttons.reviewFailedValidationFields"],n.appendChild(o),Popover.popover(n),t.prepend(n)}initialize(){const e=this,t=$(document);DocumentService.ready().then((()=>{FormEngineReview.attachButtonToModuleHeader(e)})),t.on("t3-formengine-postfieldvalidation",this.checkForReviewableField)}}export default new FormEngineReview;
\ No newline at end of file
-- 
GitLab