]> source.dussan.org Git - vaadin-framework.git/commitdiff
WAI-ARIA fields (#11180)
authormichaelvogt <michael@vaadin.com>
Wed, 20 Mar 2013 08:37:11 +0000 (10:37 +0200)
committermichaelvogt <michael@vaadin.com>
Wed, 20 Mar 2013 08:37:11 +0000 (10:37 +0200)
Field implementations of the WAI-ARIA changes in the base classes

Change-Id: Ie51e76130f3f9976a32c373334b709f0f5b68f1a

16 files changed:
WebContent/VAADIN/themes/base/common/common.scss
client/src/com/vaadin/client/VCaption.java
client/src/com/vaadin/client/VErrorMessage.java
client/src/com/vaadin/client/VTooltip.java
client/src/com/vaadin/client/ui/AriaHelper.java
client/src/com/vaadin/client/ui/HandlesAriaCaption.java
client/src/com/vaadin/client/ui/VCalendarPanel.java
client/src/com/vaadin/client/ui/VCheckBox.java
client/src/com/vaadin/client/ui/VFilterSelect.java
client/src/com/vaadin/client/ui/VFormLayout.java
client/src/com/vaadin/client/ui/VOptionGroup.java
client/src/com/vaadin/client/ui/VPopupCalendar.java
client/src/com/vaadin/client/ui/VTextField.java
client/src/com/vaadin/client/ui/VTextualDate.java
client/src/com/vaadin/client/ui/tree/TreeConnector.java
uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java

index e801ec28218b5ca34301d4b45e90fdcdd59a1761..27c6dc949cc19cce87141917034fc15f76f603f7 100644 (file)
@@ -238,4 +238,10 @@ input::-ms-clear {
        height: 0;
 }
 
+.v-assistive-device-only {
+       position: absolute;
+       left: -2000px;
+       width: 10px;
+       overflow: hidden;
+}
 }
\ No newline at end of file
index 47287636c47ec597fc70c2c2689a0a09339c461f..607a0f0b0ac33e2e774de89164c54a59b646bdd0 100644 (file)
 
 package com.vaadin.client;
 
+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;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
 import com.vaadin.client.communication.StateChangeEvent;
 import com.vaadin.client.ui.AbstractFieldConnector;
+import com.vaadin.client.ui.AriaHelper;
 import com.vaadin.client.ui.Icon;
 import com.vaadin.shared.AbstractComponentState;
 import com.vaadin.shared.AbstractFieldState;
