Repository: primefaces/primefaces (java)

Issue:
MenuButton placed in wrong position after 14.0.3 release
### Describe the bug

After fixing issue n. #12286: MenuButton menu should be attached to trigger not body

if a menu button is placed into a small scrollable container (in my example an accordion panel) the menu lays in the wrong position

### Reproducer

[primefaces-test-master.zip](https://github.com/user-attachments/files/16423663/primefaces-test-master.zip)


### Expected behavior

Until 14.0.2 the menu was attached to body, so its absolute position was really absolute. Now if the container has a relative position it does not pop up in the right way. 
MenuButton has an appendTo attribute and it seems being ignored to me, but the actual default behaviour broke some common use cases.

Probably use position fixed + the right calculation for top and an accurate recalculation in case of scrolling (top - offset) can be the right way...
 

### PrimeFaces edition

Community

### PrimeFaces version

14.0.3

### Theme

_No response_

### JSF implementation

Mojarra

### JSF version

2.3.21

### Java version

11

### Browser(s)

_No response_

Pull request:
Fix #12349: 14.0.4 MenuButton restore appendTo property
Fix #12349: 14.0.4 MenuButton restore appendTo property

Patch:
diff --git a/docs/14_0_0/components/menubutton.md b/docs/14_0_0/components/menubutton.md
index 9562ce03b7..aee85da529 100644
--- a/docs/14_0_0/components/menubutton.md
+++ b/docs/14_0_0/components/menubutton.md
@@ -32,7 +32,7 @@ MenuButton displays different commands in a popup menu.
 | icon | null | String | Icon of the menu button.
 | iconPos | left | String | Position of the icon, valid values are left and right.
 | title | null | String | Advisory tooltip information.
-| appendTo | null | String | Appends the overlay to the element defined by search expression. Defaults to document body.
+| appendTo | null | String | Appends the overlay to the element defined by search expression. Defaults to the button itself.
 | menuStyleClass | null | String | Style class of the overlay menu element.
 | ariaLabel | null | String | The aria-label attribute is used to define a string that labels the current element for accessibility.
 | collision | flip | String | For the overlay menu that shows up when the menu button is clicked. When the overlay menu overflows the window in some direction, move it to an alternative position. Supported values are flip, fit, flipfit and none. See https://api.jqueryui.com/position/ for more details. Defaults to flip. When you the body of your layout does not scroll, you may also want to set the option maxHeight.
diff --git a/primefaces/src/main/java/org/primefaces/component/datepicker/DatePickerRenderer.java b/primefaces/src/main/java/org/primefaces/component/datepicker/DatePickerRenderer.java
index f54bd909c4..091101ef6d 100644
--- a/primefaces/src/main/java/org/primefaces/component/datepicker/DatePickerRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/datepicker/DatePickerRenderer.java
@@ -216,7 +216,7 @@ public class DatePickerRenderer extends BaseCalendarRenderer {
             .attr("timeSeparator", datePicker.getTimeSeparator(), ":")
             .attr("fractionSeparator", datePicker.getFractionSeparator(), ".")
             .attr("timeInput", datePicker.isTimeInput())
-            .attr("timeZone", Objects.toString(datePicker.getTimeZone(), null), null)
+            .attr("timeZone", datePicker.getTimeZone() == null ? null : CalendarUtils.calculateZoneId(datePicker.getTimeZone()).toString(), null)
             .attr("touchable", ComponentUtils.isTouchable(context, datePicker), true)
             .attr("lazyModel", datePicker.getModel() instanceof LazyDateMetadataModel, false);
 
diff --git a/primefaces/src/main/java/org/primefaces/component/menubutton/MenuButtonBase.java b/primefaces/src/main/java/org/primefaces/component/menubutton/MenuButtonBase.java
index 40f2c722be..542665c1d7 100644
--- a/primefaces/src/main/java/org/primefaces/component/menubutton/MenuButtonBase.java
+++ b/primefaces/src/main/java/org/primefaces/component/menubutton/MenuButtonBase.java
@@ -129,7 +129,7 @@ public abstract class MenuButtonBase extends AbstractMenu implements Widget {
     }
 
     public String getAppendTo() {
-        return (String) getStateHelper().eval(PropertyKeys.appendTo, "@(body)");
+        return (String) getStateHelper().eval(PropertyKeys.appendTo, null);
     }
 
     public void setAppendTo(String appendTo) {
diff --git a/primefaces/src/main/java/org/primefaces/util/LangUtils.java b/primefaces/src/main/java/org/primefaces/util/LangUtils.java
index 8f4a2b5fa7..f1d4c97505 100644
--- a/primefaces/src/main/java/org/primefaces/util/LangUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/LangUtils.java
@@ -23,6 +23,8 @@
  */
 package org.primefaces.util;
 
+import static java.util.Locale.ENGLISH;
+
 import java.beans.BeanInfo;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
@@ -484,6 +486,19 @@ public class LangUtils {
         return String.join(" ", values);
     }
 
+    /**
+     * Capitalizes the first character of the given string.
+     *
+     * @param name The string to capitalize.
+     * @return The capitalized string if the input is not blank; otherwise, returns the input string unchanged.
+     */
+    public static String capitalize(String name) {
+        if (isBlank(name)) {
+            return name;
+        }
+        return name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1);
+    }
+
 
     /**
      * <p>Checks whether the given String is a parsable number.</p>
diff --git a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
index a1552da866..d6e7f4c5c2 100644
--- a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
+++ b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
@@ -23,12 +23,14 @@
  */
 package org.primefaces.util;
 
+
 import java.beans.IntrospectionException;
 import java.beans.PropertyDescriptor;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Pattern;
+
 import javax.faces.FacesException;
 
 public interface PropertyDescriptorResolver {
@@ -99,7 +101,18 @@ public interface PropertyDescriptorResolver {
                     return new PropertyDescriptor(k, klazz);
                 }
                 catch (IntrospectionException e) {
-                    throw new FacesException(e);
+                    // try fallback without write method, our main concern is to read properties here
+                    try {
+                        // "is" + capitalize(k) is actually copied from another PropertyDescriptor constructor
+                        // it will somehow also work for "get" prefix
+                        return new PropertyDescriptor(k,
+                                klazz,
+                                "is" + LangUtils.capitalize(k),
+                                null);
+                    }
+                    catch (IntrospectionException e2) {
+                        throw new FacesException(e2);
+                    }
                 }
             });
         }
