diff options
author | michaelvogt <michael@vaadin.com> | 2013-03-20 10:37:11 +0200 |
---|---|---|
committer | michaelvogt <michael@vaadin.com> | 2013-03-20 10:37:11 +0200 |
commit | 320b4e3776976e5dad822f01542f338cf76140bb (patch) | |
tree | 436ed9d448a1e663befde8b01ed8fa08a1f14700 | |
parent | 9764317f6be8091cfda37faac9294f5e58ecfeef (diff) | |
download | vaadin-framework-320b4e3776976e5dad822f01542f338cf76140bb.tar.gz vaadin-framework-320b4e3776976e5dad822f01542f338cf76140bb.zip |
WAI-ARIA fields (#11180)
Field implementations of the WAI-ARIA changes in the base classes
Change-Id: Ie51e76130f3f9976a32c373334b709f0f5b68f1a
-rw-r--r-- | WebContent/VAADIN/themes/base/common/common.scss | 6 | ||||
-rw-r--r-- | client/src/com/vaadin/client/VCaption.java | 15 | ||||
-rw-r--r-- | client/src/com/vaadin/client/VErrorMessage.java | 3 | ||||
-rw-r--r-- | client/src/com/vaadin/client/VTooltip.java | 130 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/AriaHelper.java | 58 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/HandlesAriaCaption.java | 8 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VCalendarPanel.java | 85 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VCheckBox.java | 25 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VFilterSelect.java | 13 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VFormLayout.java | 15 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VOptionGroup.java | 61 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VPopupCalendar.java | 113 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VTextField.java | 4 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VTextualDate.java | 21 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/tree/TreeConnector.java | 35 | ||||
-rw-r--r-- | uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java | 1 |
16 files changed, 541 insertions, 52 deletions
diff --git a/WebContent/VAADIN/themes/base/common/common.scss b/WebContent/VAADIN/themes/base/common/common.scss index e801ec2821..27c6dc949c 100644 --- a/WebContent/VAADIN/themes/base/common/common.scss +++ b/WebContent/VAADIN/themes/base/common/common.scss @@ -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 diff --git a/client/src/com/vaadin/client/VCaption.java b/client/src/com/vaadin/client/VCaption.java index 47287636c4..607a0f0b0a 100644 --- a/client/src/com/vaadin/client/VCaption.java +++ b/client/src/com/vaadin/client/VCaption.java @@ -16,12 +16,15 @@ 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) { diff --git a/client/src/com/vaadin/client/VErrorMessage.java b/client/src/com/vaadin/client/VErrorMessage.java index a384b451dd..2e42b98a05 100644 --- a/client/src/com/vaadin/client/VErrorMessage.java +++ b/client/src/com/vaadin/client/VErrorMessage.java @@ -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()); } /** diff --git a/client/src/com/vaadin/client/VTooltip.java b/client/src/com/vaadin/client/VTooltip.java index 759b90a8cd..e6d9a79a5b 100644 --- a/client/src/com/vaadin/client/VTooltip.java +++ b/client/src/com/vaadin/client/VTooltip.java @@ -15,8 +15,16 @@ */ 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,7 +280,7 @@ public class VTooltip extends VOverlay { } private class TooltipEventHandler implements MouseMoveHandler, - ClickHandler, KeyDownHandler { + ClickHandler, KeyDownHandler, FocusHandler, BlurHandler { /** * Current element hovered @@ -254,6 +288,11 @@ public class VTooltip extends VOverlay { private com.google.gwt.dom.client.Element currentElement = null; /** + * Current element focused + */ + private boolean currentIsFocused; + + /** * Current tooltip active */ private TooltipInfo currentTooltipInfo = null; @@ -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); + } } diff --git a/client/src/com/vaadin/client/ui/AriaHelper.java b/client/src/com/vaadin/client/ui/AriaHelper.java index 189149f8b5..56f358f294 100644 --- a/client/src/com/vaadin/client/ui/AriaHelper.java +++ b/client/src/com/vaadin/client/ui/AriaHelper.java @@ -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); + } } diff --git a/client/src/com/vaadin/client/ui/HandlesAriaCaption.java b/client/src/com/vaadin/client/ui/HandlesAriaCaption.java index 045bec1d4b..fbbbbff462 100644 --- a/client/src/com/vaadin/client/ui/HandlesAriaCaption.java +++ b/client/src/com/vaadin/client/ui/HandlesAriaCaption.java @@ -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(); } diff --git a/client/src/com/vaadin/client/ui/VCalendarPanel.java b/client/src/com/vaadin/client/ui/VCalendarPanel.java index e234cc911c..3e81ec734b 100644 --- a/client/src/com/vaadin/client/ui/VCalendarPanel.java +++ b/client/src/com/vaadin/client/ui/VCalendarPanel.java @@ -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); + } + } + } } diff --git a/client/src/com/vaadin/client/ui/VCheckBox.java b/client/src/com/vaadin/client/ui/VCheckBox.java index ca1e3ebcdb..a94c2fcfee 100644 --- a/client/src/com/vaadin/client/ui/VCheckBox.java +++ b/client/src/com/vaadin/client/ui/VCheckBox.java @@ -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); + } } diff --git a/client/src/com/vaadin/client/ui/VFilterSelect.java b/client/src/com/vaadin/client/ui/VFilterSelect.java index cea3489b42..5025f27248 100644 --- a/client/src/com/vaadin/client/ui/VFilterSelect.java +++ b/client/src/com/vaadin/client/ui/VFilterSelect.java @@ -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) { diff --git a/client/src/com/vaadin/client/ui/VFormLayout.java b/client/src/com/vaadin/client/ui/VFormLayout.java index 495e842bfd..4c7190340f 100644 --- a/client/src/com/vaadin/client/ui/VFormLayout.java +++ b/client/src/com/vaadin/client/ui/VFormLayout.java @@ -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()); } } diff --git a/client/src/com/vaadin/client/ui/VOptionGroup.java b/client/src/com/vaadin/client/ui/VOptionGroup.java index 2ba8a9e729..f0c7b7f83d 100644 --- a/client/src/com/vaadin/client/ui/VOptionGroup.java +++ b/client/src/com/vaadin/client/ui/VOptionGroup.java @@ -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()); + } + } } diff --git a/client/src/com/vaadin/client/ui/VPopupCalendar.java b/client/src/com/vaadin/client/ui/VPopupCalendar.java index 2a2578aa16..dfa5625341 100644 --- a/client/src/com/vaadin/client/ui/VPopupCalendar.java +++ b/client/src/com/vaadin/client/ui/VPopupCalendar.java @@ -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. * diff --git a/client/src/com/vaadin/client/ui/VTextField.java b/client/src/com/vaadin/client/ui/VTextField.java index 0fbed0dd90..9b85dd22da 100644 --- a/client/src/com/vaadin/client/ui/VTextField.java +++ b/client/src/com/vaadin/client/ui/VTextField.java @@ -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); } /** diff --git a/client/src/com/vaadin/client/ui/VTextualDate.java b/client/src/com/vaadin/client/ui/VTextualDate.java index 2f444a8587..97a868b69d 100644 --- a/client/src/com/vaadin/client/ui/VTextualDate.java +++ b/client/src/com/vaadin/client/ui/VTextualDate.java @@ -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; } - } diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java index d6d1cef8cc..6e3fffb47c 100644 --- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java +++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java @@ -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 diff --git a/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java b/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java index 126caa5985..4ffe7f806e 100644 --- a/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java +++ b/uitest/src/com/vaadin/tests/layouts/CaptionsInLayoutsWaiAria.java @@ -1,6 +1,5 @@ package com.vaadin.tests.layouts; - import java.util.ArrayList; import java.util.List; |