-Added supporting library for setting of ARIA roles/states (it is currently just a wrapper around DOM.set/get/removeElementAttribute calls)
-Added ARIA roles/states for CustomButton, Tree, TreeItem, MenuBar, MenuItem, TabBar, TabPanel
-Added keyboard navigation support to TabBar and MenuBar
-Enured that tabindexes are properly set for all widgets that derive from FocusWidget
-Set a tabindex of -1 on GWT History and Script IFRAMES so that they are not part of the tab cycle
Patch by: rshearer, rdayal, clchen, jgw, raman
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1977 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/linker/HostedModeTemplate.js b/dev/core/src/com/google/gwt/dev/linker/HostedModeTemplate.js
index 058fe4c..d4d74ad 100644
--- a/dev/core/src/com/google/gwt/dev/linker/HostedModeTemplate.js
+++ b/dev/core/src/com/google/gwt/dev/linker/HostedModeTemplate.js
@@ -1,5 +1,5 @@
/*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
@@ -348,6 +348,7 @@
iframe.src = "javascript:''";
iframe.id = "__MODULE_NAME__";
iframe.style.cssText = "position:absolute;width:0;height:0;border:none";
+ iframe.tabIndex = -1;
// Due to an IE6/7 refresh quirk, this must be an appendChild.
$doc.body.appendChild(iframe);
diff --git a/dev/core/src/com/google/gwt/dev/linker/IFrameTemplate.js b/dev/core/src/com/google/gwt/dev/linker/IFrameTemplate.js
index 39a35e0..0865515 100644
--- a/dev/core/src/com/google/gwt/dev/linker/IFrameTemplate.js
+++ b/dev/core/src/com/google/gwt/dev/linker/IFrameTemplate.js
@@ -1,5 +1,5 @@
/*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
@@ -317,6 +317,7 @@
iframe.src = "javascript:''";
iframe.id = "__MODULE_NAME__";
iframe.style.cssText = "position:absolute;width:0;height:0;border:none";
+ iframe.tabIndex = -1;
// Due to an IE6/7 refresh quirk, this must be an appendChild.
$doc.body.appendChild(iframe);
diff --git a/samples/dynatable/src/com/google/gwt/sample/dynatable/public/DynaTable.html b/samples/dynatable/src/com/google/gwt/sample/dynatable/public/DynaTable.html
index e3cc2c5..cb6e471 100644
--- a/samples/dynatable/src/com/google/gwt/sample/dynatable/public/DynaTable.html
+++ b/samples/dynatable/src/com/google/gwt/sample/dynatable/public/DynaTable.html
@@ -22,7 +22,7 @@
<title></title>
</head>
<body>
- <iframe src="javascript:''" id='__gwt_historyFrame' style='width:0;height:0;border:0'></iframe>
+ <iframe src="javascript:''" id='__gwt_historyFrame' tabIndex='-1' style='width:0;height:0;border:0'></iframe>
<script type="text/javascript" language='javascript' src='com.google.gwt.sample.dynatable.DynaTable.nocache.js'></script>
<h1>School Schedule for Professors and Students</h1>
<table width="100%" border="0" summary="School Schedule for Professors and Students">
diff --git a/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.html b/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.html
index d5d3ef5..6844b7a 100644
--- a/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.html
+++ b/samples/kitchensink/src/com/google/gwt/sample/kitchensink/public/KitchenSink.html
@@ -22,6 +22,6 @@
</head>
<body>
<script type="text/javascript" language='javascript' src='com.google.gwt.sample.kitchensink.KitchenSink.nocache.js'></script>
- <iframe src="javascript:''" id='__gwt_historyFrame' style='width:0;height:0;border:0'></iframe>
+ <iframe src="javascript:''" id='__gwt_historyFrame' tabIndex='-1' style='width:0;height:0;border:0'></iframe>
</body>
</html>
diff --git a/samples/simplerpc/src/com/google/gwt/sample/simplerpc/public/SimpleRPC.html b/samples/simplerpc/src/com/google/gwt/sample/simplerpc/public/SimpleRPC.html
index 406f66b..e7f5be8 100644
--- a/samples/simplerpc/src/com/google/gwt/sample/simplerpc/public/SimpleRPC.html
+++ b/samples/simplerpc/src/com/google/gwt/sample/simplerpc/public/SimpleRPC.html
@@ -22,7 +22,7 @@
<title>SimpleRPC</title>
</head>
<body>
- <iframe src="javascript:''" id='__gwt_historyFrame' style='width:0;height:0;border:0'></iframe>
+ <iframe src="javascript:''" id='__gwt_historyFrame' tabIndex='-1' style='width:0;height:0;border:0'></iframe>
<script type="text/javascript" language='javascript' src='com.google.gwt.sample.simplerpc.SimpleRPC.nocache.js'>
</script>
<h1> Simple RPC</h1>
diff --git a/user/src/com/google/gwt/core/public/history.html b/user/src/com/google/gwt/core/public/history.html
index 4ba2dce..b06f46a 100644
--- a/user/src/com/google/gwt/core/public/history.html
+++ b/user/src/com/google/gwt/core/public/history.html
@@ -15,7 +15,7 @@
</script></head>
<body onload='hst()'>
-<input type='text' id='__gwt_historyToken'>
+<input type='text' id='__gwt_historyToken' tabIndex='-1'>
</body>
</html>
diff --git a/user/src/com/google/gwt/user/Accessibility.gwt.xml b/user/src/com/google/gwt/user/Accessibility.gwt.xml
new file mode 100644
index 0000000..92f153f
--- /dev/null
+++ b/user/src/com/google/gwt/user/Accessibility.gwt.xml
@@ -0,0 +1,32 @@
+<!-- -->
+<!-- Copyright 2008 Google Inc. -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you -->
+<!-- may not use this file except in compliance with the License. You may -->
+<!-- may obtain a copy of the License at -->
+<!-- -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0 -->
+<!-- -->
+<!-- Unless required by applicable law or agreed to in writing, software -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS, -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -->
+<!-- implied. License for the specific language governing permissions and -->
+<!-- limitations under the License. -->
+
+<!-- Deferred binding rules for browser selection. -->
+<!-- -->
+<!-- This module is typically inherited via com.google.gwt.user.User -->
+<!-- -->
+<module>
+ <inherits name="com.google.gwt.core.Core" />
+ <inherits name="com.google.gwt.user.UserAgent" />
+
+ <!-- Mozilla has a different implementation because of support for ARIA -->
+ <replace-with
+ class="com.google.gwt.user.client.ui.impl.AccessibilityImplMozilla">
+ <when-type-is
+ class="com.google.gwt.user.client.ui.impl.AccessibilityImpl" />
+ <any>
+ <when-property-is name="user.agent" value="gecko1_8" />
+ </any>
+ </replace-with>
+</module>
diff --git a/user/src/com/google/gwt/user/User.gwt.xml b/user/src/com/google/gwt/user/User.gwt.xml
index 4110e74..d5559fe 100644
--- a/user/src/com/google/gwt/user/User.gwt.xml
+++ b/user/src/com/google/gwt/user/User.gwt.xml
@@ -35,4 +35,5 @@
<inherits name="com.google.gwt.user.SplitPanel"/>
<inherits name="com.google.gwt.user.ListBox" />
<inherits name="com.google.gwt.user.TitledPanel" />
+ <inherits name="com.google.gwt.user.Accessibility"/>
</module>
diff --git a/user/src/com/google/gwt/user/client/DOM.java b/user/src/com/google/gwt/user/client/DOM.java
index 28e724d..830f8b8 100644
--- a/user/src/com/google/gwt/user/client/DOM.java
+++ b/user/src/com/google/gwt/user/client/DOM.java
@@ -33,6 +33,8 @@
private static Event currentEvent = null;
private static final DOMImpl impl = GWT.create(DOMImpl.class);
private static Element sCaptureElem;
+ // Used to generate unique DOM ids.
+ private static int nextDOMId = 0;
// <BrowserEventPreview>
private static ArrayList<EventPreview> sEventPreviewStack;
@@ -358,6 +360,15 @@
}
/**
+ * Generates a unique DOM id. The id is of the form "gwt-id-<unique integer>".
+ *
+ * @return a unique DOM id
+ */
+ public static String createUniqueId() {
+ return "gwt-id-" + nextDOMId++;
+ }
+
+ /**
* Cancels bubbling for the given event. This will stop the event from being
* propagated to parent elements.
*
diff --git a/user/src/com/google/gwt/user/client/ui/Accessibility.java b/user/src/com/google/gwt/user/client/ui/Accessibility.java
new file mode 100644
index 0000000..66bb5ee
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/Accessibility.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.impl.AccessibilityImpl;
+import com.google.gwt.user.client.Element;
+
+/**
+ * Allows ARIA attributes to be added to widgets so that they can be
+ * identified by assistive technologies. FireFox 3 is the only browser that
+ * currently supports this feature. However, in the future, a new version of
+ * FireVox will be created that will support this implementation and work with
+ * FireFox 2.
+ *
+ * A 'role' describes the role a widget plays in a page: i.e. a checkbox widget
+ * is assigned a "checkbox" role.
+ *
+ * A 'state' describes the current state of the widget. For example, a checkbox
+ * widget has the state "checked", which is given a value of "true" or "false"
+ * depending on whether it is currently checked or unchecked.
+ *
+ * See {@see <a href="http://developer.mozilla.org/en/docs/Accessible_DHTML">http://developer.mozilla.org/en/docs/Accessible_DHTML</a>}
+ * for more information.
+ *
+ * Note that this API is package protected. At this time, the ARIA specification is still
+ * in flux, which means that this API is subject to change. Once we are fairly confident
+ * that this API will remain stable, we will make it public.
+ */
+
+final class Accessibility {
+
+ public static final String ROLE_TREE = "tree";
+ public static final String ROLE_TREEITEM = "treeitem";
+ public static final String ROLE_BUTTON = "button";
+ public static final String ROLE_TABLIST = "tablist";
+ public static final String ROLE_TAB = "tab";
+ public static final String ROLE_TABPANEL = "tabpanel";
+ public static final String ROLE_MENUBAR = "menubar";
+ public static final String ROLE_MENUITEM = "menuitem";
+
+ public static final String STATE_ACTIVEDESCENDANT = "aria-activedescendant";
+ public static final String STATE_POSINSET = "aria-posinset";
+ public static final String STATE_SETSIZE = "aria-setsize";
+ public static final String STATE_SELECTED = "aria-selected";
+ public static final String STATE_EXPANDED = "aria-expanded";
+ public static final String STATE_LEVEL = "aria-level";
+ public static final String STATE_HASPOPUP = "aria-haspopup";
+
+ private static AccessibilityImpl impl = (AccessibilityImpl) GWT
+ .create(AccessibilityImpl.class);
+
+ /**
+ * Requests the string value of the role with the specified namespace.
+ *
+ * @param elem the element which has the specified role
+ * @return the value of the role, or an empty string if none exists
+ */
+ public static String getRole(Element elem) {
+ return impl.getRole(elem);
+ }
+
+ /**
+ * Requests the string value of the state with the specified namespace.
+ *
+ * @param elem the element which has the specified state
+ * @param stateName the name of the state
+ * @return the value of the state, or an empty string if none exists
+ */
+ public static String getState(Element elem, String stateName) {
+ return impl.getState(elem, stateName);
+ }
+
+ /**
+ * Removes the state from the given element.
+ *
+ * @param elem the element which has the specified state
+ * @param stateName the name of the state to remove
+ */
+ public static void removeState(Element elem, String stateName) {
+ impl.removeState(elem, stateName);
+ }
+ /**
+ * Assigns the specified element the specified role and value for that role.
+ *
+ * @param elem the element to be given the specified role
+ * @param roleName the name of the role
+ */
+ public static void setRole(Element elem, String roleName) {
+ impl.setRole(elem, roleName);
+ }
+
+ /**
+ * Assigns the specified element the specified state and value for that state.
+ *
+ * @param elem the element to be given the specified state
+ * @param stateName the name of the state
+ * @param stateValue the value of the state
+ */
+ public static void setState(Element elem, String stateName, String stateValue) {
+ impl.setState(elem, stateName, stateValue);
+ }
+
+ private Accessibility() {
+ }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/user/client/ui/CheckBox.java b/user/src/com/google/gwt/user/client/ui/CheckBox.java
index 82a603c..6ef4f64 100644
--- a/user/src/com/google/gwt/user/client/ui/CheckBox.java
+++ b/user/src/com/google/gwt/user/client/ui/CheckBox.java
@@ -35,7 +35,6 @@
* </p>
*/
public class CheckBox extends ButtonBase implements HasName {
- private static int uniqueId;
private Element inputElem, labelElem;
/**
@@ -79,9 +78,16 @@
DOM.appendChild(getElement(), inputElem);
DOM.appendChild(getElement(), labelElem);
- String uid = "check" + (++uniqueId);
+ String uid = DOM.createUniqueId();
DOM.setElementProperty(inputElem, "id", uid);
DOM.setElementProperty(labelElem, "htmlFor", uid);
+
+ // Accessibility: setting tab index to be 0 by default, ensuring element
+ // appears in tab sequence. FocusWidget's setElement method already
+ // calls setTabIndex, which is overridden below. However, at the time
+ // that this call is made, inputElem has not been created. So, we have
+ // to call setTabIndex again, once inputElem has been created.
+ setTabIndex(0);
}
@Override
@@ -163,7 +169,13 @@
@Override
public void setTabIndex(int index) {
- getFocusImpl().setTabIndex(inputElem, index);
+ // Need to guard against call to setTabIndex before inputElem is initialized.
+ // This happens because FocusWidget's (a superclass of CheckBox) setElement method
+ // calls setTabIndex before inputElem is initialized. See CheckBox's protected
+ // constructor for more information.
+ if (inputElem != null) {
+ getFocusImpl().setTabIndex(inputElem, index);
+ }
}
@Override
@@ -247,7 +259,7 @@
// Setup the new element
DOM.sinkEvents(inputElem, sunkEvents);
DOM.setElementProperty(inputElem, "id", uid);
- if (accessKey != "") {
+ if (!accessKey.equals("")) {
DOM.setElementProperty(inputElem, "accessKey", accessKey);
}
setTabIndex(tabIndex);
diff --git a/user/src/com/google/gwt/user/client/ui/CustomButton.java b/user/src/com/google/gwt/user/client/ui/CustomButton.java
index 6810922..87da861 100644
--- a/user/src/com/google/gwt/user/client/ui/CustomButton.java
+++ b/user/src/com/google/gwt/user/client/ui/CustomButton.java
@@ -411,6 +411,9 @@
sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.FOCUSEVENTS);
setUpFace(createFace(null, "up", UP));
setStyleName(STYLENAME_DEFAULT);
+
+ // Add a11y role "button"
+ Accessibility.setRole(getElement(), Accessibility.ROLE_BUTTON);
}
/**
diff --git a/user/src/com/google/gwt/user/client/ui/FocusWidget.java b/user/src/com/google/gwt/user/client/ui/FocusWidget.java
index ebbf56b..e1ea806 100644
--- a/user/src/com/google/gwt/user/client/ui/FocusWidget.java
+++ b/user/src/com/google/gwt/user/client/ui/FocusWidget.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
@@ -164,6 +164,18 @@
impl.setTabIndex(getElement(), index);
}
+ @Override
+ protected void setElement(Element elem) {
+ super.setElement(elem);
+
+ // Accessibility: setting tab index to be 0 by default, ensuring element
+ // appears in tab sequence. Note that this call will not interfere with
+ // any calls made to FocusWidget.setTabIndex(int) by user code, because
+ // FocusWidget.setTabIndex(int) cannot be called until setElement(elem)
+ // has been called.
+ setTabIndex(0);
+ }
+
/**
* Fire all current {@link ClickListener}.
*/
diff --git a/user/src/com/google/gwt/user/client/ui/HTMLPanel.java b/user/src/com/google/gwt/user/client/ui/HTMLPanel.java
index 323ed2e..fb7e18e 100644
--- a/user/src/com/google/gwt/user/client/ui/HTMLPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/HTMLPanel.java
@@ -26,8 +26,6 @@
*/
public class HTMLPanel extends ComplexPanel {
- private static int sUid;
-
/**
* A helper method for creating unique IDs for elements within dynamically-
* generated HTML. This is important because no two elements in a document
@@ -36,7 +34,7 @@
* @return a new unique identifier
*/
public static String createUniqueId() {
- return "HTMLPanel_" + (++sUid);
+ return DOM.createUniqueId();
}
private Element hiddenDiv;
diff --git a/user/src/com/google/gwt/user/client/ui/MenuBar.java b/user/src/com/google/gwt/user/client/ui/MenuBar.java
index cf1df6e..757f44e 100644
--- a/user/src/com/google/gwt/user/client/ui/MenuBar.java
+++ b/user/src/com/google/gwt/user/client/ui/MenuBar.java
@@ -97,11 +97,15 @@
this.vertical = vertical;
- Element outer = DOM.createDiv();
+ Element outer = FocusPanel.impl.createFocusable();
DOM.appendChild(outer, table);
setElement(outer);
- sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT);
+ Accessibility.setRole(getElement(), Accessibility.ROLE_MENUBAR);
+
+ sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT
+ | Event.ONFOCUS | Event.ONKEYDOWN);
+
setStyleName("gwt-MenuBar");
if (vertical) {
addStyleDependentName("vertical");
@@ -239,6 +243,7 @@
MenuItem item = findItem(DOM.eventGetTarget(event));
switch (DOM.eventGetType(event)) {
case Event.ONCLICK: {
+ FocusPanel.impl.focus(getElement());
// Fire an item's command when the user clicks on it.
if (item != null) {
doItemAction(item, true);
@@ -259,7 +264,40 @@
}
break;
}
- }
+
+ case Event.ONFOCUS: {
+ selectFirstItemIfNoneSelected();
+ break;
+ }
+
+ case Event.ONKEYDOWN: {
+ int keyCode = DOM.eventGetKeyCode(event);
+ switch (keyCode) {
+ case KeyboardListener.KEY_LEFT:
+ moveLeft();
+ break;
+ case KeyboardListener.KEY_RIGHT:
+ moveRight();
+ break;
+ case KeyboardListener.KEY_UP:
+ moveUp();
+ break;
+ case KeyboardListener.KEY_DOWN:
+ moveDown();
+ break;
+ case KeyboardListener.KEY_ESCAPE:
+ closeAllParents();
+ break;
+ case KeyboardListener.KEY_ENTER:
+ if (!selectFirstItemIfNoneSelected()) {
+ doItemAction(selectedItem, true);
+ }
+ break;
+ } // end switch(keyCode)
+
+ break;
+ } // end case Event.ONKEYDOWN
+ } // end switch (DOM.eventGetType(event))
}
public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
@@ -361,14 +399,8 @@
*/
void closeAllParents() {
MenuBar curMenu = this;
- while (curMenu != null) {
+ while (curMenu.parentMenu != null) {
curMenu.close();
-
- if ((curMenu.parentMenu == null) && (curMenu.selectedItem != null)) {
- curMenu.selectedItem.setSelectionStyle(false);
- curMenu.selectedItem = null;
- }
-
curMenu = curMenu.parentMenu;
}
}
@@ -395,7 +427,7 @@
}
// If the item has no popup, optionally fire its command.
- if (item.getSubMenu() == null) {
+ if ((item != null) && (item.getSubMenu() == null)) {
if (fireCommand) {
// Close this menu and all of its parents.
closeAllParents();
@@ -412,6 +444,10 @@
// Ensure that the item is selected.
selectItem(item);
+ if (item == null) {
+ return;
+ }
+
// Create a new popup for this item, and position it next to
// the item (below if this is a horizontal menu bar, to the
// right if it's a vertical bar).
@@ -457,6 +493,7 @@
// Show the popup, ensuring that the menubar's event preview remains on top
// of the popup's.
popup.show();
+ shownChildMenu.focus();
}
void itemOver(MenuItem item) {
@@ -492,6 +529,9 @@
if (item != null) {
item.setSelectionStyle(true);
+
+ Accessibility.setState(getElement(), Accessibility.STATE_ACTIVEDESCENDANT,
+ DOM.getElementAttribute(item.getElement(), "id"));
}
selectedItem = item;
@@ -537,16 +577,18 @@
}
private MenuItem findItem(Element hItem) {
- for (int i = 0; i < items.size(); ++i) {
- MenuItem item = items.get(i);
+ for (MenuItem item : items) {
if (DOM.isOrHasChild(item.getElement(), hItem)) {
return item;
}
}
-
return null;
}
+ private void focus() {
+ FocusPanel.impl.focus(getElement());
+ }
+
private Element getItemContainerElement() {
if (vertical) {
return body;
@@ -555,6 +597,76 @@
}
}
+ private void moveDown() {
+ if (selectFirstItemIfNoneSelected()) {
+ return;
+ }
+
+ if (vertical) {
+ selectNextItem();
+ } else {
+ if (selectedItem.getSubMenu() != null) {
+ doItemAction(selectedItem, false);
+ } else if (parentMenu != null) {
+ if (parentMenu.vertical) {
+ parentMenu.selectNextItem();
+ } else {
+ parentMenu.moveDown();
+ }
+ }
+ }
+ }
+
+ private void moveLeft() {
+ if (selectFirstItemIfNoneSelected()) {
+ return;
+ }
+
+ if (!vertical) {
+ selectPrevItem();
+ } else {
+ if ((parentMenu != null) && (!parentMenu.vertical)) {
+ parentMenu.selectPrevItem();
+ } else {
+ close();
+ }
+ }
+ }
+
+ private void moveRight() {
+ if (selectFirstItemIfNoneSelected()) {
+ return;
+ }
+
+ if (!vertical) {
+ selectNextItem();
+ } else {
+ if ((shownChildMenu == null) && (selectedItem.getSubMenu() != null)) {
+ doItemAction(selectedItem, false);
+ } else if (parentMenu != null) {
+ if (!parentMenu.vertical) {
+ parentMenu.selectNextItem();
+ } else {
+ parentMenu.moveRight();
+ }
+ }
+ }
+ }
+
+ private void moveUp() {
+ if (selectFirstItemIfNoneSelected()) {
+ return;
+ }
+
+ if ((shownChildMenu == null) && vertical) {
+ selectPrevItem();
+ } else if ((parentMenu != null) && parentMenu.vertical) {
+ parentMenu.selectPrevItem();
+ } else {
+ close();
+ }
+ }
+
/*
* This method is called when a menu bar is hidden, so that it can hide any
* child popups that are currently being shown.
@@ -563,6 +675,7 @@
if (shownChildMenu != null) {
shownChildMenu.onHide();
popup.hide();
+ focus();
}
}
@@ -594,4 +707,69 @@
allItems.remove(idx);
return true;
}
+
+ /**
+ * Selects the first item in the menu if no items are currently selected. This method
+ * assumes that the menu has at least 1 item.
+ *
+ * @return true if no item was previously selected and the first item in the list was selected,
+ * false otherwise
+ */
+ private boolean selectFirstItemIfNoneSelected() {
+ if (selectedItem == null) {
+ MenuItem nextItem = items.get(0);
+ selectItem(nextItem);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void selectNextItem() {
+ if (selectedItem == null) {
+ return;
+ }
+
+ int index = items.indexOf(selectedItem);
+ // We know that selectedItem is set to an item that is contained in the items collection.
+ // Therefore, we know that index can never be -1.
+ assert (index != -1);
+
+ MenuItem itemToBeSelected;
+
+ if (index < items.size() - 1) {
+ itemToBeSelected = items.get(index + 1);
+ } else { // we're at the end, loop around to the start
+ itemToBeSelected = items.get(0);
+ }
+
+ selectItem(itemToBeSelected);
+ if (shownChildMenu != null) {
+ doItemAction(itemToBeSelected, false);
+ }
+ }
+
+ private void selectPrevItem() {
+ if (selectedItem == null) {
+ return;
+ }
+
+ int index = items.indexOf(selectedItem);
+ // We know that selectedItem is set to an item that is contained in the items collection.
+ // Therefore, we know that index can never be -1.
+ assert (index != -1);
+
+ MenuItem itemToBeSelected;
+ if (index > 0) {
+ itemToBeSelected = items.get(index - 1);
+
+ } else { // we're at the start, loop around to the end
+ itemToBeSelected = items.get(items.size() - 1);
+ }
+
+ selectItem(itemToBeSelected);
+ if (shownChildMenu != null) {
+ doItemAction(itemToBeSelected, false);
+ }
+ }
}
diff --git a/user/src/com/google/gwt/user/client/ui/MenuItem.java b/user/src/com/google/gwt/user/client/ui/MenuItem.java
index e03ae76..d201f30 100644
--- a/user/src/com/google/gwt/user/client/ui/MenuItem.java
+++ b/user/src/com/google/gwt/user/client/ui/MenuItem.java
@@ -23,6 +23,9 @@
* {@link com.google.gwt.user.client.ui.MenuBar}. Menu items can either fire a
* {@link com.google.gwt.user.client.Command} when they are clicked, or open a
* cascading sub-menu.
+ *
+ * Each menu item is assigned a unique DOM id in order to support ARIA. See
+ * {@link com.google.gwt.user.client.ui.Accessibility} for more information.
*/
public class MenuItem extends UIObject implements HasHTML {
@@ -87,6 +90,10 @@
setText(text);
}
setStyleName("gwt-MenuItem");
+
+ DOM.setElementAttribute(getElement(), "id", DOM.createUniqueId());
+ // Add a11y role "menuitem"
+ Accessibility.setRole(getElement(), Accessibility.ROLE_MENUITEM);
}
/**
@@ -144,6 +151,13 @@
*/
public void setSubMenu(MenuBar subMenu) {
this.subMenu = subMenu;
+
+ // Change tab index from 0 to -1, because only the root menu is supposed to
+ // be in the tab order
+ FocusPanel.impl.setTabIndex(subMenu.getElement(), -1);
+
+ // Update a11y role "haspopup"
+ Accessibility.setState(this.getElement(), Accessibility.STATE_HASPOPUP, "true");
}
public void setText(String text) {
diff --git a/user/src/com/google/gwt/user/client/ui/TabBar.java b/user/src/com/google/gwt/user/client/ui/TabBar.java
index 4d1a90d..2477531 100644
--- a/user/src/com/google/gwt/user/client/ui/TabBar.java
+++ b/user/src/com/google/gwt/user/client/ui/TabBar.java
@@ -43,7 +43,7 @@
* </p>
*/
public class TabBar extends Composite implements SourcesTabEvents,
- ClickListener {
+ ClickListener, KeyboardListener {
/**
* <code>ClickDecoratorPanel</code> decorates any widget with the minimal
@@ -52,20 +52,34 @@
* single observer is needed.
*/
private static final class ClickDecoratorPanel extends SimplePanel {
- ClickListener delegate;
+ ClickListener clickDelegate;
+ KeyboardListener keyDelegate;
- ClickDecoratorPanel(Widget child, ClickListener delegate) {
- this.delegate = delegate;
+ ClickDecoratorPanel(Widget child, ClickListener cDelegate,
+ KeyboardListener kDelegate) {
+
+ // The panel needs to be able to get keyboard focus for tab navigation
+ super(FocusPanel.impl.createFocusable());
+
+ this.clickDelegate = cDelegate;
+ this.keyDelegate = kDelegate;
setWidget(child);
- sinkEvents(Event.ONCLICK);
+ sinkEvents(Event.ONCLICK | Event.ONKEYDOWN);
}
@Override
public void onBrowserEvent(Event event) {
// No need for call to super.
switch (DOM.eventGetType(event)) {
+
case Event.ONCLICK:
- delegate.onClick(this);
+ clickDelegate.onClick(this);
+ break;
+
+ case Event.ONKEYDOWN:
+ keyDelegate.onKeyDown(this, ((char) DOM.eventGetKeyCode(event)),
+ KeyboardListenerCollection.getKeyboardModifiers(event));
+ break;
}
}
}
@@ -83,6 +97,9 @@
sinkEvents(Event.ONCLICK);
setStyleName("gwt-TabBar");
+ // Add a11y role "tablist"
+ Accessibility.setRole(panel.getElement(), Accessibility.ROLE_TABLIST);
+
panel.setVerticalAlignment(HasVerticalAlignment.ALIGN_BOTTOM);
HTML first = new HTML(" ", true), rest = new HTML(" ", true);
@@ -192,12 +209,8 @@
item = new Label(text);
}
- item.setWordWrap(false);
- item.addClickListener(this);
- item.setStyleName(STYLENAME_DEFAULT);
- panel.insert(item, beforeIndex + 1);
- setStyleName(DOM.getParent(item.getElement()), STYLENAME_DEFAULT
- + "-wrapper", true);
+ item.setWordWrap(false);
+ insertTabImpl(item, beforeIndex);
}
/**
@@ -218,23 +231,25 @@
*/
public void insertTab(Widget widget, int beforeIndex) {
checkInsertBeforeTabIndex(beforeIndex);
-
- ClickDecoratorPanel decWidget = new ClickDecoratorPanel(widget, this);
- decWidget.addStyleName(STYLENAME_DEFAULT);
- panel.insert(decWidget, beforeIndex + 1);
- setStyleName(DOM.getParent(decWidget.getElement()), STYLENAME_DEFAULT
- + "-wrapper", true);
+ insertTabImpl(widget, beforeIndex);
}
public void onClick(Widget sender) {
- for (int i = 1; i < panel.getWidgetCount() - 1; ++i) {
- if (panel.getWidget(i) == sender) {
- selectTab(i - 1);
- return;
- }
+ selectTabByTabWidget(sender);
+ }
+
+ public void onKeyDown(Widget sender, char keyCode, int modifiers) {
+ if (keyCode == KeyboardListener.KEY_ENTER) {
+ selectTabByTabWidget(sender);
}
}
+ public void onKeyPress(Widget sender, char keyCode, int modifiers) {
+ }
+
+ public void onKeyUp(Widget sender, char keyCode, int modifiers) {
+ }
+
/**
* Removes the tab at the specified index.
*
@@ -323,6 +338,38 @@
}
}
+ private void insertTabImpl(Widget widget, int beforeIndex) {
+ ClickDecoratorPanel decWidget = new ClickDecoratorPanel(widget, this, this);
+ decWidget.addStyleName(STYLENAME_DEFAULT);
+ // Add a11y role "tab"
+ Accessibility.setRole(decWidget.getElement(), Accessibility.ROLE_TAB);
+
+ panel.insert(decWidget, beforeIndex + 1);
+ setStyleName(DOM.getParent(decWidget.getElement()), STYLENAME_DEFAULT
+ + "-wrapper", true);
+ }
+
+ /**
+ * Selects the tab corresponding to the widget for the tab. To be clear
+ * the widget for the tab is not the widget INSIDE of the tab; it is the
+ * widget used to represent the tab itself.
+ *
+ * @param tabWidget The widget for the tab to be selected
+ * @return true if the tab corresponding to the widget for the tab could located and selected,
+ * false otherwise
+ */
+ private boolean selectTabByTabWidget(Widget tabWidget) {
+ int numTabs = panel.getWidgetCount() - 1;
+
+ for (int i = 1; i < numTabs; ++i) {
+ if (panel.getWidget(i) == tabWidget) {
+ return selectTab(i - 1);
+ }
+ }
+
+ return false;
+ }
+
private void setSelectionStyle(Widget item, boolean selected) {
if (item != null) {
if (selected) {
diff --git a/user/src/com/google/gwt/user/client/ui/TabPanel.java b/user/src/com/google/gwt/user/client/ui/TabPanel.java
index 4fa2c43..b4bf75d 100644
--- a/user/src/com/google/gwt/user/client/ui/TabPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/TabPanel.java
@@ -193,6 +193,8 @@
initWidget(panel);
setStyleName("gwt-TabPanel");
deck.setStyleName("gwt-TabPanelBottom");
+ // Add a11y role "tabpanel"
+ Accessibility.setRole(deck.getElement(), Accessibility.ROLE_TABPANEL);
}
public void add(Widget w) {
diff --git a/user/src/com/google/gwt/user/client/ui/Tree.java b/user/src/com/google/gwt/user/client/ui/Tree.java
index 6fdc56a..a62a5fc 100644
--- a/user/src/com/google/gwt/user/client/ui/Tree.java
+++ b/user/src/com/google/gwt/user/client/ui/Tree.java
@@ -57,7 +57,7 @@
private static class ImagesFromImageBase implements TreeImages {
/**
- * A convience image prototype that implements
+ * A convenience image prototype that implements
* {@link AbstractImagePrototype#applyTo(Image)} for a specified image name.
*/
private class Prototype extends AbstractImagePrototype {
@@ -110,6 +110,16 @@
}
}
+ static native boolean shouldTreeDelegateFocusToElement(Element elem) /*-{
+ var name = elem.nodeName;
+ return ((name == "SELECT") ||
+ (name == "INPUT") ||
+ (name == "TEXTAREA") ||
+ (name == "OPTION") ||
+ (name == "BUTTON") ||
+ (name == "LABEL"));
+ }-*/;
+
/**
* Map of TreeItem.widget -> TreeItem.
*/
@@ -191,6 +201,10 @@
};
root.setTree(this);
setStyleName("gwt-Tree");
+
+ // Add a11y role "tree"
+ Accessibility.setRole(getElement(), Accessibility.ROLE_TREE);
+ Accessibility.setRole(focusable, Accessibility.ROLE_TREEITEM);
}
/**
@@ -365,7 +379,7 @@
// to be sunk on individual items' open/close images. This leads to an
// extra event reaching the Tree, which we will ignore here.
if (DOM.eventGetCurrentTarget(event).equals(getElement())) {
- elementClicked(root, DOM.eventGetTarget(event));
+ elementClicked(DOM.eventGetTarget(event));
}
break;
}
@@ -399,7 +413,6 @@
}
case Event.ONFOCUS:
- // If we already have focus, ignore the focus event.
if (focusListeners != null) {
focusListeners.fireFocusEvent(this, event);
}
@@ -713,7 +726,7 @@
chain.add(hElem);
}
- private boolean elementClicked(TreeItem root, Element hElem) {
+ private boolean elementClicked(Element hElem) {
ArrayList<Element> chain = new ArrayList<Element>();
collectElementChain(chain, getElement(), hElem);
@@ -759,19 +772,17 @@
}
/**
- * Move the tree focus to the specified selected item.
- *
- * @param selection
+ * Move the tree focus to the currently selected item.
*/
- private void moveFocus(TreeItem selection) {
- HasFocus focusableWidget = selection.getFocusableWidget();
+ private void moveFocus() {
+ HasFocus focusableWidget = curSelection.getFocusableWidget();
if (focusableWidget != null) {
focusableWidget.setFocus(true);
DOM.scrollIntoView(((Widget) focusableWidget).getElement());
} else {
// Get the location and size of the given item's content element relative
// to the tree.
- Element selectedElem = selection.getContentElem();
+ Element selectedElem = curSelection.getContentElem();
int containerLeft = getAbsoluteLeft();
int containerTop = getAbsoluteTop();
@@ -790,9 +801,12 @@
// Scroll it into view.
DOM.scrollIntoView(focusable);
+ // Update ARIA attributes to reflect the information from the newly-selected item.
+ updateAriaAttributes();
+
// Ensure Focus is set, as focus may have been previously delegated by
// tree.
- FocusPanel.impl.focus(focusable);
+ setFocus(true);
}
}
@@ -853,7 +867,7 @@
curSelection = item;
if (moveFocus && curSelection != null) {
- moveFocus(curSelection);
+ moveFocus();
// Select the item and fire the selection event.
curSelection.setSelected(true);
@@ -863,13 +877,67 @@
}
}
- private native boolean shouldTreeDelegateFocusToElement(Element elem) /*-{
- var name = elem.nodeName;
- return ((name == "SELECT") ||
- (name == "INPUT") ||
- (name == "TEXTAREA") ||
- (name == "OPTION") ||
- (name == "BUTTON") ||
- (name == "LABEL"));
- }-*/;
+ private void updateAriaAttributes() {
+
+ Element curSelectionContentElem = curSelection.getContentElem();
+
+ // Set the 'aria-level' state. To do this, we need to compute the level of the
+ // currently selected item.
+
+ // We initialize itemLevel to -1 because the level value is zero-based.
+ // Note that the root node is not a part of the TreeItem hierachy, and we
+ // do not consider the root node to have a designated level. The level of
+ // the root's children is level 0, its children's children is level 1, etc.
+
+ int curSelectionLevel = -1;
+ TreeItem tempItem = curSelection;
+
+ while (tempItem != null) {
+ tempItem = tempItem.getParentItem();
+ ++curSelectionLevel;
+ }
+
+ Accessibility.setState(curSelectionContentElem, Accessibility.STATE_LEVEL,
+ String.valueOf(curSelectionLevel + 1));
+
+ // Set the 'aria-setsize' and 'aria-posinset' states. To do this, we need to
+ // compute the the number of siblings that the currently selected item has, and
+ // the item's position among its siblings.
+
+ TreeItem curSelectionParent = curSelection.getParentItem();
+ if (curSelectionParent == null) {
+ curSelectionParent = root;
+ }
+
+ Accessibility.setState(curSelectionContentElem, Accessibility.STATE_SETSIZE,
+ String.valueOf(curSelectionParent.getChildCount()));
+
+ int curSelectionIndex = curSelectionParent.getChildIndex(curSelection);
+
+ Accessibility.setState(curSelectionContentElem, Accessibility.STATE_POSINSET,
+ String.valueOf(curSelectionIndex + 1));
+
+ // Set the 'aria-expanded' state. This depends on the state of the currently selected item.
+ // If the item has no children, we remove the 'aria-expanded' state.
+
+ if (curSelection.getChildCount() == 0) {
+ Accessibility.removeState(curSelectionContentElem, Accessibility.STATE_EXPANDED);
+ } else {
+ if (curSelection.getState()) {
+ Accessibility.setState(curSelectionContentElem, Accessibility.STATE_EXPANDED, "true");
+ } else {
+ Accessibility.setState(curSelectionContentElem, Accessibility.STATE_EXPANDED, "false");
+ }
+ }
+
+ // Make sure that 'aria-selected' is true.
+
+ Accessibility.setState(curSelectionContentElem, Accessibility.STATE_SELECTED, "true");
+
+ // Update the 'aria-activedescendant' state for the focusable element to match the id
+ // of the currently selected item
+
+ Accessibility.setState(focusable, Accessibility.STATE_ACTIVEDESCENDANT,
+ DOM.getElementAttribute(curSelectionContentElem, "id"));
+ }
}
diff --git a/user/src/com/google/gwt/user/client/ui/TreeItem.java b/user/src/com/google/gwt/user/client/ui/TreeItem.java
index cbe236d..45f6bee 100644
--- a/user/src/com/google/gwt/user/client/ui/TreeItem.java
+++ b/user/src/com/google/gwt/user/client/ui/TreeItem.java
@@ -24,6 +24,10 @@
/**
* An item that can be contained within a
* {@link com.google.gwt.user.client.ui.Tree}.
+ *
+ * Each tree item is assigned a unique DOM id in order to support ARIA. See
+ * {@link com.google.gwt.user.client.ui.Accessibility} for more information.
+ *
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.TreeExample}
@@ -79,6 +83,9 @@
DOM.setStyleAttribute(getElement(), "whiteSpace", "nowrap");
DOM.setStyleAttribute(childSpanElem, "whiteSpace", "nowrap");
setStyleName(contentElem, "gwt-TreeItem", true);
+
+ Accessibility.setRole(contentElem, Accessibility.ROLE_TREEITEM);
+ DOM.setElementAttribute(contentElem, "id", DOM.createUniqueId());
}
/**
@@ -115,10 +122,9 @@
/**
* Adds another item as a child to this one.
- *
+ *
* @param item the item to be added
*/
-
public void addItem(TreeItem item) {
// Detach item from existing parent.
if ((item.getParentItem() != null) || (item.getTree() != null)) {
@@ -394,6 +400,13 @@
if (tree != null) {
tree.adopt(widget, this);
}
+
+ // Set tabIndex on the widget to -1, so that it doesn't mess up the tab
+ // order of the entire tree
+
+ if (Tree.shouldTreeDelegateFocusToElement(widget.getElement())) {
+ DOM.setElementAttribute(widget.getElement(), "tabIndex", "-1");
+ }
}
}
@@ -458,7 +471,7 @@
Element getImageElement() {
return statusImage.getElement();
}
-
+
void setParentItem(TreeItem parent) {
this.parent = parent;
}
diff --git a/user/src/com/google/gwt/user/client/ui/impl/AccessibilityImpl.java b/user/src/com/google/gwt/user/client/ui/impl/AccessibilityImpl.java
new file mode 100644
index 0000000..90a9b55
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/AccessibilityImpl.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui.impl;
+
+import com.google.gwt.user.client.Element;
+
+/**
+ * Native implementation class used with
+ * {@link com.google.gwt.user.client.ui.Accessibility}.
+ */
+public class AccessibilityImpl {
+
+ public String getRole(Element elem) {
+ return "";
+ }
+
+ public String getState(Element elem, String stateName) {
+ return "";
+ }
+
+ public void removeState(Element elem, String stateName) {
+ }
+
+ public void setRole(Element elem, String roleName) {
+ }
+
+ public void setState(Element elem, String stateName, String stateValue) {
+ }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/impl/AccessibilityImplMozilla.java b/user/src/com/google/gwt/user/client/ui/impl/AccessibilityImplMozilla.java
new file mode 100644
index 0000000..c3ab5f5
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/AccessibilityImplMozilla.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui.impl;
+
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.DOM;
+
+/**
+ * Firefox 1.5+ implementation of {@link AccessibilityImpl}.
+ */
+public class AccessibilityImplMozilla extends AccessibilityImpl {
+
+ private static final String ATTR_NAME_ROLE = "role";
+
+ public String getRole(Element elem) {
+ return DOM.getElementAttribute(elem, ATTR_NAME_ROLE);
+ }
+
+ public String getState(Element elem, String stateName) {
+ return DOM.getElementAttribute(elem, stateName);
+ }
+
+ public void removeState(Element elem, String stateName) {
+ DOM.removeElementAttribute(elem, stateName);
+ }
+
+ public void setRole(Element elem, String roleName) {
+ DOM.setElementAttribute(elem, ATTR_NAME_ROLE, roleName);
+ }
+
+ public void setState(Element elem, String stateName, String stateValue) {
+ DOM.setElementAttribute(elem, stateName, stateValue);
+ }
+}
diff --git a/user/src/com/google/gwt/user/tools/AppHtml.htmlsrc b/user/src/com/google/gwt/user/tools/AppHtml.htmlsrc
index c92da8d..269f56c 100644
--- a/user/src/com/google/gwt/user/tools/AppHtml.htmlsrc
+++ b/user/src/com/google/gwt/user/tools/AppHtml.htmlsrc
@@ -46,7 +46,7 @@
<body>
<!-- OPTIONAL: include this if you want history support -->
- <iframe src="javascript:''" id="__gwt_historyFrame" style="position:absolute;width:0;height:0;border:0"></iframe>
+ <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
<h1>@className</h1>