Browse Source

WAI-ARIA for form fields (#11180)

Changes in the base classes of the form fields for WAI-ARIA integration

Change-Id: I770082c353b1b0004875675e28f03d6a3e69f03f
tags/7.1.0.beta1
michaelvogt 11 years ago
parent
commit
b5c6f6cc0c

+ 95
- 0
client/src/com/vaadin/client/ui/AriaHelper.java View File

@@ -0,0 +1,95 @@
package com.vaadin.client.ui;

import com.google.gwt.aria.client.Id;
import com.google.gwt.aria.client.InvalidValue;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Widget;

/**
* Helper class that helps to implement the WAI-ARIA functionality.
*/
public class AriaHelper {

/**
* Binds a caption (label in HTML speak) to the form element as required by
* WAI-ARIA specification.
*
* @param widget
* Element, that should be bound to the caption
* @param captionElement
* Element of the caption
*/
public static void bindCaption(Widget widget, Element captionElement) {
assert widget != null : "Valid Widget required";

ensureUniqueId(captionElement);

if (widget instanceof HandlesAriaCaption) {
((HandlesAriaCaption) widget).handleAriaCaption(captionElement);
} else if (captionElement != null) {
String ownerId = ensureUniqueId(widget.getElement());
captionElement.setAttribute("for", ownerId);

Roles.getTextboxRole().setAriaLabelledbyProperty(
widget.getElement(), Id.of(captionElement));
} else {
Roles.getTextboxRole().removeAriaLabelledbyProperty(
widget.getElement());
}
}

/**
* Handles the required actions depending of the input element being
* required or not.
*
* @param inputElement
* Element, typically an input element
* @param required
* boolean, true when the element is required
*/
public static void handleInputRequired(Element inputElement,
boolean required) {
if (required) {
Roles.getTextboxRole().setAriaRequiredProperty(inputElement, true);
} else {
Roles.getTextboxRole().removeAriaRequiredProperty(inputElement);
}
}

/**
* Handles the required actions depending of the input element contains
* unaccepted input
*
* @param inputElement
* Element, typically an input element
* @param showError
* boolean, true when the element input has an error
*/
public static void handleInputError(Element inputElement, boolean showError) {
if (showError) {
Roles.getTextboxRole().setAriaInvalidState(inputElement,
InvalidValue.TRUE);
} else {
Roles.getTextboxRole().removeAriaInvalidState(inputElement);
}
}

/**
* Makes sure that the provided element has an id attribute. Adds a new
* unique id if not.
*
* @param element
* Element to check
* @return String with the id of the element
*/
private static String ensureUniqueId(Element element) {
String id = element.getId();
if (null == id || id.isEmpty()) {
id = DOM.createUniqueId();
element.setId(id);
}
return id;
}
}

+ 20
- 0
client/src/com/vaadin/client/ui/HandlesAriaCaption.java View File

@@ -0,0 +1,20 @@
package com.vaadin.client.ui;

import com.google.gwt.user.client.Element;

/**
* Some Widgets need to handle the caption handling for WAI-ARIA themselfs, as
* for example the required ids need to be set in a specific way. In such a
* case, the Widget needs to implement this interface.
*/
public interface HandlesAriaCaption {

/**
* Called to bind the provided caption (label in HTML speak) element to the
* main input element of the Widget.
*
* @param captionElement
* Element of the caption
*/
void handleAriaCaption(Element captionElement);
}

+ 7
- 0
client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java View File

@@ -31,6 +31,7 @@ import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
import com.vaadin.client.ui.AbstractFieldConnector;
import com.vaadin.client.ui.AbstractLayoutConnector;
import com.vaadin.client.ui.LayoutClickEventHandler;
import com.vaadin.client.ui.AriaHelper;
import com.vaadin.client.ui.layout.ElementResizeEvent;
import com.vaadin.client.ui.layout.ElementResizeListener;
import com.vaadin.shared.AbstractFieldState;
@@ -258,6 +259,12 @@ public abstract class AbstractOrderedLayoutConnector extends
slot.setCaption(caption, iconUrlString, styles, error, showError,
required, enabled);

AriaHelper.handleInputRequired(child.getWidget().getElement(),
required);
AriaHelper.handleInputError(child.getWidget().getElement(),
showError);
AriaHelper.bindCaption(child.getWidget(), slot.getCaptionElement());

if (slot.hasCaption()) {
CaptionPosition pos = slot.getCaptionPosition();
getLayoutManager().addElementResizeListener(

+ 6
- 1
client/src/com/vaadin/client/ui/orderedlayout/Slot.java View File

@@ -18,6 +18,7 @@ package com.vaadin.client.ui.orderedlayout;

import java.util.List;

import com.google.gwt.aria.client.Roles;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
@@ -92,7 +93,6 @@ public final class Slot extends SimplePanel {

private ElementResizeListener spacingResizeListener;


// Caption is placed after component unless there is some part which
// moves it above.
private CaptionPosition captionPosition = CaptionPosition.RIGHT;
@@ -479,6 +479,11 @@ public final class Slot extends SimplePanel {
// character)
requiredIcon.setInnerHTML("*");
requiredIcon.setClassName("v-required-field-indicator");

// The star should not be read by the screen reader, as it is
// purely visual. Required state is set at the element level for
// the screen reader.
Roles.getTextboxRole().setAriaHiddenState(requiredIcon, true);
}
caption.appendChild(requiredIcon);
} else if (requiredIcon != null) {

+ 354
- 0
uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java View File

@@ -0,0 +1,354 @@
package com.vaadin.tests.layouts;


import java.util.ArrayList;
import java.util.List;

import com.vaadin.data.Item;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.server.ThemeResource;
import com.vaadin.server.UserError;
import com.vaadin.tests.components.TestBase;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.DateField;
import com.vaadin.ui.FormLayout;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Layout;
import com.vaadin.ui.NativeSelect;
import com.vaadin.ui.OptionGroup;
import com.vaadin.ui.PasswordField;
import com.vaadin.ui.TextArea;
import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout;

public class CaptionsInLayoutsWaiAria extends TestBase {

private static final Object CAPTION = "CAPTION";
private static final Object CLASS = "C";
private static final Object WIDTH = "W";

private NativeSelect layoutSelect;
private Layout layout;
private VerticalLayout verticalLayout;
private HorizontalLayout horizontalLayout;
private GridLayout gridLayout;
private FormLayout formLayout;
private List<AbstractField<?>> components = new ArrayList<AbstractField<?>>();
private CssLayout cssLayout;
private HorizontalLayout layoutParent = new HorizontalLayout();

@Override
protected void setup() {
// setTheme("tests-tickets");
addComponent(createLayoutSelect());
addComponent(toggleRequired());
// addComponent(toggleCaptions());
// addComponent(toggleError());
addComponent(toggleIcon());
addComponent(toggleReadOnly());
addComponent(toggleInvalid());
addComponent(addCaptionText());
// layoutParent.addComponent(new
// NativeButton("Button right of layout"));
addComponent(layoutParent);
// addComponent(new NativeButton("Button below layout"));
createComponents();
layoutSelect.setValue(layoutSelect.getItemIds().iterator().next());
}

private Component addCaptionText() {
Button b = new Button("Add caption text");
b.addListener(new ClickListener() {

@Override
public void buttonClick(ClickEvent event) {
prependCaptions("a");
}
});
return b;
}

protected void prependCaptions(String prepend) {
for (AbstractField<?> c : components) {
c.setCaption(prepend + c.getCaption());
}

}

private Component toggleRequired() {
CheckBox requiredToggle = new CheckBox();
requiredToggle.setImmediate(true);
requiredToggle.setCaption("Required");
requiredToggle.addListener(new ValueChangeListener() {

@Override
public void valueChange(ValueChangeEvent event) {
setRequired((Boolean) event.getProperty().getValue());
}
});
return requiredToggle;
}

private Component toggleIcon() {
CheckBox iconToggle = new CheckBox();
iconToggle.setImmediate(true);
iconToggle.setCaption("Icons");
iconToggle.addListener(new ValueChangeListener() {

@Override
public void valueChange(ValueChangeEvent event) {
setIcon((Boolean) event.getProperty().getValue());
}
});
return iconToggle;
}

private Component toggleReadOnly() {
CheckBox readOnlyToggle = new CheckBox();
readOnlyToggle.setImmediate(true);
readOnlyToggle.setCaption("Read only");
readOnlyToggle.addValueChangeListener(new ValueChangeListener() {
@Override
public void valueChange(ValueChangeEvent event) {
setReadOnly((Boolean) event.getProperty().getValue());
}
});

return readOnlyToggle;
}

private Component toggleInvalid() {
CheckBox invalid = new CheckBox("Invalid");
invalid.setImmediate(true);
invalid.addValueChangeListener(new ValueChangeListener() {
@Override
public void valueChange(ValueChangeEvent event) {
setInvalid((Boolean) event.getProperty().getValue());
}
});

return invalid;
}

protected void setInvalid(boolean value) {
UserError userError = null;
if (value) {
userError = new UserError(
"Der eingegebene Wert ist nicht zulässig!");
}

for (AbstractField<?> c : components) {
c.setComponentError(userError);
}
}

protected void setRequired(boolean value) {
for (AbstractField<?> c : components) {
c.setRequired(value);
}

}

protected void setIcon(boolean value) {
for (AbstractField<?> c : components) {
if (!value) {
c.setIcon(null);
} else {
c.setIcon(new ThemeResource("../runo/icons/16/ok.png"));
}
}

}

protected void setReadOnly(boolean value) {
for (AbstractField<?> c : components) {
c.setReadOnly(value);
}
}

private Component toggleError() {
CheckBox errorToggle = new CheckBox();
errorToggle.setImmediate(true);
errorToggle.setCaption("Error");
errorToggle.addListener(new ValueChangeListener() {

@Override
public void valueChange(ValueChangeEvent event) {
setError((Boolean) event.getProperty().getValue());
}
});
return errorToggle;
}

protected void setError(boolean value) {
for (AbstractField<?> c : components) {
if (value) {
c.setComponentError(new UserError("error"));
} else {
c.setComponentError(null);

}
}

}

private void createComponents() {
components.add(new TextField("Default TextBox"));
components.add(new TextArea("Default TextArea."));
// components.add(new RichTextArea("Default RichtTextArea"));
components.add(new PasswordField("Default Password"));
components.add(new DateField("Default DateField"));

// PopupDateField popupDateField = new
// PopupDateField("Default DateField");
// popupDateField.setTextFieldEnabled(false);
// components.add(popupDateField);

components.add(new CheckBox("Default CheckBox"));

ComboBox comboBox = new ComboBox("Default ComboBox");
comboBox.addItem("Item1");
components.add(comboBox);

OptionGroup radioGroup = new OptionGroup("Single Items");
radioGroup.addItem("Single Item 1");
radioGroup.addItem("Single Item 2");
radioGroup.setMultiSelect(false);
components.add(radioGroup);

OptionGroup checkGroup = new OptionGroup("Multi Items");
checkGroup.addItem("Multi Item 1");
checkGroup.addItem("Multi Item 2");
checkGroup.setMultiSelect(true);
components.add(checkGroup);

// Tree tree = new Tree();
// tree.setCaption("tree");
// tree.addItem("single item");
// components.add(tree);
}

private void setLayout(Layout newLayout) {
if (layout == null) {
layoutParent.addComponent(newLayout, 0);
} else {
layoutParent.replaceComponent(layout, newLayout);
}
layout = newLayout;

for (Component c : components) {
if (c.getParent() != layout) {
layout.addComponent(c);
}
}

}

private Layout getLayout(String caption,
Class<? extends Layout> layoutClass, String width) {
Layout l;
if (layoutClass == VerticalLayout.class) {
if (verticalLayout == null) {
verticalLayout = new VerticalLayout();
verticalLayout.setStyleName("borders");
}
l = verticalLayout;
} else if (layoutClass == HorizontalLayout.class) {
if (horizontalLayout == null) {
horizontalLayout = new HorizontalLayout();
horizontalLayout.setStyleName("borders");
}
l = horizontalLayout;
} else if (layoutClass == GridLayout.class) {
if (gridLayout == null) {
gridLayout = new GridLayout();
gridLayout.setStyleName("borders");
}
l = gridLayout;
} else if (layoutClass == CssLayout.class) {
if (cssLayout == null) {
cssLayout = new CssLayout();
cssLayout.setStyleName("borders");
}
l = cssLayout;
} else if (layoutClass == FormLayout.class) {
if (formLayout == null) {
formLayout = new FormLayout();
formLayout.setStyleName("borders");
}
l = formLayout;
} else {
return null;
}

l.setCaption(caption);
if (width.equals("auto")) {
width = null;
}

l.setWidth(width);

// addComponent(l);

return l;
}

private Component createLayoutSelect() {
layoutSelect = new NativeSelect("Layout");
layoutSelect.addContainerProperty(CAPTION, String.class, "");
layoutSelect.addContainerProperty(CLASS, Class.class, "");
layoutSelect.addContainerProperty(WIDTH, String.class, "");
layoutSelect.setItemCaptionPropertyId(CAPTION);
layoutSelect.setNullSelectionAllowed(false);

for (Class<?> cls : new Class[] { HorizontalLayout.class,
VerticalLayout.class, GridLayout.class, CssLayout.class,
FormLayout.class }) {
for (String width : new String[] { "auto" }) {
Object id = layoutSelect.addItem();
Item i = layoutSelect.getItem(id);
i.getItemProperty(CAPTION).setValue(
cls.getSimpleName() + ", " + width);
i.getItemProperty(CLASS).setValue(cls);
i.getItemProperty(WIDTH).setValue(width);
}

}
layoutSelect.setImmediate(true);
layoutSelect.addListener(new ValueChangeListener() {

@Override
@SuppressWarnings("unchecked")
public void valueChange(ValueChangeEvent event) {
Item i = layoutSelect.getItem(event.getProperty().getValue());

setLayout(getLayout((String) i.getItemProperty(CAPTION)
.getValue(), (Class<? extends Layout>) i
.getItemProperty(CLASS).getValue(), (String) i
.getItemProperty(WIDTH).getValue()));
}
});

return layoutSelect;
}

@Override
protected String getDescription() {
return "Tests what happens when the caption changes in various layouts. Behavior should be consistent.";
}

@Override
protected Integer getTicketNumber() {
return 5424;
}

}

Loading…
Cancel
Save