@@ -88,6 +91,8 @@ public class VCaption extends HTML {
         this.client = client;
         owner = component;
 
+        AriaHelper.bindCaption(component.getWidget(), getElement());
+
         if (client != null && owner != null) {
             setOwnerPid(getElement(), owner.getConnectorId());
         }
@@ -209,11 +214,21 @@ public class VCaption extends HTML {
 
                 DOM.insertChild(getElement(), requiredFieldIndicator,
                         getInsertPosition(InsertPosition.REQUIRED));
+
+                Roles.getTextboxRole().setAriaRequiredProperty(
+                        owner.getWidget().getElement(), true);
+
+                // Hide the required indicator from assistive device
+                Roles.getTextboxRole().setAriaHiddenState(
+                        requiredFieldIndicator, true);
             }
         } else if (requiredFieldIndicator != null) {
             // Remove existing
             DOM.removeChild(getElement(), requiredFieldIndicator);
             requiredFieldIndicator = null;
+
+            Roles.getTextboxRole().removeAriaRequiredProperty(
+                    owner.getWidget().getElement());
         }
 
         if (showError) {
index a384b451dd8387e979b02df93680ecfbada3d02b..2e42b98a05431069f41c23042d3aad6b85d4aacc 100644 (file)
@@ -31,6 +31,9 @@ public class VErrorMessage extends FlowPanel {
     public VErrorMessage() {
         super();
         setStyleName(CLASSNAME);
+
+        // Needed for binding with WAI-ARIA attributes
+        getElement().setId(DOM.createUniqueId());
     }
 
     /**
index 759b90a8cd38d8cc8c968eb3e31ec2e71de7b1c5..e6d9a79a5b68aec19134e498a0e4948136299e0e 100644 (file)
  */
 package com.vaadin.client;
 
+import com.google.gwt.aria.client.Id;
+import com.google.gwt.aria.client.LiveValue;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.dom.client.MouseMoveEvent;
@@ -54,6 +62,9 @@ public class VTooltip extends VOverlay {
     // Open next tooltip faster. Disabled after 2 sec of showTooltip-silence.
     private boolean justClosed = false;
 
+    private String uniqueId = DOM.createUniqueId();
+    private Element layoutElement;
+
     /**
      * Used to show tooltips; usually used via the singleton in
      * {@link ApplicationConnection}. NOTE that #setOwner(Widget)} should be
@@ -68,8 +79,19 @@ public class VTooltip extends VOverlay {
         setWidget(layout);
         layout.add(em);
         DOM.setElementProperty(description, "className", CLASSNAME + "-text");
-        DOM.appendChild(layout.getElement(), description);
+
+        layoutElement = layout.getElement();
+        DOM.appendChild(layoutElement, description);
         setSinkShadowEvents(true);
+
+        // WAI-ARIA additions
+        layoutElement.setId(uniqueId);
+        Roles.getTooltipRole().setAriaLiveProperty(getElement(),
+                LiveValue.POLITE);
+
+        description.setId(DOM.createUniqueId());
+        Roles.getTooltipRole().set(layoutElement);
+        Roles.getTooltipRole().setAriaHiddenState(layoutElement, true);
     }
 
     /**
@@ -204,15 +226,27 @@ public class VTooltip extends VOverlay {
         closing = true;
         justClosed = true;
         justClosedTimer.schedule(QUICK_OPEN_TIMEOUT);
+    }
 
+    @Override
+    public void hide() {
+        super.hide();
+        Roles.getTooltipRole().setAriaHiddenState(layoutElement, true);
+        Roles.getTooltipRole().removeAriaDescribedbyProperty(
+                tooltipEventHandler.currentElement);
     }
 
     private int tooltipEventMouseX;
     private int tooltipEventMouseY;
 
-    public void updatePosition(Event event) {
-        tooltipEventMouseX = DOM.eventGetClientX(event);
-        tooltipEventMouseY = DOM.eventGetClientY(event);
+    public void updatePosition(Event event, boolean isFocused) {
+        if (isFocused) {
+            tooltipEventMouseX = -1000;
+            tooltipEventMouseY = -1000;
+        } else {
+            tooltipEventMouseX = DOM.eventGetClientX(event);
+            tooltipEventMouseY = DOM.eventGetClientY(event);
+        }
     }
 
     @Override
@@ -246,13 +280,18 @@ public class VTooltip extends VOverlay {
     }
 
     private class TooltipEventHandler implements MouseMoveHandler,
-            ClickHandler, KeyDownHandler {
+            ClickHandler, KeyDownHandler, FocusHandler, BlurHandler {
 
         /**
          * Current element hovered
          */
         private com.google.gwt.dom.client.Element currentElement = null;
 
+        /**
+         * Current element focused
+         */
+        private boolean currentIsFocused;
+
         /**
          * Current tooltip active
          */
@@ -319,41 +358,77 @@ public class VTooltip extends VOverlay {
 
         @Override
         public void onMouseMove(MouseMoveEvent mme) {
-            Event event = Event.as(mme.getNativeEvent());
+            handleShowHide(mme, false);
+        }
+
+        @Override
+        public void onClick(ClickEvent event) {
+            handleHideEvent();
+        }
+
+        @Override
+        public void onKeyDown(KeyDownEvent event) {
+            handleHideEvent();
+        }
+
+        /**
+         * Displays Tooltip when page is navigated with the keyboard.
+         * 
+         * Tooltip is not visible. This makes it possible for assistive devices
+         * to recognize the tooltip.
+         */
+        @Override
+        public void onFocus(FocusEvent fe) {
+            handleShowHide(fe, true);
+        }
+
+        /**
+         * Hides Tooltip when the page is navigated with the keyboard.
+         * 
+         * Removes the Tooltip from page to make sure assistive devices don't
+         * recognize it by accident.
+         */
+        @Override
+        public void onBlur(BlurEvent be) {
+            handleHideEvent();
+        }
+
+        private void handleShowHide(DomEvent domEvent, boolean isFocused) {
+            Event event = Event.as(domEvent.getNativeEvent());
             com.google.gwt.dom.client.Element element = Element.as(event
                     .getEventTarget());
 
             // We can ignore move event if it's handled by move or over already
-            if (currentElement == element) {
+            if (currentElement == element && currentIsFocused == isFocused) {
                 return;
             }
-            currentElement = element;
 
             boolean connectorAndTooltipFound = resolveConnector((com.google.gwt.user.client.Element) element);
             if (!connectorAndTooltipFound) {
                 if (isShowing()) {
                     handleHideEvent();
+                    Roles.getButtonRole()
+                            .removeAriaDescribedbyProperty(element);
                 } else {
                     currentTooltipInfo = null;
                 }
             } else {
-                updatePosition(event);
+                updatePosition(event, isFocused);
+
                 if (isShowing()) {
                     replaceCurrentTooltip();
+                    Roles.getTooltipRole().removeAriaDescribedbyProperty(
+                            currentElement);
                 } else {
                     showTooltip();
                 }
-            }
-        }
 
-        @Override
-        public void onClick(ClickEvent event) {
-            handleHideEvent();
-        }
+                Roles.getTooltipRole().setAriaDescribedbyProperty(element,
+                        Id.of(uniqueId));
+            }
 
-        @Override
-        public void onKeyDown(KeyDownEvent event) {
-            handleHideEvent();
+            currentIsFocused = isFocused;
+            currentElement = element;
         }
     }
 
@@ -370,6 +445,25 @@ public class VTooltip extends VOverlay {
         widget.addDomHandler(tooltipEventHandler, MouseMoveEvent.getType());
         widget.addDomHandler(tooltipEventHandler, ClickEvent.getType());
         widget.addDomHandler(tooltipEventHandler, KeyDownEvent.getType());
+        widget.addDomHandler(tooltipEventHandler, FocusEvent.getType());
+        widget.addDomHandler(tooltipEventHandler, BlurEvent.getType());
         Profiler.leave("VTooltip.connectHandlersToWidget");
     }
+
+    /**
+     * Returns the unique id of the tooltip element.
+     * 
+     * @return String containing the unique id of the tooltip, which always has
+     *         a value
+     */
+    public String getUniqueId() {
+        return uniqueId;
+    }
+
+    @Override
+    public void setPopupPositionAndShow(PositionCallback callback) {
+        super.setPopupPositionAndShow(callback);
+
+        Roles.getTooltipRole().setAriaHiddenState(layoutElement, false);
+    }
 }
index 189149f8b5aabb5086f5c9bd8d1d0c1a30b19c84..56f358f294dc221b1816a2f33f1f53c042841eb5 100644 (file)
@@ -27,6 +27,7 @@ import com.google.gwt.user.client.ui.Widget;
  * Helper class that helps to implement the WAI-ARIA functionality.
  */
 public class AriaHelper {
+    public static final String ASSISTIVE_DEVICE_ONLY_STYLE = "v-assistive-device-only";
 
     /**
      * Binds a caption (label in HTML speak) to the form element as required by
@@ -40,24 +41,32 @@ public class AriaHelper {
     public static void bindCaption(Widget widget, Element captionElement) {
         assert widget != null : "Valid Widget required";
 
-        if (null != captionElement) {
-            ensureUniqueId(captionElement);
-
-            if (widget instanceof HandlesAriaCaption) {
-                ((HandlesAriaCaption) widget).handleAriaCaption(captionElement);
+        if (widget instanceof HandlesAriaCaption) {
+            // Let the widget handle special cases itself
+            if (captionElement == null) {
+                ((HandlesAriaCaption) widget).clearAriaCaption();
             } else {
-                String ownerId = ensureUniqueId(widget.getElement());
-                captionElement.setAttribute("for", ownerId);
-
-                Roles.getTextboxRole().setAriaLabelledbyProperty(
-                        widget.getElement(), Id.of(captionElement));
+                ensureUniqueId(captionElement);
+                ((HandlesAriaCaption) widget).bindAriaCaption(captionElement);
             }
+        } else if (captionElement != null) {
+            // Handle the default case
+            ensureUniqueId(captionElement);
+            String ownerId = ensureUniqueId(widget.getElement());
+            captionElement.setAttribute("for", ownerId);
+
+            Roles.getTextboxRole().setAriaLabelledbyProperty(
+                    widget.getElement(), Id.of(captionElement));
         } else {
-            Roles.getTextboxRole().removeAriaLabelledbyProperty(
-                    widget.getElement());
+            clearCaption(widget);
         }
     }
 
+    public static void clearCaption(Widget widget) {
+        Roles.getTextboxRole()
+                .removeAriaLabelledbyProperty(widget.getElement());
+    }
+
     /**
      * Handles the required actions depending of the input element being
      * required or not.
@@ -102,7 +111,7 @@ public class AriaHelper {
      *            Element to check
      * @return String with the id of the element
      */
-    private static String ensureUniqueId(Element element) {
+    public static String ensureUniqueId(Element element) {
         String id = element.getId();
         if (null == id || id.isEmpty()) {
             id = DOM.createUniqueId();
@@ -110,4 +119,27 @@ public class AriaHelper {
         }
         return id;
     }
+
+    /**
+     * Moves an element out of sight. That way it is possible to have additional
+     * information for an assistive device, that is not in the way for visual
+     * users.
+     * 
+     * @param element
+     *            Element to move out of sight
+     */
+    public static void visibleForAssistiveDevicesOnly(Element element) {
+        element.addClassName(ASSISTIVE_DEVICE_ONLY_STYLE);
+    }
+
+    /**
+     * Clears the settings that moved the element out of sight, so it is visible
+     * on the page again.
+     * 
+     * @param element
+     *            Element to clear the specific styles from
+     */
+    public static void visibleForAll(Element element) {
+        element.removeClassName(ASSISTIVE_DEVICE_ONLY_STYLE);
+    }
 }
index 045bec1d4ba01620d6414d67357a0ccf1cb98477..fbbbbff4624d20e795b2aece53b78aeca5a471bf 100644 (file)
@@ -32,5 +32,11 @@ public interface HandlesAriaCaption {
      * @param captionElement
      *            Element of the caption
      */
-    void handleAriaCaption(Element captionElement);
+    void bindAriaCaption(Element captionElement);
+
+    /**
+     * Called to clear the binding to a caption from the main input element of
+     * the widget.
+     */
+    void clearAriaCaption();
 }
index e234cc911cef1e2f432a44db5ba1ca4556ad589e..3e81ec734bee50656b6ae5b6e97eacf1c7837dfc 100644 (file)
@@ -19,6 +19,8 @@ package com.vaadin.client.ui;
 import java.util.Date;
 import java.util.Iterator;
 
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.aria.client.SelectedValue;
 import com.google.gwt.dom.client.Node;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
@@ -40,6 +42,7 @@ import com.google.gwt.event.dom.client.MouseOutEvent;
 import com.google.gwt.event.dom.client.MouseOutHandler;
 import com.google.gwt.event.dom.client.MouseUpEvent;
 import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.Button;
@@ -175,9 +178,9 @@ public class VCalendarPanel extends FocusableFlexTable implements
 
     private boolean showISOWeekNumbers;
 
-    private Date displayedMonth;
+    private FocusedDate displayedMonth;
 
-    private Date focusedDate;
+    private FocusedDate focusedDate;
 
     private Day selectedDay;
 
@@ -198,8 +201,9 @@ public class VCalendarPanel extends FocusableFlexTable implements
     private boolean initialRenderDone = false;
 
     public VCalendarPanel() {
-
+        getElement().setId(DOM.createUniqueId());
         setStyleName(VDateField.CLASSNAME + "-calendarpanel");
+        Roles.getGridRole().set(getElement());
 
         /*
          * Firefox auto-repeat works correctly only if we use a key press
@@ -267,6 +271,8 @@ public class VCalendarPanel extends FocusableFlexTable implements
     private void selectDate(Date date) {
         if (selectedDay != null) {
             selectedDay.removeStyleDependentName(CN_SELECTED);
+            Roles.getGridcellRole().removeAriaSelectedState(
+                    selectedDay.getElement());
         }
 
         int rowCount = days.getRowCount();
@@ -279,6 +285,8 @@ public class VCalendarPanel extends FocusableFlexTable implements
                     if (curday.getDate().equals(date)) {
                         curday.addStyleDependentName(CN_SELECTED);
                         selectedDay = curday;
+                        Roles.getGridcellRole().setAriaSelectedState(
+                                selectedDay.getElement(), SelectedValue.TRUE);
                         return;
                     }
                 }
@@ -528,6 +536,10 @@ public class VCalendarPanel extends FocusableFlexTable implements
             } else {
                 days.setHTML(headerRow, firstWeekdayColumn + i, "");
             }
+
+            Roles.getColumnheaderRole().set(
+                    days.getCellFormatter().getElement(headerRow,
+                            firstWeekdayColumn + i));
         }
 
         // Zero out hours, minutes, seconds, and milliseconds to compare dates
@@ -557,6 +569,8 @@ public class VCalendarPanel extends FocusableFlexTable implements
 
                 if (curr.equals(selectedDate)) {
                     day.addStyleDependentName(CN_SELECTED);
+                    Roles.getGridcellRole().setAriaSelectedState(
+                            day.getElement(), SelectedValue.TRUE);
                     selectedDay = day;
                 }
                 if (curr.equals(today)) {
@@ -574,10 +588,14 @@ public class VCalendarPanel extends FocusableFlexTable implements
                 }
 
                 days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek, day);
+                Roles.getGridcellRole().set(
+                        days.getCellFormatter().getElement(weekOfMonth,
+                                firstWeekdayColumn + dayOfWeek));
 
                 // ISO week numbers if requested
                 days.getCellFormatter().setVisible(weekOfMonth, weekColumn,
                         isShowISOWeekNumbers());
+
                 if (isShowISOWeekNumbers()) {
                     final String baseCssClass = parent.getStylePrimaryName()
                             + "-calendarpanel-weeknumber";
@@ -615,8 +633,9 @@ public class VCalendarPanel extends FocusableFlexTable implements
         if (focusedDate == null) {
             Date now = new Date();
             // focusedDate must have zero hours, mins, secs, millisecs
-            focusedDate = new Date(now.getYear(), now.getMonth(), now.getDate());
-            displayedMonth = new Date(now.getYear(), now.getMonth(), 1);
+            focusedDate = new FocusedDate(now.getYear(), now.getMonth(),
+                    now.getDate());
+            displayedMonth = new FocusedDate(now.getYear(), now.getMonth(), 1);
         }
 
         if (getResolution().getCalendarField() <= Resolution.MONTH
@@ -1062,9 +1081,10 @@ public class VCalendarPanel extends FocusableFlexTable implements
              */
         } else if (keycode == getResetKey() && !shift) {
             // Restore showing value the selected value
-            focusedDate = new Date(value.getYear(), value.getMonth(),
+            focusedDate = new FocusedDate(value.getYear(), value.getMonth(),
                     value.getDate());
-            displayedMonth = new Date(value.getYear(), value.getMonth(), 1);
+            displayedMonth = new FocusedDate(value.getYear(), value.getMonth(),
+                    1);
             renderCalendar();
             return true;
         }
@@ -1264,9 +1284,10 @@ public class VCalendarPanel extends FocusableFlexTable implements
         if (value == null) {
             focusedDate = displayedMonth = null;
         } else {
-            focusedDate = new Date(value.getYear(), value.getMonth(),
+            focusedDate = new FocusedDate(value.getYear(), value.getMonth(),
                     value.getDate());
-            displayedMonth = new Date(value.getYear(), value.getMonth(), 1);
+            displayedMonth = new FocusedDate(value.getYear(), value.getMonth(),
+                    1);
         }
 
         // Re-render calendar if the displayed month is changed,
@@ -1821,4 +1842,50 @@ public class VCalendarPanel extends FocusableFlexTable implements
             mouseTimer.cancel();
         }
     }
+
+    /**
+     * Helper class to inform the screen reader that the user changed the
+     * selected date. It sets the value of a field that is outside the view, and
+     * is defined as a live area. That way the screen reader recognizes the
+     * change and reads it to the user.
+     */
+    public class FocusedDate extends Date {
+
+        public FocusedDate(int year, int month, int date) {
+            super(year, month, date);
+        }
+
+        @Override
+        public void setTime(long time) {
+            super.setTime(time);
+            setLabel();
+        }
+
+        @Override
+        @Deprecated
+        public void setDate(int date) {
+            super.setDate(date);
+            setLabel();
+        }
+
+        @Override
+        @Deprecated
+        public void setMonth(int month) {
+            super.setMonth(month);
+            setLabel();
+        }
+
+        @Override
+        @Deprecated
+        public void setYear(int year) {
+            super.setYear(year);
+            setLabel();
+        }
+
+        private void setLabel() {
+            if (parent instanceof VPopupCalendar) {
+                ((VPopupCalendar) parent).setFocusedDate(this);
+            }
+        }
+    }
 }
index ca1e3ebcdbb4747b6f2a6ee0a23ec0ece92f8e1a..a94c2fcfee185c8a3762ae4f700db0ab8e5a9596 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.vaadin.client.ui;
 
+import com.google.gwt.aria.client.CheckedValue;
+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;
@@ -46,6 +48,11 @@ public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox implements
     public VCheckBox() {
         setStyleName(CLASSNAME);
 
+        // Add a11y role "checkbox"
+        Roles.getCheckboxRole().set(getElement());
+        Roles.getCheckboxRole().setAriaCheckedState(getElement(),
+                CheckedValue.FALSE);
+
         Element el = DOM.getFirstChild(getElement());
         while (el != null) {
             DOM.sinkEvents(el,
@@ -69,4 +76,22 @@ public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox implements
         }
     }
 
+    @Override
+    public void setValue(Boolean value, boolean fireEvents) {
+        setCheckedValue(value);
+        super.setValue(value, fireEvents);
+    }
+
+    private void setCheckedValue(Boolean value) {
+        CheckedValue checkedValue = value ? CheckedValue.TRUE
+                : CheckedValue.FALSE;
+        Roles.getCheckboxRole().setAriaCheckedState(getElement(), checkedValue);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+
+        Roles.getCheckboxRole().setAriaDisabledState(getElement(), true);
+    }
 }
index cea3489b42e6ba19235991a3a8635fe8e3835655..5025f272485ac11e9f367f52aa8f1f034897a189 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.dom.client.Style;
@@ -219,6 +220,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
 
             DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
             addCloseHandler(this);
+
+            Roles.getListRole().set(getElement());
         }
 
         /**
@@ -684,6 +687,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
             while (it.hasNext()) {
                 final FilterSelectSuggestion s = it.next();
                 final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
+                Roles.getListitemRole().set(mi.getElement());
 
                 Util.sinkOnloadForImages(mi.getElement());
 
@@ -1049,9 +1053,13 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
         });
 
         popupOpener.sinkEvents(Event.ONMOUSEDOWN);
+        Roles.getButtonRole().set(popupOpener.getElement());
+
         panel.add(tb);
         panel.add(popupOpener);
         initWidget(panel);
+        Roles.getComboboxRole().set(panel.getElement());
+
         tb.addKeyDownHandler(this);
         tb.addKeyUpHandler(this);
 
@@ -1059,6 +1067,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
         tb.addBlurHandler(this);
         tb.addClickHandler(this);
 
+        Roles.getTextboxRole().set(tb.getElement());
+
         popupOpener.addClickHandler(this);
 
         setStyleName(CLASSNAME);
@@ -1163,8 +1173,11 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
         // Always update styles as they might have been overwritten
         if (textInputEnabled) {
             removeStyleDependentName(STYLE_NO_INPUT);
+            Roles.getTextboxRole().removeAriaReadonlyProperty(tb.getElement());
         } else {
             addStyleDependentName(STYLE_NO_INPUT);
+            Roles.getTextboxRole().setAriaReadonlyProperty(tb.getElement(),
+                    true);
         }
 
         if (this.textInputEnabled == textInputEnabled) {
index 495e842bfdb3eeab381e88adcdf505fdf7cea6f2..4c7190340f76e6d0215de35fcc7c2957983042db 100644 (file)
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.DOM;
@@ -276,6 +277,9 @@ public class VFormLayout extends SimplePanel {
             if (state.caption != null) {
                 if (captionText == null) {
                     captionText = DOM.createSpan();
+
+                    AriaHelper.bindCaption(owner.getWidget(), captionText);
+
                     DOM.insertChild(getElement(), captionText, icon == null ? 0
                             : 1);
                 }
@@ -305,11 +309,22 @@ public class VFormLayout extends SimplePanel {
                     DOM.setElementProperty(requiredFieldIndicator, "className",
                             "v-required-field-indicator");
                     DOM.appendChild(getElement(), requiredFieldIndicator);
+
+                    Roles.getTextboxRole().setAriaRequiredProperty(
+                            owner.getWidget().getElement(), true);
+
+                    // Hide the required indicator from screen reader, as this
+                    // information is set directly at the input field
+                    Roles.getTextboxRole().setAriaHiddenState(
+                            requiredFieldIndicator, true);
                 }
             } else {
                 if (requiredFieldIndicator != null) {
                     DOM.removeChild(getElement(), requiredFieldIndicator);
                     requiredFieldIndicator = null;
+
+                    Roles.getTextboxRole().removeAriaRequiredProperty(
+                            owner.getWidget().getElement());
                 }
             }
 
index 2ba8a9e7291060de36d54bb24536bc91a30dd21a..f0c7b7f83d44e2d41a2ba4f1300ea7196a6314a2 100644 (file)
@@ -22,6 +22,9 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
+import com.google.gwt.aria.client.CheckedValue;
+import com.google.gwt.aria.client.Id;
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
@@ -32,6 +35,7 @@ import com.google.gwt.event.dom.client.LoadEvent;
 import com.google.gwt.event.dom.client.LoadHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.Focusable;
@@ -46,7 +50,7 @@ import com.vaadin.shared.EventId;
 import com.vaadin.shared.ui.optiongroup.OptionGroupConstants;
 
 public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
-        BlurHandler {
+        BlurHandler, HandlesAriaCaption {
 
     public static final String CLASSNAME = "v-select-optiongroup";
 
@@ -85,6 +89,8 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
     /** For internal use only. May be removed or replaced in the future. */
     public boolean htmlContentAllowed = false;
 
+    private String labelId;
+
     public VOptionGroup() {
         super(CLASSNAME);
         panel = (Panel) optionsContainer;
@@ -99,6 +105,13 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
     public void buildOptions(UIDL uidl) {
         panel.clear();
         optionsEnabled.clear();
+
+        if (isMultiselect()) {
+            Roles.getGroupRole().set(getElement());
+        } else {
+            Roles.getRadiogroupRole().set(getElement());
+        }
+
         for (final Iterator<?> it = uidl.getChildIterator(); it.hasNext();) {
             final UIDL opUidl = (UIDL) it.next();
             CheckBox op;
@@ -118,9 +131,30 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
             if (isMultiselect()) {
                 op = new VCheckBox();
                 op.setHTML(itemHtml);
+
+                // Add a11y role "checkbox" - FIXME - did not find a good
+                // solution to prevent getFirstChild()
+                com.google.gwt.dom.client.Element checkBoxElement = op
+                        .getElement().getFirstChildElement();
+                Roles.getCheckboxRole().set(checkBoxElement);
+                Roles.getCheckboxRole().setAriaCheckedState(checkBoxElement,
+                        CheckedValue.FALSE);
             } else {
                 op = new RadioButton(paintableId, itemHtml, true);
                 op.setStyleName("v-radiobutton");
+
+                // Add a11y role "radio" - FIXME - did not find a good solution
+                // to prevent getFirstChild()
+                com.google.gwt.dom.client.Element radioElement = op
+                        .getElement().getFirstChildElement();
+                Roles.getRadioRole().set(radioElement);
+                Roles.getRadioRole().setAriaCheckedState(radioElement,
+                        CheckedValue.FALSE);
+            }
+
+            if (labelId != null && !labelId.isEmpty()) {
+                Roles.getFormRole().setAriaDescribedbyProperty(
+                        op.getElement().getFirstChildElement(), Id.of(labelId));
             }
 
             if (icon != null && icon.length() != 0) {
@@ -165,6 +199,13 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
             }
             client.updateVariable(paintableId, "selected", getSelectedItems(),
                     isImmediate());
+
+            for (CheckBox item : optionsToKeys.keySet()) {
+                CheckedValue value = item.getValue() ? CheckedValue.TRUE
+                        : CheckedValue.FALSE;
+                Roles.getCheckboxRole().setAriaCheckedState(item.getElement(),
+                        value);
+            }
         }
     }
 
@@ -238,4 +279,22 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
             });
         }
     }
