diff options
author | michaelvogt <michael@vaadin.com> | 2013-03-21 10:40:45 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2013-04-04 16:15:00 +0000 |
commit | f980667fdfef13bcb3bfcd7e86910bed39f39bb2 (patch) | |
tree | bddca5db6884e3a01c6d4455d9aff481d1b18e5c | |
parent | 3ee3b4926b1af4409b32196ef290baf017b63379 (diff) | |
download | vaadin-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.java | 2 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/VTree.java | 63 | ||||
-rw-r--r-- | client/src/com/vaadin/client/ui/tree/TreeConnector.java | 7 | ||||
-rw-r--r-- | server/src/com/vaadin/ui/Tree.java | 51 | ||||
-rw-r--r-- | shared/src/com/vaadin/shared/ui/tree/TreeConstants.java | 2 | ||||
-rw-r--r-- | uitest/src/com/vaadin/tests/components/tree/SimpleTree.java | 122 |
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 |