Reputation: 11078
We are starting to develop with PrimeFaces 3.4, JSF 2.0 and Tomcat 7.0. We are facing the problem that when we make a form page, we can navigate with the tab button on all the PrimeFaces input components, expect of <p:selectBooleanButton>
. For example,
<h:form id="formId">
<p:inputText id="inputId1" />
<p:inputText id="inputId2" />
<p:selectBooleanButton id="buttonId" onLabel="Yes" offLabel="No" />
<p:inputText id="inputId3" />
<p:inputText id="inputId4" />
</h:form>
Pressing tab in inputId2
goes directly to inputId3
. Is this the expected behaviour? Is there any workaround?
Upvotes: 2
Views: 2275
Reputation: 1405
An alternative way to apply the exact same workaround proposed by BalusC is the following.
Benefits:
encodeMarkup
implementation, but rather decorates it; this should be a bit more robust in case a PF upgrade changes the super implementation slightly, unless they completely change the DOM structure and/or the way in which inputId is computedThe idea is to add a JavaScript script that manipulates the original markup in order to provide the focusability to the component, using the same technique (based on applying the appropriate CSS classes) proposed by BalusC.
package com.example;
import java.io.IOException;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import org.primefaces.component.selectbooleanbutton.SelectBooleanButton;
import org.primefaces.component.selectbooleanbutton.SelectBooleanButtonRenderer;
public class MySelectBooleanButtonRenderer extends SelectBooleanButtonRenderer {
@Override
protected void
encodeMarkup(FacesContext context, SelectBooleanButton button)
throws IOException {
super.encodeMarkup(context, button);
ResponseWriter writer = context.getResponseWriter();
writer.startElement("script", null);
writer.writeAttribute("type", "text/javascript", null);
writer.append(getMakeButtonFocusableScript(button.getClientId(context)));
writer.endElement("script");
}
protected String getMakeButtonFocusableScript(final String clientId) {
String inputId = clientId + "_input";
return "{\r\n"
+ " var input = document.getElementById('"
+ inputId
+ "');\r\n"
+ " input.classList.remove('ui-helper-hidden');\r\n"
+ " var mainDiv = document.getElementById('"
+ clientId
+ "');\r\n"
+ " var newDiv = document.createElement('div');\r\n"
+ " newDiv.setAttribute('class', 'ui-helper-hidden-accessible');\r\n"
+ " newDiv.appendChild(input);\r\n"
+ " mainDiv.appendChild(newDiv);\r\n"
+ " input.onfocus = function() {document.getElementById('"
+ clientId + "').classList.add('ui-state-focus'); };\r\n"
+ " input.onblur = function() { document.getElementById('"
+ clientId + "').classList.remove('ui-state-focus'); };\r\n"
+ "}";
}
}
Upvotes: 0
Reputation: 1108742
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");
});
In real PrimeFaces source code this should be solved in init()
function of the PrimeFaces.widget.SelectBooleanButton
function of the forms.js
file.
Upvotes: 7