+
+    @Override
+    public void bindAriaCaption(Element captionElement) {
+        labelId = captionElement.getId();
+        for (CheckBox item : optionsToKeys.keySet()) {
+            Roles.getCheckboxRole().setAriaLabelledbyProperty(
+                    item.getElement(), Id.of(labelId));
+        }
+    }
+
+    @Override
+    public void clearAriaCaption() {
+        labelId = null;
+        for (CheckBox item : optionsToKeys.keySet()) {
+            Roles.getCheckboxRole().removeAriaLabelledbyProperty(
+                    item.getElement());
+        }
+    }
 }
index 2a2578aa16932ab4b9cd8baa0666eba9546d26c2..dfa56253417356a5c7bad10c2d7c7302e857ab16 100644 (file)
@@ -18,6 +18,9 @@ package com.vaadin.client.ui;
 
 import java.util.Date;
 
+import com.google.gwt.aria.client.Id;
+import com.google.gwt.aria.client.LiveValue;
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -25,14 +28,18 @@ import com.google.gwt.event.dom.client.DomEvent;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwt.user.client.ui.Widget;
 import com.vaadin.client.BrowserInfo;
 import com.vaadin.client.VConsole;
 import com.vaadin.client.ui.VCalendarPanel.FocusOutListener;
@@ -68,6 +75,10 @@ public class VPopupCalendar extends VTextualDate implements Field,
 
     private boolean textFieldEnabled = true;
 
