It's because of the way how the checkbox representing the state of <p:selectBooleanButton> was actually rendered by PrimeFaces SelectBooleanButtonRenderer:
<div id="formId:buttonId" type="button" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only">
<input id="formId:buttonId_input" name="formId:buttonId_input" type="checkbox" class="ui-helper-hidden">
<span class="ui-button-text">no</span>
</div>
The checkbox is completely hidden by the CSS display:none property in .ui-helper-hidden class and can thus never receive focus.
If we look at the checkbox counterpart <p:selectBooleanCheckbox>, which also replaces the checkbox by a visually more appealing widget which is actually focusable, then we see that the checkbox isn't completely hidden by CSS, but just made invisible by being wrapped in a <div> which is absolutely positioned by CSS position:absolute in .ui-helper-hidden-accessible class and is thus just overlayed by the checkbox widget:
<div id="formId:checkboxId" class="ui-chkbox ui-widget">
<div class="ui-helper-hidden-accessible">
<input id="formId:checkboxId_input" name="formId:checkboxId_input" type="checkbox">
</div>
<div class="ui-chkbox-box ui-widget ui-corner-all ui-state-default">
<span class="ui-chkbox-icon"></span>
</div>
</div>
I wouldn't consider the <p:selectBooleanButton> being unfocusable "expected" or "intuitive" behaviour and if I were you, I'd surely report this UX matter to PrimeFaces.
In the meanwhile, your best bet to workaround this is to create a custom renderer which overrides the encodeMarkup() method of the PrimeFaces SelectBooleanButtonRenderer as follows in order to remove the class="ui-helper-hidden" from the checkbox and wrap it in a <div class="ui-helper-hidden-accessible>, exactly as <p:selectBooleanCheckbox> is doing:
public class MySelectBooleanButtonRenderer extends SelectBooleanButtonRenderer {
@Override
protected void encodeMarkup(FacesContext context, SelectBooleanButton button) throws IOException {
ResponseWriter writer = context.getResponseWriter();
String clientId = button.getClientId(context);
boolean checked = Boolean.valueOf(ComponentUtils.getValueToRender(context, button));
boolean disabled = button.isDisabled();
String inputId = clientId + "_input";
String label = checked ? button.getOnLabel() : button.getOffLabel();
String icon = checked ? button.getOnIcon() : button.getOffIcon();
//button
writer.startElement("div", null);
writer.writeAttribute("id", clientId, "id");
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", button.resolveStyleClass(checked, disabled), null);
if(disabled) writer.writeAttribute("disabled", "disabled", null);
if(button.getTitle()!= null) writer.writeAttribute("title", button.getTitle(), null);
if(button.getStyle() != null) writer.writeAttribute("style", button.getStyle(), "style");
//input
writer.startElement("div", null); // <-- Added.
writer.writeAttribute("class", "ui-helper-hidden-accessible", null); // <-- Added.
writer.startElement("input", null);
writer.writeAttribute("id", inputId, "id");
writer.writeAttribute("name", inputId, null);
writer.writeAttribute("type", "checkbox", null);
// writer.writeAttribute("class", "ui-helper-hidden", null); <-- Removed.
if(checked) writer.writeAttribute("checked", "checked", null);
if(disabled) writer.writeAttribute("disabled", "disabled", null);
if(button.getOnchange() != null) writer.writeAttribute("onchange", button.getOnchange(), null);
writer.endElement("input");
writer.endElement("div"); // <-- Added.
//icon
if(icon != null) {
writer.startElement("span", null);
writer.writeAttribute("class", HTML.BUTTON_LEFT_ICON_CLASS + " " + icon, null);
writer.endElement("span");
}
//label
writer.startElement("span", null);
writer.writeAttribute("class", HTML.BUTTON_TEXT_CLASS, null);
writer.writeText(label, "value");
writer.endElement("span");
writer.endElement("div");
}
}
(look at the //input section, I have added <-- comments to explain which lines I've added/removed to the original source code which is been copypasted)
To get it to run, register it as follows in faces-config.xml:
<render-kit>
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.SelectBooleanButtonRenderer</renderer-type>
<renderer-class>com.example.MySelectBooleanButtonRenderer</renderer-class>
</renderer>
</render-kit>
(the component-family and renderer-type values are extracted from SelectBooleanButton component)
This works for me, well, kind of. The <p:selectBooleanButton> gets focus and you can use the spacebar to toggle the boolean state. However, the focus isn't visually visible in any way. This needs to be solved in the JavaScript side. The <div class="ui-button"> representing the button should get an .ui-state-focus class when the hidden checkbox gets focus. The following piece of jQuery achieves that:
$(".ui-button input[type=checkbox]").focus(function() {
$(this).closest(".ui-button").addClass("ui-state-focus");
}).blur(function() {
$(this).closest(".ui-button").removeClass("ui-state-focus");
});
Now it totally works for me.
In real PrimeFaces source code this should be solved in init() function of the PrimeFaces.widget.SelectBooleanButton function of the forms.js file.