This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 98ee4e1d6 WW-5781 fix(tags): render a single class and style on 
checkboxlist and radio inputs (#2026)
98ee4e1d6 is described below

commit 98ee4e1d6e478af9dd8ee31878e5eaebd9e00506
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Oct 16 09:11:42 2026 +0200

    WW-5781 fix(tags): render a single class and style on checkboxlist and 
radio inputs (#2026)
    
    checkboxlist always, and radio whenever listCssClass or listCssStyle was 
set,
    wrote class/style themselves and then again through css.ftl. Browsers keep 
the
    first duplicate attribute, so cssErrorClass and cssErrorStyle never applied.
    
    A shared list-item-css.ftl now computes one class and one style per item:
    the item value replaces the tag value, the error class is appended and the
    error style applied when the field has errors, following the css.ftl rules.
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../main/resources/template/html5/checkboxlist.ftl | 16 +-----
 .../src/main/resources/template/html5/radiomap.ftl |  8 +--
 .../resources/template/simple/checkboxlist.ftl     | 16 +-----
 .../resources/template/simple/list-item-css.ftl    | 41 ++++++++++++++
 .../main/resources/template/simple/radiomap.ftl    |  8 +--
 .../freemarker/FreemarkerResultMockedTest.java     |  4 ++
 .../struts2/views/jsp/ui/CheckboxListTest.java     | 64 ++++++++++++++++++++++
 .../org/apache/struts2/views/jsp/ui/RadioTest.java | 48 ++++++++++++++++
 .../apache/struts2/views/jsp/ui/CheckboxList-5.txt |  5 ++
 .../apache/struts2/views/jsp/ui/CheckboxList-6.txt |  5 ++
 .../apache/struts2/views/jsp/ui/CheckboxList-7.txt |  5 ++
 .../org/apache/struts2/views/jsp/ui/Radio-12.txt   |  4 ++
 .../org/apache/struts2/views/jsp/ui/Radio-13.txt   |  4 ++
 13 files changed, 184 insertions(+), 44 deletions(-)

diff --git a/core/src/main/resources/template/html5/checkboxlist.ftl 
b/core/src/main/resources/template/html5/checkboxlist.ftl
index 0a2550aed..680edcfc7 100644
--- a/core/src/main/resources/template/html5/checkboxlist.ftl
+++ b/core/src/main/resources/template/html5/checkboxlist.ftl
@@ -72,20 +72,7 @@
 <#if attributes.disabled!false>
  disabled="disabled"<#rt/>
 </#if>
-<#if itemCssClass??>
- class="${itemCssClass}"<#rt/>
-<#else>
-<#if attributes.cssClass?has_content>
- class="${attributes.cssClass}"<#rt/>
-</#if>
-</#if>
-<#if itemCssStyle??>
- style="${itemCssStyle}"<#rt/>
-<#else>
-<#if attributes.cssStyle?has_content>
- style="${attributes.cssStyle}"<#rt/>
-</#if>
-</#if>
+<#include 
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl" 
/><#rt/>
 <#if itemTitle??>
  title="${itemTitle}"<#rt/>
 <#else>
@@ -93,7 +80,6 @@
  title="${attributes.title}"<#rt/>
 </#if>
 </#if>
-<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" 
/><#rt/>
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl" 
/><#rt/>
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl" 
/><#rt/>
 <#global evaluate_dynamic_attributes = true/><#t/>
diff --git a/core/src/main/resources/template/html5/radiomap.ftl 
b/core/src/main/resources/template/html5/radiomap.ftl
index 5826fe47f..55fd97c95 100644
--- a/core/src/main/resources/template/html5/radiomap.ftl
+++ b/core/src/main/resources/template/html5/radiomap.ftl
@@ -79,12 +79,7 @@ file for it's localized value.  This is then used as a label 
-->
 <#if attributes.tabindex?has_content>
  tabindex="${attributes.tabindex}"<#rt/>
 </#if>
-<#if itemCssClass?has_content>
- class="${itemCssClass}"<#rt/>
-</#if>
-<#if itemCssStyle?has_content>
- style="${itemCssStyle}"<#rt/>
-</#if>
+<#include 
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl" 
/><#rt/>
 <#if itemTitle?has_content>
  title="${itemTitle}"<#rt/>
 <#else>
@@ -92,7 +87,6 @@ file for it's localized value.  This is then used as a label 
-->
  title="${attributes.title}"<#rt/>
 </#if>
 </#if>
-<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" 
/><#rt/>
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl" 
/><#rt/>
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl" 
/><#rt/>
 <#global evaluate_dynamic_attributes = true/><#t/>
diff --git a/core/src/main/resources/template/simple/checkboxlist.ftl 
b/core/src/main/resources/template/simple/checkboxlist.ftl
index 63cf52e32..7fa09ddf8 100644
--- a/core/src/main/resources/template/simple/checkboxlist.ftl
+++ b/core/src/main/resources/template/simple/checkboxlist.ftl
@@ -71,20 +71,7 @@
     <#if attributes.disabled!false>
        disabled="disabled"<#rt/>
     </#if>
-    <#if itemCssClass??>
-     class="${itemCssClass}"<#rt/>
-    <#else>
-        <#if attributes.cssClass?has_content>
-     class="${attributes.cssClass}"<#rt/>
-        </#if>
-    </#if>
-    <#if itemCssStyle??>
-     style="${itemCssStyle}"<#rt/>
-    <#else>
-        <#if attributes.cssStyle?has_content>
-     style="${attributes.cssStyle}"<#rt/>
-        </#if>
-    </#if>
+    <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl" />
     <#if itemTitle??>
      title="${itemTitle}"<#rt/>
     <#else>
@@ -92,7 +79,6 @@
      title="${attributes.title}"<#rt/>
         </#if>
     </#if>
-    <#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" />
     <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl" />
     <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl" />
     <#global evaluate_dynamic_attributes = true/>
diff --git a/core/src/main/resources/template/simple/list-item-css.ftl 
b/core/src/main/resources/template/simple/list-item-css.ftl
new file mode 100644
index 000000000..3d67bb99f
--- /dev/null
+++ b/core/src/main/resources/template/simple/list-item-css.ftl
@@ -0,0 +1,41 @@
+<#--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<#assign hasFieldErrors = attributes.name?? && fieldErrors?? && 
fieldErrors.get(attributes.name)??/>
+<#assign hasErrorClass = hasFieldErrors && attributes.cssErrorClass??/>
+<#assign hasErrorStyle = hasFieldErrors && attributes.cssErrorStyle??/>
+<#if itemCssClass?has_content>
+    <#assign listItemClass = itemCssClass/>
+<#else>
+    <#assign listItemClass = attributes.cssClass!''/>
+</#if>
+<#if hasErrorClass>
+    <#assign listItemClass = (listItemClass + ' ' + 
attributes.cssErrorClass)?trim/>
+</#if>
+<#if listItemClass?has_content>
+ class="${listItemClass}"<#rt/>
+</#if>
+<#if hasErrorStyle>
+ style="${attributes.cssErrorStyle}"<#rt/>
+<#elseif itemCssStyle?has_content>
+ style="${itemCssStyle}"<#rt/>
+<#elseif attributes.cssStyle?has_content && !hasErrorClass>
+ style="${attributes.cssStyle}"<#rt/>
+</#if>
diff --git a/core/src/main/resources/template/simple/radiomap.ftl 
b/core/src/main/resources/template/simple/radiomap.ftl
index 524bc7f7f..a091d25a7 100644
--- a/core/src/main/resources/template/simple/radiomap.ftl
+++ b/core/src/main/resources/template/simple/radiomap.ftl
@@ -78,12 +78,7 @@
 <#if attributes.tabindex?has_content>
  tabindex="${attributes.tabindex}"<#rt/>
 </#if>
-<#if itemCssClass?has_content>
- class="${itemCssClass}"<#rt/>
-</#if>
-<#if itemCssStyle?has_content>
- style="${itemCssStyle}"<#rt/>
-</#if>
+<#include 
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl" />
 <#if itemTitle?has_content>
  title="${itemTitle}"<#rt/>
 <#else>
@@ -91,7 +86,6 @@
  title="${attributes.title}"<#rt/>
     </#if>
 </#if>
-<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" />
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl" />
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl" />
 <#global evaluate_dynamic_attributes = true/>
diff --git 
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
 
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
index 50e3313ae..70a3493c7 100644
--- 
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
+++ 
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
@@ -134,6 +134,10 @@ public class FreemarkerResultMockedTest extends 
StrutsInternalTestCase {
         file = new 
File(ClassLoaderUtil.getResource("template/simple/radiomap.ftl", 
getClass()).toURI());
         
EasyMock.expect(servletContext.getRealPath("/template/simple/radiomap.ftl")).andReturn(file.getAbsolutePath());
 
+        file = new 
File(ClassLoaderUtil.getResource("template/simple/list-item-css.ftl", 
getClass()).toURI());
+        
EasyMock.expect(servletContext.getRealPath("/template/simple/list-item-css.ftl")).andReturn(file.getAbsolutePath());
+        
EasyMock.expect(servletContext.getRealPath("/template/~~~simple/list-item-css.ftl")).andReturn(file.getAbsolutePath());
+
         file = new File(ClassLoaderUtil.getResource("template/simple/css.ftl", 
getClass()).toURI());
         
EasyMock.expect(servletContext.getRealPath("/template/simple/css.ftl")).andReturn(file.getAbsolutePath());
         
EasyMock.expect(servletContext.getRealPath("/template/~~~simple/css.ftl")).andReturn(file.getAbsolutePath());
diff --git 
a/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java 
b/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
index 72ce56933..a16d2abc8 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.struts2.views.jsp.ui;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.struts2.TestAction;
 import org.apache.struts2.views.jsp.AbstractUITagTest;
 
@@ -383,4 +384,67 @@ public class CheckboxListTest extends AbstractUITagTest {
                 "May indicate that clearTagStateForTagPoolingServers() calls 
are not working properly.",
                 strutsBodyTagsAreReflectionEqual(tag, freshTag));
     }
+
+    public void testCssClassAndStyleRenderedOnce() throws Exception {
+        CheckboxListTag tag = createStyledTag("simple");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        verify(CheckboxListTag.class.getResource("CheckboxList-5.txt"));
+    }
+
+    public void testListCssClassAndStyleReplaceTagCss() throws Exception {
+        CheckboxListTag tag = createStyledTag("simple");
+        tag.setListCssClass("'itemClass'");
+        tag.setListCssStyle("'itemstyle'");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        verify(CheckboxListTag.class.getResource("CheckboxList-6.txt"));
+    }
+
+    public void testCssErrorClassAndStyleWithFieldErrors() throws Exception {
+        ((TestAction) action).addFieldError("collection", "collection error");
+        CheckboxListTag tag = createStyledTag("simple");
+        tag.setCssErrorClass("errclass");
+        tag.setCssErrorStyle("errstyle");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        verify(CheckboxListTag.class.getResource("CheckboxList-7.txt"));
+    }
+
+    public void testCssClassAndStyleRenderedOnceHtml5() throws Exception {
+        CheckboxListTag tag = createStyledTag("html5");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        String output = writer.toString();
+        assertEquals(2, StringUtils.countMatches(output, "class=\"myClass\""));
+        assertEquals(2, StringUtils.countMatches(output, "style=\"mystyle\""));
+    }
+
+    private CheckboxListTag createStyledTag(String theme) {
+        TestAction testAction = (TestAction) action;
+        testAction.setCollection(List.of("hello"));
+        testAction.setList(new String[][]{
+                {"hello", "world"},
+                {"foo", "bar"}
+        });
+
+        CheckboxListTag tag = new CheckboxListTag();
+        tag.setPageContext(pageContext);
+        tag.setTheme(theme);
+        tag.setName("collection");
+        tag.setList("list");
+        tag.setListKey("top[0]");
+        tag.setListValue("top[1]");
+        tag.setCssClass("myClass");
+        tag.setCssStyle("mystyle");
+        return tag;
+    }
 }
