Repository: primefaces/primefaces (java)

Issue:
DatePicker: Focus remains after selecting date, popup not re-opening
### Describe the bug

When selecting a date using the DatePicker component, the focus remains on the component. This causes the date selection popup to not re-open when clicking in the field again. This worked in PF13, since the focus was not on the component after selecting a date.

### Reproducer

1. Go to http://www.primefaces.org/showcase/ui/input/datepicker/datePicker.xhtml
2. Click on the "Basic" field
3. Select a date
4. Click on the "Basic" field again
5. The popup does not open

### Expected behavior

The popup should open every time when clicking on the field (except it is already open).

### PrimeFaces edition

Elite

### PrimeFaces version

14.0.2

### Theme

Rain

### JSF implementation

All

### JSF version

4.0

### Java version

17

### Browser(s)

_No response_

Pull request:
Fix #12361: 14.0.4 DatePicker allow click to popup panel again
Fix #12361: 14.0.4 DatePicker allow click to popup panel again

Patch:
diff --git a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
index d6e7f4c5c2..455ddf70f1 100644
--- a/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
+++ b/primefaces/src/main/java/org/primefaces/util/PropertyDescriptorResolver.java
@@ -23,21 +23,22 @@
  */
 package org.primefaces.util;
 
-
+import javax.faces.FacesException;
 import java.beans.IntrospectionException;
 import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
 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 {
 
-    PropertyDescriptor get(Class<?> klazz, String name);
+    PropertyDescriptor get(Class<?> klazz, String expression);
 
-    Object getValue(Object bean, String name);
+    Object getValue(Object obj, String expression);
+
+    void setValue(Object obj, String expression, Object value);
 
     void flush();
 
@@ -64,8 +65,8 @@ public interface PropertyDescriptorResolver {
             PropertyDescriptor pd = null;
             Class<?> parent = klazz;
 
-            for (String field : NESTED_EXPRESSION_PATTERN.split(expression)) {
-                pd = getSimpleProperty(parent, field);
+            for (String property : NESTED_EXPRESSION_PATTERN.split(expression)) {
+                pd = getSimpleProperty(parent, property);
                 parent = pd.getPropertyType();
             }
 
@@ -73,16 +74,45 @@ public interface PropertyDescriptorResolver {
         }
 
         @Override
-        public Object getValue(Object bean, String expression) {
+        public Object getValue(Object obj, String expression) {
             try {
-                for (String field : NESTED_EXPRESSION_PATTERN.split(expression)) {
-                    bean = getSimpleProperty(bean.getClass(), field).getReadMethod().invoke(bean);
-                    if (bean == null) {
+                for (String property : NESTED_EXPRESSION_PATTERN.split(expression)) {
+                    obj = getSimpleProperty(obj.getClass(), property).getReadMethod().invoke(obj);
+                    if (obj == null) {
                         break;
                     }
                 }
 
-                return bean;
+                return obj;
+            }
+            catch (ReflectiveOperationException e) {
+                throw new FacesException(e);
+            }
+        }
+
+        @Override
+        public void setValue(Object obj, String expression, Object value) {
+            try {
+                String[] expressions = NESTED_EXPRESSION_PATTERN.split(expression);
+
+                for (int i = 0; i < expressions.length; i++) {
+                    String property = expressions[i];
+                    boolean lastExpression = i + 1 == expressions.length;
+
+                    if (lastExpression) {
+                        Method writeMethod = getSimpleProperty(obj.getClass(), property).getWriteMethod();
+                        if (writeMethod == null) {
+                            throw new FacesException("property '" + property + "' is readonly");
+                        }
+                        writeMethod.invoke(obj, value);
+                    }
+                    else {
+                        obj = getSimpleProperty(obj.getClass(), property).getReadMethod().invoke(obj);
+                        if (obj == null) {
+                            throw new FacesException("property '" + property + "' resolved to null");
+                        }
+                    }
+                }
             }
             catch (ReflectiveOperationException e) {
                 throw new FacesException(e);
@@ -94,9 +124,9 @@ public interface PropertyDescriptorResolver {
             pdCache.clear();
         }
 
-        private PropertyDescriptor getSimpleProperty(Class<?> klazz, String field) {
+        private PropertyDescriptor getSimpleProperty(Class<?> klazz, String property) {
             String cacheKey = klazz.getName();
-            return pdCache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).computeIfAbsent(field, k -> {
+            return pdCache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).computeIfAbsent(property, k -> {
                 try {
                     return new PropertyDescriptor(k, klazz);
                 }
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 b36b8cf291..c4cfc85fa5 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
@@ -2214,9 +2214,16 @@
             if (this.documentClickListener) {
                 this.datepickerClick = true;
             }
-            // #11928 allow the input to be clicked again to close panel and allow typing of date
-            if (!this.datepickerFocus && this.isPanelVisible()) {
-                this.hideOverlay();
+            
+            if (this.isPanelVisible()) {
+                // #11928 allow the input to be clicked again to close panel and allow typing of date
+                if (!this.datepickerFocus) {
+                    this.hideOverlay();
+                }
+            } 
+            else if (this.options.showOnFocus) {
+                // #12361 allow the input to be clicked again to open the panel if showOnFocus is true
+                this.showOverlay();
             }
         },
 


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 8bf271fdcf..c5f1d2d3fd 100644
--- a/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/PropertyDescriptorResolverTest.java
@@ -37,7 +37,7 @@ import org.junit.jupiter.api.Test;
 class PropertyDescriptorResolverTest {
 
     @Test
-    void getPropertyDescriptor() {
+    void get() {
         PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
 
         PropertyDescriptor pd = propResolver.get(A.class, "name");
@@ -60,7 +60,7 @@ class PropertyDescriptorResolverTest {
     }
 
     @Test
-    void getValueFromPropertyDescriptor() {
+    void getValue() {
         PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
 
         B b = new B("b", true, Boolean.FALSE, DummyEnum.X);
@@ -80,7 +80,7 @@ class PropertyDescriptorResolverTest {
     }
 
     @Test
-    void getValueFromReadOnlyPropertyDescriptor() {
+    void getValueReadOnly() {
         PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
 
         F f = new F(true, false, "test");
@@ -93,7 +93,27 @@ class PropertyDescriptorResolverTest {
         assertNull(propResolver.get(f.getClass(), "readOnly3").getWriteMethod());
     }
 
-    private abstract class A {
+    @Test
+    void setValue() {
+        PropertyDescriptorResolver propResolver = new PropertyDescriptorResolver.DefaultResolver();
+
+        A a = new A(null, false, false);
+        assertNull(propResolver.getValue(a, "name"));
+        propResolver.setValue(a, "name", "Hans");
+        assertEquals("Hans", propResolver.getValue(a, "name"));
+
+        B b = new B("b", true, Boolean.FALSE, DummyEnum.X);
+        D d = new D("d", false, Boolean.FALSE, DummyEnum.Z, 123);
+        C c = new C(d);
+
+        assertEquals(123, propResolver.getValue(c, "b.foo"));
+        propResolver.setValue(c, "b.foo", 124);
+        assertEquals(124, propResolver.getValue(c, "b.foo"));
+
+        assertThrows(FacesException.class, () -> propResolver.setValue(new C(null), "b.dummyEnum", null));
+    }
+
+    private class A {
 
         private String name;
         private boolean bool1;