Repository: primefaces/primefaces (java)

Issue:
SelectOneRadio: aria-labelledby attribute is using text instead of the ID.
### Describe the bug

According to the aria-labelledby specification, its value should be:

"The id or space-separated list of element ids that describe the current element."
(Source: [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby)).

But the SelectOneRadio is using the text instead of the element ID.

However, this issue cannot be reproduced in the showcase because it uses `<h5>` instead of `<p:outputLabel>`.

Other option is use just the aria-label instead of aria-labelledby. Remembering that "aria-labelledby takes precedence over all other methods of providing an accessible name, including aria-label, `<label>`, and the element's inner text."


### Reproducer


```
<p:outputLabel value="Label Text" for="idRadio"/>
<p:selectOneRadio id="idRadio"
				value="#{controller.value}">
				<f:selectItem itemLabel="Yes" itemValue="0" />
				<f:selectItem itemLabel="No" itemValue="1" />
</p:selectOneRadio>
```

**Actual Output**
The generated HTML is as follows:
`<div id="form:idRadio" role="radiogroup" aria-labelledby="Label Text" ...>
`


### Expected behavior

The expected HTML should be:
`<div id="form:idRadio" role="radiogroup" aria-labelledby="idRadio" ...>
`


### PrimeFaces edition

Community

### PrimeFaces version

14.0.8

### Theme

custom

### JSF implementation

Mojarra

### JSF version

2.3

### Java version

21

### Browser(s)

_No response_

Pull request:
Fix #12953: 14.0.9 SelectOneRadio proper identification by label
Fix #12953: 14.0.9 SelectOneRadio proper identification by label

Patch:
diff --git a/pom.xml b/pom.xml
index 47fb0cc0e7..48de3eafd8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
     <groupId>org.primefaces</groupId>
     <artifactId>primefaces-parent</artifactId>
     <packaging>pom</packaging>
-    <version>14.0.8</version>
+    <version>14.0.9-SNAPSHOT</version>
     <name>PrimeFaces Parent</name>
     <url>https://www.primefaces.org</url>
 
diff --git a/primefaces-cli/pom.xml b/primefaces-cli/pom.xml
index 2db0d4def7..52720857d9 100644
--- a/primefaces-cli/pom.xml
+++ b/primefaces-cli/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-parent</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces-selenium/pom.xml b/primefaces-selenium/pom.xml
index d023249813..091610cd9a 100644
--- a/primefaces-selenium/pom.xml
+++ b/primefaces-selenium/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-parent</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces-selenium/primefaces-selenium-components/pom.xml b/primefaces-selenium/primefaces-selenium-components/pom.xml
index f2ec838165..55f7fbb592 100644
--- a/primefaces-selenium/primefaces-selenium-components/pom.xml
+++ b/primefaces-selenium/primefaces-selenium-components/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-selenium</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces-selenium/primefaces-selenium-core/pom.xml b/primefaces-selenium/primefaces-selenium-core/pom.xml
index 800ad574b8..402325d6ba 100644
--- a/primefaces-selenium/primefaces-selenium-core/pom.xml
+++ b/primefaces-selenium/primefaces-selenium-core/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-selenium</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces-showcase/pom.xml b/primefaces-showcase/pom.xml
index ff7b634f24..4f7846d654 100644
--- a/primefaces-showcase/pom.xml
+++ b/primefaces-showcase/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-parent</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces/pom.xml b/primefaces/pom.xml
index 4f66442763..b1627e183a 100644
--- a/primefaces/pom.xml
+++ b/primefaces/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-parent</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadio.java b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadio.java
index 443c6d3e15..9f2701700c 100644
--- a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadio.java
+++ b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadio.java
@@ -47,4 +47,24 @@ public class SelectOneRadio extends SelectOneRadioBase {
     public String getRadioButtonId(FacesContext context, int index) {
         return this.getClientId(context) + UINamingContainer.getSeparatorChar(context) + index;
     }
+
+    @Override
+    public String getInputClientId() {
+        return getRadioButtonId(getFacesContext(), 0);
+    }
+
+    @Override
+    public String getValidatableInputClientId() {
+        return getRadioButtonId(getFacesContext(), 0);
+    }
+
+    @Override
+    public String getLabelledBy() {
+        return (String) getStateHelper().get("labelledby");
+    }
+
+    @Override
+    public void setLabelledBy(String labelledBy) {
+        getStateHelper().put("labelledby", labelledBy);
+    }
 }
\ No newline at end of file
diff --git a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioBase.java b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioBase.java
index cf016795b8..b6956e5c1f 100644
--- a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioBase.java
+++ b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioBase.java
@@ -23,12 +23,13 @@
  */
 package org.primefaces.component.selectoneradio;
 
-import javax.faces.component.html.HtmlSelectOneRadio;
-
 import org.primefaces.component.api.FlexAware;
+import org.primefaces.component.api.InputHolder;
 import org.primefaces.component.api.Widget;
 
