Repository: primefaces/primefaces (java)

Issue:
SplitButton: Insufficient accessibility suitability, operation with screen reader not possible
### Description

During our accessibility assessment of our application the SplitButtons were criticised as a last problem.
Although normal usability via keyboard is completely fine, it is insufficient when screenreaders (JAWS or NVDA)
are used: 
It is possible to open the split button with the screenreader navigation, but it is impossible 
to navigate to the subelement buttons. Also the screenreaders do not properly read the elements and subelements of the split button.


### Describe the solution you would like

The testers analyzed the showcase:
https://www.primefaces.org/showcase/ui/button/splitButton.xhtml?jfwid=61a87
and gave some details and hints with a screenshot, which I attached.

![Image](https://github.com/user-attachments/assets/98c97008-2182-42f4-bee3-bf968725d60c)

The attribute „aria-haspopup“ is on the wrong html level. It should be moved to the affected interactive element, in
this case the second, highlighted <button> element in the screenshot, because this has the popup. It's value should not be "true" but "menu" for this situation.

The dropdown button has an improper name "ui-button". For usage with screenreader it should be adjustable, e.g. to contain a text like "more options", or "more actions."

The testers did not give us more recommendations how to finally fix the split button,
instead they pointed to an example how such a split button should be 
implemented to be accessibility compliant:
https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links/

### Additional context

We have a workaround: 
We used standard buttons for the actions as we only grouped 2 or 3 buttons with the split button and we had enough space in our tool bar.

Pull request:
Fix #13538: SplitButton aria menu button fixes
Fix #13538: SplitButton aria menu button fixes

Patch:
diff --git a/docs/10_0_0/core/ajaxProcessing.md b/docs/10_0_0/core/ajaxProcessing.md
index ecce00eeba..9be6b5eb44 100644
--- a/docs/10_0_0/core/ajaxProcessing.md
+++ b/docs/10_0_0/core/ajaxProcessing.md
@@ -1,3 +1,24 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element if possible.
+
+Alternatively, you can place an empty form at the start of your page to provide a fallback form for submission.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/10_0_0/core/multiviewstate.md b/docs/10_0_0/core/multiviewstate.md
index 1716c7640d..b7210dc3f4 100644
--- a/docs/10_0_0/core/multiviewstate.md
+++ b/docs/10_0_0/core/multiviewstate.md
@@ -12,6 +12,12 @@ Following components support MultiViewState:
 
 Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
 
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
+
 This can be configured in web.xml
 ```xml
 <context-param>
diff --git a/docs/11_0_0/core/ajaxProcessing.md b/docs/11_0_0/core/ajaxProcessing.md
index ecce00eeba..9be6b5eb44 100644
--- a/docs/11_0_0/core/ajaxProcessing.md
+++ b/docs/11_0_0/core/ajaxProcessing.md
@@ -1,3 +1,24 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element if possible.
+
+Alternatively, you can place an empty form at the start of your page to provide a fallback form for submission.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/11_0_0/core/multiviewstate.md b/docs/11_0_0/core/multiviewstate.md
index 1716c7640d..b7210dc3f4 100644
--- a/docs/11_0_0/core/multiviewstate.md
+++ b/docs/11_0_0/core/multiviewstate.md
@@ -12,6 +12,12 @@ Following components support MultiViewState:
 
 Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
 
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
+
 This can be configured in web.xml
 ```xml
 <context-param>
diff --git a/docs/12_0_0/core/ajaxProcessing.md b/docs/12_0_0/core/ajaxProcessing.md
index ecce00eeba..9be6b5eb44 100644
--- a/docs/12_0_0/core/ajaxProcessing.md
+++ b/docs/12_0_0/core/ajaxProcessing.md
@@ -1,3 +1,24 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element if possible.
+
+Alternatively, you can place an empty form at the start of your page to provide a fallback form for submission.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/12_0_0/core/multiviewstate.md b/docs/12_0_0/core/multiviewstate.md
index 1716c7640d..b7210dc3f4 100644
--- a/docs/12_0_0/core/multiviewstate.md
+++ b/docs/12_0_0/core/multiviewstate.md
@@ -12,6 +12,12 @@ Following components support MultiViewState:
 
 Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
 
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
+
 This can be configured in web.xml
 ```xml
 <context-param>
diff --git a/docs/13_0_0/core/ajaxProcessing.md b/docs/13_0_0/core/ajaxProcessing.md
index ecce00eeba..9be6b5eb44 100644
--- a/docs/13_0_0/core/ajaxProcessing.md
+++ b/docs/13_0_0/core/ajaxProcessing.md
@@ -1,3 +1,24 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element if possible.
+
+Alternatively, you can place an empty form at the start of your page to provide a fallback form for submission.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/13_0_0/core/multiviewstate.md b/docs/13_0_0/core/multiviewstate.md
index 5431e4d275..418a9ad4c0 100644
--- a/docs/13_0_0/core/multiviewstate.md
+++ b/docs/13_0_0/core/multiviewstate.md
@@ -15,6 +15,12 @@ Following components support MultiViewState:
 
 Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
 
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
+
 This can be configured in web.xml
 ```xml
 <context-param>
diff --git a/docs/14_0_0/core/ajaxProcessing.md b/docs/14_0_0/core/ajaxProcessing.md
index ecce00eeba..9be6b5eb44 100644
--- a/docs/14_0_0/core/ajaxProcessing.md
+++ b/docs/14_0_0/core/ajaxProcessing.md
@@ -1,3 +1,24 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element if possible.
+
+Alternatively, you can place an empty form at the start of your page to provide a fallback form for submission.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/14_0_0/core/multiviewstate.md b/docs/14_0_0/core/multiviewstate.md
index 5431e4d275..418a9ad4c0 100644
--- a/docs/14_0_0/core/multiviewstate.md
+++ b/docs/14_0_0/core/multiviewstate.md
@@ -15,6 +15,12 @@ Following components support MultiViewState:
 
 Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
 
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
+
 This can be configured in web.xml
 ```xml
 <context-param>
diff --git a/docs/15_0_0/core/ajaxProcessing.md b/docs/15_0_0/core/ajaxProcessing.md
index 711d0379a4..82fdef6d86 100644
--- a/docs/15_0_0/core/ajaxProcessing.md
+++ b/docs/15_0_0/core/ajaxProcessing.md
@@ -1,3 +1,22 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/15_0_0/core/multiviewstate.md b/docs/15_0_0/core/multiviewstate.md
index 56cdbc9da9..177f0d2ae2 100644
--- a/docs/15_0_0/core/multiviewstate.md
+++ b/docs/15_0_0/core/multiviewstate.md
@@ -15,6 +15,12 @@ Following components support MultiViewState:
 
 Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
 
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
+
 This can be configured in web.xml
 ```xml
 <context-param>
diff --git a/docs/16_0_0/core/ajaxProcessing.md b/docs/16_0_0/core/ajaxProcessing.md
index 711d0379a4..7af973d271 100644
--- a/docs/16_0_0/core/ajaxProcessing.md
+++ b/docs/16_0_0/core/ajaxProcessing.md
@@ -1,3 +1,24 @@
+# AJAX Processing Requirements
+
+AJAX requires a form to submit requests. When looking for a form to use, components will search in this order:
+
+1. If an explicit form ID is defined in the component configuration, it will use that form
+2. Otherwise, it will look for the closest parent form of the component
+3. If no parent form exists, it will use the first form found in the document
+
+If no form is available, AJAX will not function correctly and each AJAX request will throw an HTTP 405 Method Not Allowed. Therefore, ensure your components that use AJAX are placed within a form element if possible.
+
+Alternatively, you can place an empty form at the start of your page to provide a fallback form for submission.
+
+```xml
+<h:body>
+   <h:form id="emptyForm"></h:form>
+   <p:accordionPanel>
+   ...
+
+</h:body>
+```
+
 # Partial Processing
 
 In Partial Page Rendering, only specified components are rendered, similarly in Partial Processing
diff --git a/docs/16_0_0/core/multiviewstate.md b/docs/16_0_0/core/multiviewstate.md
index 7c6a781477..75cbf290d2 100644
--- a/docs/16_0_0/core/multiviewstate.md
+++ b/docs/16_0_0/core/multiviewstate.md
@@ -13,7 +13,13 @@ Following components support MultiViewState:
 - [TabView](/components/tabview.md)
 - [TreeTable](/components/treetable.md) (partial support)
 
-Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage.
+Per default MultiViewState is stored per session. As an alternative it can be stored per client window to support multi-window usage. 
+
+## General Requirements
+
+MultiViewState requires AJAX to work properly to persist its state. See [AJAX Processing Requirements](ajaxProcessing.md) for more information.
+
+## Client Window Mode
 
 This can be configured in web.xml
 ```xml
@@ -23,7 +29,7 @@ This can be configured in web.xml
 </context-param>
 ```
 
-## Requirements
+## Client Window Requirements
 
 For multi-window usage you need an implementation that adds jfwid-parameter to all request.
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/input/CalendarJava8View.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/input/CalendarJava8View.java
index 7ede301165..5df7f7ffea 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/input/CalendarJava8View.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/input/CalendarJava8View.java
@@ -139,6 +139,15 @@ public class CalendarJava8View implements Serializable {
         facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Date Selected", event.getObject().format(formatter)));
     }
 
+    public void onDateRangeSelect(SelectEvent<List<LocalDate>> event) {
+        FacesContext facesContext = FacesContext.getCurrentInstance();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
+        String range = event.getObject().get(0).format(formatter)
+                + " - "
+                + event.getObject().get(event.getObject().size() - 1).format(formatter);
+        facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Range Selected", range));
+    }
+
     public void onDateTimeSelect(SelectEvent<LocalDateTime> event) {
         FacesContext facesContext = FacesContext.getCurrentInstance();
         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
diff --git a/primefaces-showcase/src/main/webapp/ui/input/datepicker/datePickerJava8.xhtml b/primefaces-showcase/src/main/webapp/ui/input/datepicker/datePickerJava8.xhtml
index 4d7becb426..68c23ed3c8 100644
--- a/primefaces-showcase/src/main/webapp/ui/input/datepicker/datePickerJava8.xhtml
+++ b/primefaces-showcase/src/main/webapp/ui/input/datepicker/datePickerJava8.xhtml
@@ -70,7 +70,9 @@
 
                     <div class="field col-12 md:col-4">
                         <p:outputLabel for="@next" value="Week Picker" />
-                        <p:datePicker id="week" view="week" value="#{calendarJava8View.week}" />
+                        <p:datePicker id="week" view="week" value="#{calendarJava8View.week}">
+                            <p:ajax event="dateSelect" listener="#{calendarJava8View.onDateRangeSelect}" update="msgs" />
+                        </p:datePicker>
                     </div>
 
                     <div class="field col-12 md:col-4">
diff --git a/primefaces/src/main/frontend/packages/components/src/forms/forms.selectoneradio.widget.js b/primefaces/src/main/frontend/packages/components/src/forms/forms.selectoneradio.widget.js
index b5c6464c49..7f1ca05c68 100644
--- a/primefaces/src/main/frontend/packages/components/src/forms/forms.selectoneradio.widget.js
+++ b/primefaces/src/main/frontend/packages/components/src/forms/forms.selectoneradio.widget.js
@@ -171,7 +171,10 @@ PrimeFaces.widget.SelectOneRadio = class SelectOneRadio extends PrimeFaces.widge
                 else
                     radio = target.children('.ui-radiobutton-box'); //custom layout
 
-                radio.trigger('click.selectOneRadio');
+                // #13546 only trigger radio if its not already active
+                if (!radio.hasClass('ui-state-active')) {
+                    radio.trigger('click.selectOneRadio');
+                }
 
                 e.preventDefault();
             });
