diff options
author | michaelvogt <michael@vaadin.com> | 2013-03-05 10:48:11 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2013-03-05 09:04:41 +0000 |
commit | b5c6f6cc0c75fa2849ad14dd395af69698440257 (patch) | |
tree | 62453df37489afee37458de72c4ec7f64b28c790 | |
parent | d81cd5b9c3eb4a32d4119f946706f76f305372e3 (diff) | |
download | vaadin-framework-b5c6f6cc0c75fa2849ad14dd395af69698440257.tar.gz vaadin-framework-b5c6f6cc0c75fa2849ad14dd395af69698440257.zip |
WAI-ARIA for form fields (#11180)
Changes in the base classes of the form fields for WAI-ARIA integration
Change-Id: I770082c353b1b0004875675e28f03d6a3e69f03f
5 files changed, 482 insertions, 1 deletions
diff --git a/client/src/com/vaadin/client/ui/AriaHelper.java b/client/src/com/vaadin/client/ui/AriaHelper.java new file mode 100644 index 0000000000..e762ba57ce --- /dev/null +++ b/client/src/com/vaadin/client/ui/AriaHelper.java @@ -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; + } +} diff --git a/client/src/com/vaadin/client/ui/HandlesAriaCaption.java b/client/src/com/vaadin/client/ui/HandlesAriaCaption.java new file mode 100644 index 0000000000..4eef0c5c25 --- /dev/null +++ b/client/src/com/vaadin/client/ui/HandlesAriaCaption.java @@ -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); +} diff --git a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java index 50de8e0936..ac5a08475e 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java @@ -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( diff --git a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java index 795b724292..cf19da3496 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java @@ -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) { diff --git a/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java b/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java new file mode 100644 index 0000000000..126caa5985 --- /dev/null +++ b/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java @@ -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; + } + +} |