+    private String captionId;
+
+    private Label selectedDate;
+
     public VPopupCalendar() {
         super();
 
@@ -75,6 +86,11 @@ public class VPopupCalendar extends VTextualDate implements Field,
         calendarToggle.addClickHandler(this);
         // -2 instead of -1 to avoid FocusWidget.onAttach to reset it
         calendarToggle.getElement().setTabIndex(-2);
+
+        Roles.getButtonRole().set(calendarToggle.getElement());
+        Roles.getButtonRole().setAriaHiddenState(calendarToggle.getElement(),
+                true);
+
         add(calendarToggle);
 
         calendar = GWT.create(VCalendarPanel.class);
@@ -88,6 +104,9 @@ public class VPopupCalendar extends VTextualDate implements Field,
             }
         });
 
+        Roles.getButtonRole().setAriaControlsProperty(
+                calendarToggle.getElement(), Id.of(calendar.getElement()));
+
         calendar.setSubmitListener(new SubmitListener() {
             @Override
             public void onSubmit() {
@@ -109,7 +128,19 @@ public class VPopupCalendar extends VTextualDate implements Field,
         popup = new VOverlay(true, true, true);
         popup.setOwner(this);
 
-        popup.setWidget(calendar);
+        FlowPanel wrapper = new FlowPanel();
+        selectedDate = new Label();
+        selectedDate.setStyleName(getStylePrimaryName() + "-selecteddate");
+        AriaHelper.visibleForAssistiveDevicesOnly(selectedDate.getElement());
+
+        Roles.getTextboxRole().setAriaLiveProperty(selectedDate.getElement(),
+                LiveValue.ASSERTIVE);
+        Roles.getTextboxRole().setAriaAtomicProperty(selectedDate.getElement(),
+                true);
+        wrapper.add(selectedDate);
+        wrapper.add(calendar);
+
+        popup.setWidget(wrapper);
         popup.addCloseHandler(this);
 
         DOM.setElementProperty(calendar.getElement(), "id",
@@ -181,9 +212,63 @@ public class VPopupCalendar extends VTextualDate implements Field,
         text.setEnabled(textFieldEnabled);
         if (textFieldEnabled) {
             calendarToggle.setTabIndex(-1);
+            Roles.getButtonRole().setAriaHiddenState(
+                    calendarToggle.getElement(), true);
         } else {
             calendarToggle.setTabIndex(0);
+            Roles.getButtonRole().setAriaHiddenState(
+                    calendarToggle.getElement(), false);
+        }
+
+        handleAriaAttributes();
+    }
+
+    @Override
+    public void bindAriaCaption(Element captionElement) {
+        captionId = captionElement.getId();
+
+        if (isTextFieldEnabled()) {
+            super.bindAriaCaption(captionElement);
+        } else {
+            AriaHelper.bindCaption(calendarToggle, captionElement);
+        }
+
+        handleAriaAttributes();
+    }
+
+    private void handleAriaAttributes() {
+        Widget removeFromWidget;
+        Widget setForWidget;
+
+        if (isTextFieldEnabled()) {
+            setForWidget = text;
+            removeFromWidget = calendarToggle;
+        } else {
+            setForWidget = calendarToggle;
+            removeFromWidget = text;
         }
+
+        Roles.getFormRole().removeAriaLabelledbyProperty(
+                removeFromWidget.getElement());
+        if (captionId == null) {
+            Roles.getFormRole().removeAriaLabelledbyProperty(
+                    setForWidget.getElement());
+        } else {
+            Roles.getFormRole().setAriaLabelledbyProperty(
+                    setForWidget.getElement(), Id.of(captionId));
+        }
+    }
+
+    @Override
+    public void clearAriaCaption() {
+        captionId = null;
+        if (isTextFieldEnabled()) {
+            super.clearAriaCaption();
+        } else {
+            AriaHelper.clearCaption(calendarToggle);
+        }
+
+        handleAriaAttributes();
     }
 
     /*
@@ -350,6 +435,32 @@ public class VPopupCalendar extends VTextualDate implements Field,
         calendar.setFocus(focus);
     }
 
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+
+        if (enabled) {
+            Roles.getButtonRole().setAriaDisabledState(
+                    calendarToggle.getElement(), true);
+        } else {
+            Roles.getButtonRole().setAriaDisabledState(
+                    calendarToggle.getElement(), false);
+        }
+    }
+
+    /**
+     * Sets the content of a special field for assistive devices, so that they
+     * can recognize the change and inform the user (reading out in case of
+     * screen reader)
+     * 
+     * @param selectedDate
+     *            Date that is currently selected
+     */
+    public void setFocusedDate(Date selectedDate) {
+        this.selectedDate.setText(DateTimeFormat.getFormat("dd, MMMM, yyyy")
+                .format(selectedDate));
+    }
+
     /**
      * For internal use only. May be removed or replaced in the future.
      * 
index 0fbed0dd90f32a522e1552b0883b66515098f64a..9b85dd22dad0c93f42a6a6f43223aea92e07c905 100644 (file)
@@ -16,6 +16,7 @@
 
 package com.vaadin.client.ui;
 
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -95,6 +96,9 @@ public class VTextField extends TextBoxBase implements Field, ChangeHandler,
         }
         addFocusHandler(this);
         addBlurHandler(this);
+
+        // Add a11y role "textbox"
+        Roles.getTextboxRole().set(node);
     }
 
     /**
index 2f444a85875a4d32eeb76460e99104c32add9c8a..97a868b69d8e4108a57c5cbda4cd2ac75ddc58bd 100644 (file)
@@ -18,6 +18,7 @@ package com.vaadin.client.ui;
 
 import java.util.Date;
 
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -34,7 +35,7 @@ import com.vaadin.shared.EventId;
 import com.vaadin.shared.ui.datefield.Resolution;
 
 public class VTextualDate extends VDateField implements Field, ChangeHandler,
-        Focusable, SubPartAware {
+        Focusable, SubPartAware, HandlesAriaCaption {
 
     private static final String PARSE_ERROR_CLASSNAME = "-parseerror";
 
@@ -96,6 +97,9 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler,
                 }
             }
         });
+
+        Roles.getTextboxRole().set(text.getElement());
+
         add(text);
     }
 
@@ -150,6 +154,16 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler,
         return formatStr;
     }
 
+    @Override
+    public void bindAriaCaption(Element captionElement) {
+        AriaHelper.bindCaption(text, captionElement);
+    }
+
+    @Override
+    public void clearAriaCaption() {
+        AriaHelper.clearCaption(text);
+    }
+
     /**
      * Updates the text field according to the current date (provided by
      * {@link #getDate()}). Takes care of updating text, enabling and disabling
@@ -178,8 +192,12 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler,
 
         if (readonly) {
             text.addStyleName("v-readonly");
+            Roles.getTextboxRole().setAriaReadonlyProperty(text.getElement(),
+                    true);
         } else {
             text.removeStyleName("v-readonly");
+            Roles.getTextboxRole()
+                    .removeAriaReadonlyProperty(text.getElement());
         }
 
     }
@@ -348,5 +366,4 @@ public class VTextualDate extends VDateField implements Field, ChangeHandler,
 
         return null;
     }
-
 }
index d6d1cef8ccd0b801883015de41b208615f9e65ae..6e3fffb47c5c127dd5462a413ffe32a59c130294 100644 (file)
@@ -19,6 +19,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.dom.client.Element;
 import com.vaadin.client.ApplicationConnection;
 import com.vaadin.client.BrowserInfo;
@@ -26,6 +27,7 @@ import com.vaadin.client.Paintable;
 import com.vaadin.client.TooltipInfo;
 import com.vaadin.client.UIDL;
 import com.vaadin.client.Util;
+import com.vaadin.client.VConsole;
 import com.vaadin.client.communication.StateChangeEvent;
 import com.vaadin.client.ui.AbstractComponentConnector;
 import com.vaadin.client.ui.VTree;
@@ -93,7 +95,7 @@ public class TreeConnector extends AbstractComponentConnector implements
             }
             childTree = getWidget().new TreeNode();
             getConnection().getVTooltip().connectHandlersToWidget(childTree);
-            updateNodeFromUIDL(childTree, childUidl);
+            updateNodeFromUIDL(childTree, childUidl, 1);
             getWidget().body.add(childTree);
             childTree.addStyleDependentName("root");
             childTree.childNodeContainer.addStyleDependentName("root");
@@ -108,6 +110,9 @@ public class TreeConnector extends AbstractComponentConnector implements
         getWidget().isMultiselect = "multi".equals(selectMode);
 
         if (getWidget().isMultiselect) {
+            Roles.getTreeRole().setAriaMultiselectableProperty(
+                    getWidget().getElement(), true);
+
             if (BrowserInfo.get().isTouchDevice()) {
                 // Always use the simple mode for touch devices that do not have
                 // shift/ctrl keys (#8595)
@@ -116,6 +121,9 @@ public class TreeConnector extends AbstractComponentConnector implements
                 getWidget().multiSelectMode = MultiSelectMode.valueOf(uidl
                         .getStringAttribute("multiselectmode"));
             }
+        } else {
+            Roles.getTreeRole().setAriaMultiselectableProperty(
+                    getWidget().getElement(), false);
         }
 
         getWidget().selectedIds = uidl.getStringArrayVariableAsSet("selected");
@@ -169,7 +177,18 @@ public class TreeConnector extends AbstractComponentConnector implements
                 // expanding node happened server side
                 rootNode.setState(true, false);
             }
-            renderChildNodes(rootNode, (Iterator) uidl.getChildIterator());
+            String levelPropertyString = Roles.getTreeitemRole()
+                    .getAriaLevelProperty(rootNode.getElement());
+            int levelProperty;
+            try {
+                levelProperty = Integer.valueOf(levelPropertyString);
+            } catch (NumberFormatException e) {
+                levelProperty = 1;
+                VConsole.error(e);
+            }
+
+            renderChildNodes(rootNode, (Iterator) uidl.getChildIterator(),
+                    levelProperty + 1);
         }
     }
 
@@ -196,7 +215,10 @@ public class TreeConnector extends AbstractComponentConnector implements
 
     }
 
-    public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl) {
+    public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl, int level) {
+        Roles.getTreeitemRole().setAriaLevelProperty(treeNode.getElement(),
+                level);
+
         String nodeKey = uidl.getStringAttribute("key");
         treeNode.setText(uidl
                 .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION));
@@ -212,7 +234,8 @@ public class TreeConnector extends AbstractComponentConnector implements
             if (uidl.getChildCount() == 0) {
                 treeNode.childNodeContainer.setVisible(false);
             } else {
-                renderChildNodes(treeNode, (Iterator) uidl.getChildIterator());
+                renderChildNodes(treeNode, (Iterator) uidl.getChildIterator(),
+                        level + 1);
                 treeNode.childrenLoaded = true;
             }
         } else {
@@ -243,7 +266,7 @@ public class TreeConnector extends AbstractComponentConnector implements
                 .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON));
     }
 
-    void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i) {
+    void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i, int level) {
         containerNode.childNodeContainer.clear();
         containerNode.childNodeContainer.setVisible(true);
         while (i.hasNext()) {
@@ -256,7 +279,7 @@ public class TreeConnector extends AbstractComponentConnector implements
             }
             final TreeNode childTree = getWidget().new TreeNode();
             getConnection().getVTooltip().connectHandlersToWidget(childTree);
-            updateNodeFromUIDL(childTree, childUidl);
+            updateNodeFromUIDL(childTree, childUidl, level);
             containerNode.childNodeContainer.add(childTree);
             if (!i.hasNext()) {
                 childTree
index 126caa59850d5e6a23872d8f1c49e2cfdbb6e165..4ffe7f806ea78de7c50ab19c14b5316151a63c3c 100644 (file)
@@ -1,6 +1,5 @@
 package com.vaadin.tests.layouts;
 
-
 import java.util.ArrayList;
 import java.util.List;