diff --git a/primefaces/src/main/frontend/packages/components/src/forms/forms.splitbutton.widget.ts b/primefaces/src/main/frontend/packages/components/src/forms/forms.splitbutton.widget.ts
index 2fe2c405a8..99f83470bb 100644
--- a/primefaces/src/main/frontend/packages/components/src/forms/forms.splitbutton.widget.ts
+++ b/primefaces/src/main/frontend/packages/components/src/forms/forms.splitbutton.widget.ts
@@ -177,6 +177,7 @@ export class SplitButton<Cfg extends SplitButtonCfg = SplitButtonCfg> extends Pr
         this.button.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);
 
         // toggle menu
+        this.menuButton.attr('aria-label', this.getAriaLabel('listLabel'));
         this.menuButton.on('click.splitbutton', () => {
             if(!this.cfg.disabled && this.menu.is(':hidden'))
                 this.show();
@@ -544,7 +545,7 @@ export class SplitButton<Cfg extends SplitButtonCfg = SplitButtonCfg> extends Pr
                 onEntered: () => {
                     this.bindPanelEvents();
 
-                    this.jq.attr('aria-expanded', "true");
+                    this.menuButton.attr('aria-expanded', "true");
 
                     if (this.cfg.filter && this.cfg.filterInputAutoFocus) {
                         this.filterInput.trigger('focus');
@@ -567,7 +568,7 @@ export class SplitButton<Cfg extends SplitButtonCfg = SplitButtonCfg> extends Pr
                     this.unbindPanelEvents();
                 },
                 onExited: () => {
-                    this.jq.attr('aria-expanded', "false");
+                    this.menuButton.attr('aria-expanded', "false");
                     this.menuitems.filter('.ui-state-hover').removeClass('ui-state-hover');
                     this.menuButton.removeClass('ui-state-focus');
                 }
diff --git a/primefaces/src/main/frontend/packages/core/index.ts b/primefaces/src/main/frontend/packages/core/index.ts
index c4f6eb9301..ddb45aefa3 100644
--- a/primefaces/src/main/frontend/packages/core/index.ts
+++ b/primefaces/src/main/frontend/packages/core/index.ts
@@ -1717,6 +1717,7 @@ declare global {
             "jumpToPageDropdownLabel"?: string;
             "jumpToPageInputLabel"?: string;
             "lastPageLabel"?: string;
+            "listLabel"?: string;
             "listView"?: string;
             "moveAllToSource"?: string;
             "moveAllToTarget"?: string;
diff --git a/primefaces/src/main/frontend/packages/core/src/core/core.ajax.ts b/primefaces/src/main/frontend/packages/core/src/core/core.ajax.ts
index 49d1071364..b7f734bc87 100644
--- a/primefaces/src/main/frontend/packages/core/src/core/core.ajax.ts
+++ b/primefaces/src/main/frontend/packages/core/src/core/core.ajax.ts
@@ -48,6 +48,22 @@ export class AjaxUtils {
         return content;
     }
 
+    /**
+     * Checks if the given form is a Faces form.
+     * @param form The form to check.
+     * @return `true` if the form is a Faces form.
+     */
+    isFacesForm(form: HTMLFormElement): boolean {
+        if (form.method === 'post') {
+            for (let child of form.children) {
+                if (child instanceof HTMLInputElement && child.name && child.name.includes(core.VIEW_STATE)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Resolves the URL which should be used for the POST request.
      * For portlets, a different URL is used.
@@ -651,7 +667,9 @@ export class AjaxRequest {
 
             //source has no parent form so use first form in document
             if (form.length === 0) {
-                form = $('form').eq(0);
+                form = $('form').filter(function(index, element) {
+                    return core.ajax.Utils.isFacesForm(element);
+                }).first();
             }
         }
 
diff --git a/primefaces/src/main/frontend/packages/core/src/core/core.ts b/primefaces/src/main/frontend/packages/core/src/core/core.ts
index 7f52948b5b..524b9ed847 100644
--- a/primefaces/src/main/frontend/packages/core/src/core/core.ts
+++ b/primefaces/src/main/frontend/packages/core/src/core/core.ts
@@ -129,6 +129,7 @@ const LocaleEnUs: PrimeType.Locale = {
         "jumpToPageDropdownLabel": "Jump to Page Dropdown",
         "jumpToPageInputLabel": "Jump to Page Input",
         "lastPageLabel": "Last Page",
+        "listLabel": "Options List",
         "listView": "List View",
         "moveAllToSource": "Move All to Source",
         "moveAllToTarget": "Move All to Target",
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 93e1af0b4d..536a3d49ec 100644
--- a/primefaces/src/main/java/org/primefaces/component/datepicker/DatePickerRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/datepicker/DatePickerRenderer.java
@@ -371,7 +371,8 @@ public class DatePickerRenderer extends BaseCalendarRenderer<DatePicker> {
 
                 return multi;
             case "range":
-                List<String> rangeStr = CalendarUtils.splitRange(submittedValue, datePicker.calculatePattern(), datePicker.getRangeSeparator());
+                List<String> rangeStr = CalendarUtils.splitRange(submittedValue, datePicker.calculatePattern(),
+                        datePicker.getRangeSeparator(), "week".equals(datePicker.getView()));
                 List<Object> range = new ArrayList<>();
                 if (rangeStr.size() == 2) {
                     for (int i = 0; i < rangeStr.size(); i++) {
diff --git a/primefaces/src/main/java/org/primefaces/component/splitbutton/SplitButtonRenderer.java b/primefaces/src/main/java/org/primefaces/component/splitbutton/SplitButtonRenderer.java
index 03fba4b9cf..0ea296f8fb 100644
--- a/primefaces/src/main/java/org/primefaces/component/splitbutton/SplitButtonRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/splitbutton/SplitButtonRenderer.java
@@ -104,9 +104,6 @@ public class SplitButtonRenderer extends MenuItemAwareRenderer<SplitButton> {
         writer.startElement("div", component);
         writer.writeAttribute("id", clientId, "id");
         writer.writeAttribute("class", styleClass, "id");
-        writer.writeAttribute(HTML.ARIA_HASPOPUP, Boolean.toString(hasOverlay), null);
-        writer.writeAttribute(HTML.ARIA_CONTROLS, menuId, null);
-        writer.writeAttribute(HTML.ARIA_EXPANDED, "false", null);
         if (component.getStyle() != null) {
             writer.writeAttribute("style", component.getStyle(), "id");
         }
@@ -114,7 +111,7 @@ public class SplitButtonRenderer extends MenuItemAwareRenderer<SplitButton> {
         encodeDefaultButton(context, component, buttonId);
         OverlayPanel customOverlay = component.getCustomOverlay();
         if (customOverlay != null || component.getElementsCount() > 0) {
-            encodeMenuIcon(context, component, menuButtonId);
+            encodeMenuIcon(context, component, menuButtonId, menuId, hasOverlay);
             if (hasOverlay) {
                 encodeMenu(context, component, menuId);
             }
@@ -185,7 +182,7 @@ public class SplitButtonRenderer extends MenuItemAwareRenderer<SplitButton> {
         writer.endElement("button");
     }
 
-    protected void encodeMenuIcon(FacesContext context, SplitButton component, String id) throws IOException {
+    protected void encodeMenuIcon(FacesContext context, SplitButton component, String id, String menuId, boolean hasOverlay) throws IOException {
         ResponseWriter writer = context.getResponseWriter();
         String buttonClass = SplitButton.MENU_ICON_BUTTON_CLASS;
         if (component.isDisabled()) {
@@ -197,6 +194,9 @@ public class SplitButtonRenderer extends MenuItemAwareRenderer<SplitButton> {
         writer.writeAttribute("name", id, null);
         writer.writeAttribute("type", "button", null);
         writer.writeAttribute("class", buttonClass, null);
+        writer.writeAttribute(HTML.ARIA_HASPOPUP, hasOverlay ? "menu" : "false", null);
+        writer.writeAttribute(HTML.ARIA_CONTROLS, menuId, null);
+        writer.writeAttribute(HTML.ARIA_EXPANDED, "false", null);
 
         if (component.isDisabled()) {
             writer.writeAttribute("disabled", "disabled", "disabled");
diff --git a/primefaces/src/main/java/org/primefaces/util/CalendarUtils.java b/primefaces/src/main/java/org/primefaces/util/CalendarUtils.java
index d7c3943bda..a8bfed15c6 100644
--- a/primefaces/src/main/java/org/primefaces/util/CalendarUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/CalendarUtils.java
@@ -41,7 +41,8 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.time.temporal.Temporal;
-import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
@@ -637,19 +638,30 @@ public class CalendarUtils {
         return now;
     }
 
-    public static List<String> splitRange(final String dateRange, final String pattern, final String separator) {
-        final String token = Constants.SPACE + separator + Constants.SPACE;
-        if (pattern.contains(token)) {
-            throw new FacesException("Pattern '" + pattern + "' contains separator '" + token + "'");
+    public static List<String> splitRange(String input, String datePattern, String rangeSeparator,
+                                          boolean week) {
+
+        // security check
+        String token = Constants.SPACE + rangeSeparator + Constants.SPACE;
+        if (datePattern.contains(token)) {
+            throw new FacesException("Pattern '" + datePattern + "' contains separator '" + token + "'");
         }
-        final List<String> dates = new ArrayList<>();
-        if (!dateRange.contains(token)) {
-            return dates;
+
+        // fast return
+        if (!input.contains(token)) {
+            return Collections.emptyList();
         }
-        final int index = dateRange.indexOf(token);
-        dates.add(dateRange.substring(0, index));
-        dates.add(dateRange.substring(index + token.length()));
-        return dates;
+
+        int tokenIndex = input.indexOf(token);
+        String from = input.substring(0, tokenIndex);
+        String to = input.substring(tokenIndex + token.length());
+        if (week) {
+            // hardcoded pattern, see JS:
+            // formattedValue += ' (' + this.options.locale.weekHeader + ' ' + week + ')';
+            to = to.substring(0, to.lastIndexOf(" ("));
+        }
+
+        return Arrays.asList(from, to);
     }
 
 }


Test patch:
diff --git a/primefaces/src/test/java/org/primefaces/util/CalendarUtilsTest.java b/primefaces/src/test/java/org/primefaces/util/CalendarUtilsTest.java
index 44e088c1d0..401933b6c8 100644
--- a/primefaces/src/test/java/org/primefaces/util/CalendarUtilsTest.java
+++ b/primefaces/src/test/java/org/primefaces/util/CalendarUtilsTest.java
@@ -217,18 +217,33 @@ class CalendarUtilsTest {
 
     @Test
     void splitRange() {
-        List<String> splitRange = CalendarUtils.splitRange("2021-03-01 - 2021-03-31", "yyyy-MM-dd", "-");
+        List<String> splitRange = CalendarUtils.splitRange("2021-03-01 - 2021-03-31", "yyyy-MM-dd", "-", false);
         assertEquals(2, splitRange.size());
         assertEquals("2021-03-01", splitRange.get(0));
         assertEquals("2021-03-31", splitRange.get(1));
-        splitRange = CalendarUtils.splitRange("2021-03-01", "yyyy-MM-dd", "-");
+        splitRange = CalendarUtils.splitRange("2021-03-01", "yyyy-MM-dd", "-", false);
         assertTrue(splitRange.isEmpty());
-        splitRange = CalendarUtils.splitRange("", "yyyy-MM-dd", "-");
+        splitRange = CalendarUtils.splitRange("", "yyyy-MM-dd", "-", false);
         assertTrue(splitRange.isEmpty());
 
         assertThrows(FacesException.class, () -> {
-            CalendarUtils.splitRange("2021 - 03 - 01 - 2021 - 03 - 31", "yyyy - MM - dd", "-");
+            CalendarUtils.splitRange("2021 - 03 - 01 - 2021 - 03 - 31", "yyyy - MM - dd", "-", false);
         });
     }
 
+    @Test
+    void splitRangeWeek() {
+        List<String> splitRange = CalendarUtils.splitRange("2021-03-01 - 2021-03-31 (Wk 21)", "yyyy-MM-dd", "-", true);
+        assertEquals(2, splitRange.size());
+        assertEquals("2021-03-01", splitRange.get(0));
+        assertEquals("2021-03-31", splitRange.get(1));
+        splitRange = CalendarUtils.splitRange("2021-03-01 (Wk 21)", "yyyy-MM-dd", "-", true);
+        assertTrue(splitRange.isEmpty());
+        splitRange = CalendarUtils.splitRange("", "yyyy-MM-dd (Wk 21)", "-", true);
+        assertTrue(splitRange.isEmpty());
+
+        assertThrows(FacesException.class, () -> {
+            CalendarUtils.splitRange("2021 - 03 - 01 - 2021 - 03 - 31 (Wk 21)", "yyyy - MM - dd", "-", true);
+        });
+    }
 }