-public abstract class SelectOneRadioBase extends HtmlSelectOneRadio implements Widget, FlexAware {
+import javax.faces.component.html.HtmlSelectOneRadio;
+
+public abstract class SelectOneRadioBase extends HtmlSelectOneRadio implements Widget, FlexAware, InputHolder {
 
     public static final String COMPONENT_FAMILY = "org.primefaces.component";
 
diff --git a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java
index 7cfd444d5b..c4a76b85bf 100644
--- a/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/selectoneradio/SelectOneRadioRenderer.java
@@ -111,7 +111,7 @@ public class SelectOneRadioRenderer extends SelectOneRenderer {
                 .add(SelectOneRadio.STYLE_CLASS)
                 .add(radio.isReadonly(), "ui-state-readonly")
                 .build();
-        String labelledBy = radio.getLabel();
+        String labelledBy = radio.getLabelledBy();
 
         writer.startElement("div", radio);
         writer.writeAttribute("id", clientId, "id");
@@ -251,7 +251,7 @@ public class SelectOneRadioRenderer extends SelectOneRenderer {
                     .add(SelectOneRadio.STYLE_CLASS)
                     .add(radio.isReadonly(), "ui-state-readonly")
                     .build();
-            String labelledBy = radio.getLabel();
+            String labelledBy = radio.getLabelledBy();
             writer.startElement("span", radio);
             writer.writeAttribute("id", radio.getClientId(context), "id");
             writer.writeAttribute("role", "radiogroup", null);


Test patch:
diff --git a/primefaces-integration-tests-jakarta/pom.xml b/primefaces-integration-tests-jakarta/pom.xml
index 5cf48b172f..3794d925b5 100644
--- a/primefaces-integration-tests-jakarta/pom.xml
+++ b/primefaces-integration-tests-jakarta/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-parent</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces-integration-tests/pom.xml b/primefaces-integration-tests/pom.xml
index e678d8dd48..7157c297ee 100644
--- a/primefaces-integration-tests/pom.xml
+++ b/primefaces-integration-tests/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.primefaces</groupId>
         <artifactId>primefaces-parent</artifactId>
-        <version>14.0.8</version>
+        <version>14.0.9-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
diff --git a/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio001.xhtml b/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio001.xhtml
index 71c321cdea..ca9f64f23e 100644
--- a/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio001.xhtml
+++ b/primefaces-integration-tests/src/main/webapp/selectoneradio/selectOneRadio001.xhtml
@@ -12,6 +12,7 @@
     <h:body>
 
         <h:form id="form">
+            <p:outputLabel id="outputlabel" value="Drivers Label" for="selectoneradio"/>
             <p:selectOneRadio id="selectoneradio" value="#{selectOneRadio001.value}">
                 <f:selectItems value="#{selectOneRadio001.drivers}" var="driver" itemLabel="#{driver.name}" itemValue="#{driver.id}"/>
                 <p:ajax process="@this"/>
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio001Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio001Test.java
index 5210ef021c..61fe9b2993 100644
--- a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio001Test.java
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/selectoneradio/SelectOneRadio001Test.java
@@ -23,8 +23,11 @@
  */
 package org.primefaces.integrationtests.selectoneradio;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import org.primefaces.selenium.AbstractPrimePage;
+import org.primefaces.selenium.AbstractPrimePageTest;
+import org.primefaces.selenium.component.CommandButton;
+import org.primefaces.selenium.component.OutputLabel;
+import org.primefaces.selenium.component.SelectOneRadio;
 
 import org.json.JSONObject;
 import org.junit.jupiter.api.DisplayName;
@@ -33,10 +36,9 @@ import org.junit.jupiter.api.Test;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
-import org.primefaces.selenium.AbstractPrimePage;
-import org.primefaces.selenium.AbstractPrimePageTest;
-import org.primefaces.selenium.component.CommandButton;
-import org.primefaces.selenium.component.SelectOneRadio;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 
 class SelectOneRadio001Test extends AbstractPrimePageTest {
 
@@ -163,6 +165,24 @@ class SelectOneRadio001Test extends AbstractPrimePageTest {
         assertConfiguration(selectOneRadio.getWidgetConfiguration());
     }
 
+    @Test
+    @Order(1)
+    @DisplayName("SelectOneRadio: ensure aria-labelledby is set from OutputLabel")
+    void ariaLabelledBy(Page page) {
+        // Arrange
+        SelectOneRadio selectOneRadio = page.selectOneRadio;
+        OutputLabel outputLabel = page.outputLabel;
+        assertEquals(4, selectOneRadio.getItemsSize());
+        assertEquals("Lewis", selectOneRadio.getSelectedLabel());
+
+        // Act
+
+
+        // Assert
+        assertEquals(outputLabel.getId(), selectOneRadio.getDomAttribute("aria-labelledby"));
+        assertConfiguration(selectOneRadio.getWidgetConfiguration());
+    }
+
     private void assertConfiguration(JSONObject cfg) {
         assertNoJavascriptErrors();
         System.out.println("SelectOneRadio Config = " + cfg);
@@ -170,6 +190,9 @@ class SelectOneRadio001Test extends AbstractPrimePageTest {
     }
 
     public static class Page extends AbstractPrimePage {
+        @FindBy(id = "form:outputlabel")
+        OutputLabel outputLabel;
+
         @FindBy(id = "form:selectoneradio")
         SelectOneRadio selectOneRadio;