diff --git a/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java 
b/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
index bc86a9909..ba7913b8a 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
@@ -191,6 +191,54 @@ public class RadioTest extends AbstractUITagTest {
             strutsBodyTagsAreReflectionEqual(tag, freshTag));
     }
 
+    public void testListCssClassAndStyleReplaceTagCss() throws Exception {
+        RadioTag tag = createStyledTag("simple");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        verify(RadioTag.class.getResource("Radio-12.txt"));
+    }
+
+    public void testListCssClassAndStyleReplaceTagCssHtml5() throws Exception {
+        RadioTag tag = createStyledTag("html5");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        verify(RadioTag.class.getResource("Radio-12.txt"));
+    }
+
+    public void testCssErrorClassAndStyleWithListCssClass() throws Exception {
+        ((TestAction) action).addFieldError("status", "status error");
+        RadioTag tag = createStyledTag("simple");
+        tag.setCssErrorClass("errclass");
+        tag.setCssErrorStyle("errstyle");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        verify(RadioTag.class.getResource("Radio-13.txt"));
+    }
+
+    private RadioTag createStyledTag(String theme) {
+        TestAction testAction = (TestAction) action;
+        testAction.setEnumList(new 
ArrayList<>(Arrays.asList(SomeEnum.values())));
+        testAction.setStatus(SomeEnum.INIT);
+
+        RadioTag tag = new RadioTag();
+        tag.setTheme(theme);
+        tag.setPageContext(pageContext);
+        tag.setName("status");
+        tag.setValue("status");
+        tag.setList("enumList");
+        tag.setCssClass("myClass");
+        tag.setCssStyle("mystyle");
+        tag.setListCssClass("'itemClass'");
+        tag.setListCssStyle("'itemstyle'");
+        return tag;
+    }
+
     public void testMapCheckedUsingEnum() throws Exception {
         TestAction testAction = (TestAction) action;
 
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-5.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-5.txt
new file mode 100644
index 000000000..e6c0c54f4
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-5.txt
@@ -0,0 +1,5 @@
+<input type="checkbox" name="collection" value="hello" id="collection-1" 
checked="checked" class="myClass" style="mystyle"/>
+<label for="collection-1" class="checkboxLabel">world</label>
+<input type="checkbox" name="collection" value="foo" id="collection-2" 
class="myClass" style="mystyle"/>
+<label for="collection-2" class="checkboxLabel">bar</label>
+<input type="hidden" id="__multiselect_collection" 
name="__multiselect_collection" value=""/>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-6.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-6.txt
new file mode 100644
index 000000000..23f46f4c0
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-6.txt
@@ -0,0 +1,5 @@
+<input type="checkbox" name="collection" value="hello" id="collection-1" 
checked="checked" class="itemClass" style="itemstyle"/>
+<label for="collection-1" class="checkboxLabel">world</label>
+<input type="checkbox" name="collection" value="foo" id="collection-2" 
class="itemClass" style="itemstyle"/>
+<label for="collection-2" class="checkboxLabel">bar</label>
+<input type="hidden" id="__multiselect_collection" 
name="__multiselect_collection" value=""/>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-7.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-7.txt
new file mode 100644
index 000000000..0d7b86f49
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-7.txt
@@ -0,0 +1,5 @@
+<input type="checkbox" name="collection" value="hello" id="collection-1" 
checked="checked" class="myClass errclass" style="errstyle"/>
+<label for="collection-1" class="checkboxLabel">world</label>
+<input type="checkbox" name="collection" value="foo" id="collection-2" 
class="myClass errclass" style="errstyle"/>
+<label for="collection-2" class="checkboxLabel">bar</label>
+<input type="hidden" id="__multiselect_collection" 
name="__multiselect_collection" value=""/>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-12.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-12.txt
new file mode 100644
index 000000000..c561c4317
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-12.txt
@@ -0,0 +1,4 @@
+<input type="radio" name="status" id="statusINIT" checked="checked" 
value="INIT" class="itemClass" style="itemstyle"/>
+<label for="statusINIT" class="myClass" style="mystyle">INIT</label>
+<input type="radio" name="status" id="statusCOMPLETED" value="COMPLETED" 
class="itemClass" style="itemstyle"/>
+<label for="statusCOMPLETED" class="myClass" style="mystyle">COMPLETED</label>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-13.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-13.txt
new file mode 100644
index 000000000..8129f8c08
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-13.txt
@@ -0,0 +1,4 @@
+<input type="radio" name="status" id="statusINIT" checked="checked" 
value="INIT" class="itemClass errclass" style="errstyle"/>
+<label for="statusINIT" class="myClass errclass" style="errstyle">INIT</label>
+<input type="radio" name="status" id="statusCOMPLETED" value="COMPLETED" 
class="itemClass errclass" style="errstyle"/>
+<label for="statusCOMPLETED" class="myClass errclass" 
style="errstyle">COMPLETED</label>

Reply via email to