aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormichaelvogt <michael@vaadin.com>2013-03-21 10:40:45 +0200
committerVaadin Code Review <review@vaadin.com>2013-04-04 16:15:00 +0000
commitf980667fdfef13bcb3bfcd7e86910bed39f39bb2 (patch)
treebddca5db6884e3a01c6d4455d9aff481d1b18e5c
parent3ee3b4926b1af4409b32196ef290baf017b63379 (diff)
downloadvaadin-framework-f980667fdfef13bcb3bfcd7e86910bed39f39bb2.tar.gz
vaadin-framework-f980667fdfef13bcb3bfcd7e86910bed39f39bb2.zip
WAI-ARIA functions for Tree (#11389)
All to navigate the tree with an assisitve device Change-Id: I531cefc95d7a720caf69aca579549e5a497ad586
-rw-r--r--client/src/com/vaadin/client/ui/VContextMenu.java2
-rw-r--r--client/src/com/vaadin/client/ui/VTree.java63
-rw-r--r--client/src/com/vaadin/client/ui/tree/TreeConnector.java7
-rw-r--r--server/src/com/vaadin/ui/Tree.java51
-rw-r--r--shared/src/com/vaadin/shared/ui/tree/TreeConstants.java2
-rw-r--r--uitest/src/com/vaadin/tests/components/tree/SimpleTree.java122
6 files changed, 242 insertions, 5 deletions
diff --git a/client/src/com/vaadin/client/ui/VContextMenu.java b/client/src/com/vaadin/client/ui/VContextMenu.java
index 80751652df..e601c8027a 100644
--- a/client/src/com/vaadin/client/ui/VContextMenu.java
+++ b/client/src/com/vaadin/client/ui/VContextMenu.java
@@ -37,6 +37,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.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.MenuBar;
@@ -75,6 +76,7 @@ public class VContextMenu extends VOverlay implements SubPartAware {
super(true, false, true);
setWidget(menu);
setStyleName("v-contextmenu");
+ getElement().setId(DOM.createUniqueId());
}
protected void imagesLoaded() {
diff --git a/client/src/com/vaadin/client/ui/VTree.java b/client/src/com/vaadin/client/ui/VTree.java
index 624dce4f13..20b3050a5d 100644
--- a/client/src/com/vaadin/client/ui/VTree.java
+++ b/client/src/com/vaadin/client/ui/VTree.java
@@ -24,6 +24,10 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
+import com.google.gwt.aria.client.ExpandedValue;
+import com.google.gwt.aria.client.Id;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.aria.client.SelectedValue;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -75,7 +79,8 @@ import com.vaadin.shared.ui.tree.TreeConstants;
*/
public class VTree extends FocusElementPanel implements VHasDropHandler,
FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler,
- SubPartAware, ActionOwner {
+ SubPartAware, ActionOwner, HandlesAriaCaption {
+ private String lastNodeKey = "";
public static final String CLASSNAME = "v-tree";
@@ -168,6 +173,8 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
public VTree() {
super();
setStyleName(CLASSNAME);
+
+ Roles.getTreeRole().set(body.getElement());
add(body);
addFocusHandler(this);
@@ -865,12 +872,22 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
}
protected void constructDom() {
+ String labelId = DOM.createUniqueId();
+
addStyleName(CLASSNAME);
+ getElement().setId(DOM.createUniqueId());
+ Roles.getTreeitemRole().set(getElement());
+ Roles.getTreeitemRole().setAriaSelectedState(getElement(),
+ SelectedValue.FALSE);
+ Roles.getTreeitemRole().setAriaLabelledbyProperty(getElement(),
+ Id.of(labelId));
nodeCaptionDiv = DOM.createDiv();
DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME
+ "-caption");
Element wrapper = DOM.createDiv();
+ wrapper.setId(labelId);
+
nodeCaptionSpan = DOM.createSpan();
DOM.appendChild(getElement(), nodeCaptionDiv);
DOM.appendChild(nodeCaptionDiv, wrapper);
@@ -886,6 +903,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
childNodeContainer = new FlowPanel();
childNodeContainer.setStyleName(CLASSNAME + "-children");
+ Roles.getGroupRole().set(childNodeContainer.getElement());
setWidget(childNodeContainer);
}
@@ -914,10 +932,13 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
new String[] { key }, true);
}
addStyleName(CLASSNAME + "-expanded");
+ Roles.getTreeitemRole().setAriaExpandedState(getElement(),
+ ExpandedValue.TRUE);
childNodeContainer.setVisible(true);
-
} else {
removeStyleName(CLASSNAME + "-expanded");
+ Roles.getTreeitemRole().setAriaExpandedState(getElement(),
+ ExpandedValue.FALSE);
childNodeContainer.setVisible(false);
if (notifyServer) {
client.updateVariable(paintableId, "collapse",
@@ -1094,15 +1115,17 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
Util.scrollIntoViewVertically(nodeCaptionDiv);
}
- public void setIcon(String iconUrl) {
+ public void setIcon(String iconUrl, String altText) {
if (iconUrl != null) {
// Add icon if not present
if (icon == null) {
icon = new Icon(client);
+ Roles.getImgRole().set(icon.getElement());
DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv),
icon.getElement(), nodeCaptionSpan);
}
icon.setUri(iconUrl);
+ icon.getElement().setAttribute("alt", altText);
} else {
// Remove icon if present
if (icon != null) {
@@ -1517,10 +1540,34 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
// Unfocus previously focused node
if (focusedNode != null) {
focusedNode.setFocused(false);
+
+ Roles.getTreeRole().removeAriaActivedescendantProperty(
+ focusedNode.getElement());
}
if (node != null) {
node.setFocused(true);
+ Roles.getTreeitemRole().setAriaSelectedState(node.getElement(),
+ SelectedValue.TRUE);
+
+ /*
+ * FIXME: This code needs to be changed when the keyboard navigation
+ * doesn't immediately trigger a selection change anymore.
+ *
+ * Right now this function is called before and after the Tree is
+ * rebuilt when up/down arrow keys are pressed. This leads to the
+ * problem, that the newly selected item is announced too often with
+ * a screen reader.
+ *
+ * Behaviour is different when using the Tree with and without
+ * screen reader.
+ */
+ if (node.key.equals(lastNodeKey)) {
+ Roles.getTreeRole().setAriaActivedescendantProperty(
+ getFocusElement(), Id.of(node.getElement()));
+ } else {
+ lastNodeKey = node.key;
+ }
}
focusedNode = node;
@@ -2161,4 +2208,14 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
keyToNode.clear();
}
+ @Override
+ public void bindAriaCaption(Element captionElement) {
+ AriaHelper.bindCaption(body, captionElement);
+ }
+
+ @Override
+ public void clearAriaCaption() {
+ AriaHelper.clearCaption(body);
+ }
+
}
diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java
index 6e3fffb47c..d8ad7d6634 100644
--- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java
+++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java
@@ -262,8 +262,11 @@ public class TreeConnector extends AbstractComponentConnector implements
getWidget().selectedIds.add(nodeKey);
}
- treeNode.setIcon(uidl
- .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON));
+ String iconUrl = uidl
+ .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON);
+ String iconAltText = uidl
+ .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT);
+ treeNode.setIcon(iconUrl, iconAltText);
}
void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i, int level) {
diff --git a/server/src/com/vaadin/ui/Tree.java b/server/src/com/vaadin/ui/Tree.java
index 34cfbaf61b..700195cd4b 100644
--- a/server/src/com/vaadin/ui/Tree.java
+++ b/server/src/com/vaadin/ui/Tree.java
@@ -73,6 +73,11 @@ public class Tree extends AbstractSelect implements Container.Hierarchical,
/* Private members */
/**
+ * Item icons alt texts.
+ */
+ private final HashMap<Object, String> itemIconAlts = new HashMap<Object, String>();
+
+ /**
* Set of expanded nodes.
*/
private HashSet<Object> expanded = new HashSet<Object>();
@@ -163,6 +168,50 @@ public class Tree extends AbstractSelect implements Container.Hierarchical,
super(caption, dataSource);
}
+ @Override
+ public void setItemIcon(Object itemId, Resource icon) {
+ setItemIcon(itemId, icon, "");
+ }
+
+ /**
+ * Sets the icon for an item.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @param icon
+ * the icon to use or null.
+ *
+ * @param altText
+ * String with the alternative text for the icon
+ */
+ public void setItemIcon(Object itemId, Resource icon, String altText) {
+ if (itemId != null) {
+ super.setItemIcon(itemId, icon);
+
+ if (icon == null) {
+ itemIconAlts.remove(itemId);
+ } else if (altText == null) {
+ throw new IllegalArgumentException(
+ "Parameter 'altText' needs to be non null");
+ } else {
+ itemIconAlts.put(itemId, altText);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Return the alternate text of an icon in a tree item.
+ *
+ * @param itemId
+ * Object with the ID of the item
+ * @return String with the alternate text of the icon, or null when no icon
+ * was set
+ */
+ public String getItemIconAlternateText(Object itemId) {
+ return itemIconAlts.get(itemId);
+ }
+
/* Expanding and collapsing */
/**
@@ -638,6 +687,8 @@ public class Tree extends AbstractSelect implements Container.Hierarchical,
if (icon != null) {
target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON,
getItemIcon(itemId));
+ target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT,
+ getItemIconAlternateText(itemId));
}
final String key = itemIdMapper.key(itemId);
target.addAttribute("key", key);
diff --git a/shared/src/com/vaadin/shared/ui/tree/TreeConstants.java b/shared/src/com/vaadin/shared/ui/tree/TreeConstants.java
index 7adc69511d..a57ca31246 100644
--- a/shared/src/com/vaadin/shared/ui/tree/TreeConstants.java
+++ b/shared/src/com/vaadin/shared/ui/tree/TreeConstants.java
@@ -26,6 +26,8 @@ public class TreeConstants implements Serializable {
public static final String ATTRIBUTE_NODE_CAPTION = "caption";
@Deprecated
public static final String ATTRIBUTE_NODE_ICON = "icon";
+ @Deprecated
+ public static final String ATTRIBUTE_NODE_ICON_ALT = "iconalt";
@Deprecated
public static final String ATTRIBUTE_ACTION_CAPTION = "caption";
diff --git a/uitest/src/com/vaadin/tests/components/tree/SimpleTree.java b/uitest/src/com/vaadin/tests/components/tree/SimpleTree.java
new file mode 100644
index 0000000000..2fd3f05dbb
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/tree/SimpleTree.java
@@ -0,0 +1,122 @@
+package com.vaadin.tests.components.tree;
+
+import java.util.Date;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.HierarchicalContainer;
+import com.vaadin.event.Action;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.tests.components.TestBase;
+import com.vaadin.ui.AbstractSelect;
+import com.vaadin.ui.Tree;
+
+public class SimpleTree extends TestBase implements Action.Handler {
+ private static final String[][] hardware = { //
+ { "Desktops", "Dell OptiPlex GX240", "Dell OptiPlex GX260",
+ "Dell OptiPlex GX280" },
+ { "Monitors", "Benq T190HD", "Benq T220HD", "Benq T240HD" },
+ { "Laptops", "IBM ThinkPad T40", "IBM ThinkPad T43",
+ "IBM ThinkPad T60" } };
+
+ ThemeResource notCachedFolderIconLargeOther = new ThemeResource(
+ "../runo/icons/16/ok.png?" + new Date().getTime());
+ ThemeResource notCachedFolderIconLarge = new ThemeResource(
+ "../runo/icons/16/folder.png?" + new Date().getTime());
+
+ // Actions for the context menu
+ private static final Action ACTION_ADD = new Action("Add child item");
+ private static final Action ACTION_DELETE = new Action("Delete");
+ private static final Action[] ACTIONS = new Action[] { ACTION_ADD,
+ ACTION_DELETE };
+
+ private Tree tree;
+
+ @Override
+ public void setup() {
+ // Create the Tree,a dd to layout
+ tree = new Tree("Hardware Inventory");
+ addComponent(tree);
+
+ // Contents from a (prefilled example) hierarchical container:
+ tree.setContainerDataSource(getHardwareContainer());
+
+ // Add actions (context menu)
+ tree.addActionHandler(this);
+
+ // Cause valueChange immediately when the user selects
+ tree.setImmediate(true);
+
+ // Set tree to show the 'name' property as caption for items
+ tree.setItemCaptionPropertyId("name");
+ tree.setItemCaptionMode(AbstractSelect.ITEM_CAPTION_MODE_PROPERTY);
+
+ tree.setItemIcon(9, notCachedFolderIconLargeOther, "First Choice");
+ tree.setItemIcon(11, notCachedFolderIconLarge);
+
+ // Expand whole tree
+ for (Object id : tree.rootItemIds()) {
+ tree.expandItemsRecursively(id);
+ }
+ }
+
+ public static HierarchicalContainer getHardwareContainer() {
+ Item item = null;
+ int itemId = 0; // Increasing numbering for itemId:s
+
+ // Create new container
+ HierarchicalContainer hwContainer = new HierarchicalContainer();
+ // Create containerproperty for name
+ hwContainer.addContainerProperty("name", String.class, null);
+ // Create containerproperty for icon
+ hwContainer.addContainerProperty("icon", ThemeResource.class,
+ new ThemeResource("../runo/icons/16/document.png"));
+ for (int i = 0; i < hardware.length; i++) {
+ // Add new item
+ item = hwContainer.addItem(itemId);
+ // Add name property for item
+ item.getItemProperty("name").setValue(hardware[i][0]);
+ // Allow children
+ hwContainer.setChildrenAllowed(itemId, true);
+ itemId++;
+ for (int j = 1; j < hardware[i].length; j++) {
+ if (j == 1) {
+ item.getItemProperty("icon").setValue(
+ new ThemeResource("../runo/icons/16/folder.png"));
+ }
+
+ // Add child items
+ item = hwContainer.addItem(itemId);
+ item.getItemProperty("name").setValue(hardware[i][j]);
+ hwContainer.setParent(itemId, itemId - j);
+
+ hwContainer.setChildrenAllowed(itemId, false);
+ if (j == 2) {
+ hwContainer.setChildrenAllowed(itemId, true);
+ }
+
+ itemId++;
+ }
+ }
+ return hwContainer;
+ }
+
+ @Override
+ protected String getDescription() {
+ return "Sample Tree for testing WAI-ARIA functionality";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 0;
+ }
+
+ @Override
+ public Action[] getActions(Object target, Object sender) {
+ return ACTIONS;
+ }
+
+ @Override
+ public void handleAction(Action action, Object sender, Object target) {
+ System.out.println("Action: " + action.getCaption());
+ }
+} \ No newline at end of file