Eugenio Cuevas
Eugenio Cuevas

Reputation: 11078

How to focus a p:selectBooleanButton component using tab key?

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

Answers (2)

Mauro Molinari
Mauro Molinari

Reputation: 1405

An alternative way to apply the exact same workaround proposed by BalusC is the following.

Benefits:

  • you only need to create and declare the renderer class: no need to add further JavaScript code to your facelet in order to add the visual clue of the focused state
  • the renderer extension does not replace the whole super 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 computed

The 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

BalusC
BalusC

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");
});

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.

Upvotes: 7

Related Questions