diff --git a/primefaces/src/main/resources/META-INF/primefaces.taglib.xml b/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
index da62752edb..ad277abe96 100644
--- a/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
+++ b/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
@@ -17699,7 +17699,7 @@
         </attribute>
         <attribute>
             <description>
-                <![CDATA[Appends the overlay to the element defined by search expression. Defaults to document body.]]>
+                <![CDATA[Appends the overlay to the element defined by search expression. Defaults to the button itself.]]>
             </description>
             <name>appendTo</name>
             <required>false</required>
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js b/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js
index 18fdc5050f..b36b8cf291 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/datepicker/0-datepicker.js
@@ -3284,14 +3284,14 @@
          * @throws {Error} If the input Java time zone string is in an invalid format.
          */
         convertTimeZone: function (javaTimeZone) {
-            if (!javaTimeZone || javaTimeZone.toUpperCase() === 'UTC' || /^(GMT|UTC)$/.test(javaTimeZone)) {
+            if (!javaTimeZone || ['ETC/UTC', 'ETC/GMT', 'UTC', 'GMT'].includes(javaTimeZone.toUpperCase())) {
                 return javaTimeZone;
             }
 
             // Extract the sign and the offset (hours and minutes)
             const matches = javaTimeZone.match(/^(GMT|UTC)([+-])(\d{2}):(\d{2})$/);
             if (!matches) {
-                throw new Error('Invalid GMT/UTC format');
+                return javaTimeZone;
             }
 
             const sign = matches[2];
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/menu/menu.menubutton.js b/primefaces/src/main/resources/META-INF/resources/primefaces/menu/menu.menubutton.js
index 8a49a63ef6..c515214eaa 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/menu/menu.menubutton.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/menu/menu.menubutton.js
@@ -30,7 +30,7 @@ PrimeFaces.widget.MenuButton = PrimeFaces.widget.TieredMenu.extend({
         this._super(cfg);
 
         this.trigger = this.jq.children('button');
-        this.cfg.appendTo = PrimeFaces.escapeClientId(this.trigger.attr("id"));
+        this.cfg.appendTo = this.cfg.appendTo || PrimeFaces.escapeClientId(this.trigger.attr("id"));
         this.cfg.disabled = this.trigger.is(':disabled');
         this.cfg.overlay = true;
         this.menu = null;


Test patch:
diff --git a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
index 5c0beb1e12..8bf271fdcf 100644
--- a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
@@ -23,9 +23,13 @@
  */
 package org.primefaces.util;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.beans.PropertyDescriptor;
+
 import javax.faces.FacesException;
 
 import org.junit.jupiter.api.Test;
@@ -75,6 +79,20 @@ class PropertyDescriptorResolverTest {
         assertThrows(FacesException.class, () -> propResolver.getValue(b, "unknown"));
     }
 
+    @Test
+    void getValueFromReadOnlyPropertyDescriptor() {
+        PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
+
+        F f = new F(true, false, "test");
+        assertEquals(true, propResolver.getValue(f, "readOnly1"));
+        assertEquals(false, propResolver.getValue(f, "readOnly2"));
+        assertEquals("test", propResolver.getValue(f, "readOnly3"));
+
+        assertNull(propResolver.get(f.getClass(), "readOnly1").getWriteMethod());
+        assertNull(propResolver.get(f.getClass(), "readOnly2").getWriteMethod());
+        assertNull(propResolver.get(f.getClass(), "readOnly3").getWriteMethod());
+    }
+
     private abstract class A {
 
         private String name;
@@ -169,4 +187,28 @@ class PropertyDescriptorResolverTest {
         Y,
         Z;
     }
+
+    public class F {
+        private boolean readOnly1;
+        private boolean readOnly2;
+        private String readOnly3;
+
+        public F(boolean readOnly1, boolean readOnly2, String readOnly3) {
+            this.readOnly1 = readOnly1;
+            this.readOnly2 = readOnly2;
+            this.readOnly3 = readOnly3;
+        }
+
+        public boolean isReadOnly1() {
+            return readOnly1;
+        }
+
+        public boolean isReadOnly2() {
+            return readOnly2;
+        }
+
+        public String getReadOnly3() {
+            return readOnly3;
+        }
+    }
 }