First pass at keyboard navigation (currently only for CellTable)
Review at http://gwt-code-reviews.appspot.com/710802
Review by: jlabanca@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8488 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/cell/client/AbstractCell.java b/user/src/com/google/gwt/cell/client/AbstractCell.java
index 288cb5a..b1f8f2b 100644
--- a/user/src/com/google/gwt/cell/client/AbstractCell.java
+++ b/user/src/com/google/gwt/cell/client/AbstractCell.java
@@ -77,6 +77,14 @@
}
/**
+ * Returns false. Subclasses that support editing should override this method
+ * to return the current editing status.
+ */
+ public boolean isEditing(Element element, C value, Object key) {
+ return false;
+ }
+
+ /**
* {@inheritDoc}
*
* This method is a no-op in {@link AbstractCell}. If you override this method
diff --git a/user/src/com/google/gwt/cell/client/Cell.java b/user/src/com/google/gwt/cell/client/Cell.java
index 7e3b974..96d5310 100644
--- a/user/src/com/google/gwt/cell/client/Cell.java
+++ b/user/src/com/google/gwt/cell/client/Cell.java
@@ -62,6 +62,18 @@
boolean handlesSelection();
/**
+ * Returns true if the cell is currently editing the data identified by the
+ * given element and key. While a cell is editing, widgets containing the cell
+ * may chooses to pass keystrokes directly to the cell rather than using them
+ * for navigation purposes.
+ *
+ * @param parent the parent Element
+ * @param value the value associated with the cell
+ * @param key the unique key associated with the row object
+ */
+ boolean isEditing(Element parent, C value, Object key);
+
+ /**
* Handle a browser event that took place within the cell. The default
* implementation returns null.
*
@@ -77,7 +89,6 @@
/**
* Render a cell as HTML into a StringBuilder, suitable for passing to
* {@link Element#setInnerHTML} on a container element.
- *
* @param value the cell value to be rendered
* @param key the unique key associated with the row object
* @param sb the StringBuilder to be written to
diff --git a/user/src/com/google/gwt/cell/client/CheckboxCell.java b/user/src/com/google/gwt/cell/client/CheckboxCell.java
index 7aa6edc..8839a24 100644
--- a/user/src/com/google/gwt/cell/client/CheckboxCell.java
+++ b/user/src/com/google/gwt/cell/client/CheckboxCell.java
@@ -18,9 +18,11 @@
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
/**
- * A {@link Cell} used to render a checkbox.
+ * A {@link Cell} used to render a checkbox. The value of the checkbox
+ * may be toggled using the ENTER key as well as via mouse click.
*
* <p>
* Note: This class is new and its interface subject to change.
@@ -43,7 +45,7 @@
* @param isSelectBox true if the cell controls the selection state
*/
public CheckboxCell(boolean isSelectBox) {
- super("change");
+ super("change", "keyup");
this.isSelectBox = isSelectBox;
}
@@ -61,9 +63,19 @@
public void onBrowserEvent(Element parent, Boolean value, Object key,
NativeEvent event, ValueUpdater<Boolean> valueUpdater) {
String type = event.getType();
- if ("change".equals(type)) {
+
+ boolean enterPressed = "keyup".equals(type) &&
+ event.getKeyCode() == KeyCodes.KEY_ENTER;
+ if ("change".equals(type) || enterPressed) {
InputElement input = parent.getFirstChild().cast();
Boolean isChecked = input.isChecked();
+
+ // If the enter key was pressed, toggle the value
+ if (enterPressed) {
+ isChecked = !isChecked;
+ input.setChecked(isChecked);
+ }
+
setViewData(key, isChecked);
if (valueUpdater != null) {
valueUpdater.update(isChecked);
diff --git a/user/src/com/google/gwt/cell/client/EditTextCell.java b/user/src/com/google/gwt/cell/client/EditTextCell.java
index 7bfaea5..44c3874 100644
--- a/user/src/com/google/gwt/cell/client/EditTextCell.java
+++ b/user/src/com/google/gwt/cell/client/EditTextCell.java
@@ -123,7 +123,12 @@
}
public EditTextCell() {
- super("click", "keydown", "keyup", "blur");
+ super("click", "keyup", "keydown", "blur");
+ }
+
+ public boolean isEditing(Element element, String value, Object key) {
+ ViewData viewData = getViewData(key);
+ return viewData == null ? false : viewData.isEditing();
}
@Override
@@ -133,15 +138,21 @@
if (viewData != null && viewData.isEditing()) {
// Handle the edit event.
editEvent(parent, key, viewData, event, valueUpdater);
- } else if ("click".equals(event.getType())) {
- // Go into edit mode.
- if (viewData == null) {
- viewData = new ViewData(value);
- setViewData(key, viewData);
- } else {
- viewData.setEditing(true);
+ } else {
+ String type = event.getType();
+ int keyCode = event.getKeyCode();
+ boolean enterPressed = "keyup".equals(type) &&
+ keyCode == KeyCodes.KEY_ENTER;
+ if ("click".equals(type) || enterPressed) {
+ // Go into edit mode.
+ if (viewData == null) {
+ viewData = new ViewData(value);
+ setViewData(key, viewData);
+ } else {
+ viewData.setEditing(true);
+ }
+ edit(parent, value, key);
}
- edit(parent, value, key);
}
}
@@ -209,12 +220,14 @@
private void editEvent(Element parent, Object key, ViewData viewData,
NativeEvent event, ValueUpdater<String> valueUpdater) {
String type = event.getType();
- if ("keydown".equals(type)) {
+ boolean keyUp = "keyup".equals(type);
+ boolean keyDown = "keydown".equals(type);
+ if (keyUp || keyDown) {
int keyCode = event.getKeyCode();
- if (keyCode == KeyCodes.KEY_ENTER) {
+ if (keyUp && keyCode == KeyCodes.KEY_ENTER) {
// Commit the change.
commit(parent, viewData, valueUpdater);
- } else if (keyCode == KeyCodes.KEY_ESCAPE) {
+ } else if (keyUp && keyCode == KeyCodes.KEY_ESCAPE) {
// Cancel edit mode.
String originalText = viewData.getOriginal();
if (viewData.isEditingAgain()) {
@@ -224,10 +237,10 @@
setViewData(key, null);
}
cancel(parent, originalText);
+ } else {
+ // Update the text in the view data on each key.
+ updateViewData(parent, viewData, true);
}
- } else if ("keyup".equals(type)) {
- // Update the text in the view data on each key.
- updateViewData(parent, viewData, true);
} else if ("blur".equals(type)) {
// Commit the change. Ensure that we are blurring the input element and
// not the parent element itself.
diff --git a/user/src/com/google/gwt/cell/client/IconCellDecorator.java b/user/src/com/google/gwt/cell/client/IconCellDecorator.java
index f2575a5..3a1bb92 100644
--- a/user/src/com/google/gwt/cell/client/IconCellDecorator.java
+++ b/user/src/com/google/gwt/cell/client/IconCellDecorator.java
@@ -77,6 +77,10 @@
public boolean handlesSelection() {
return cell.handlesSelection();
}
+
+ public boolean isEditing(Element element, C value, Object key) {
+ return cell.isEditing(element, value, key);
+ }
public void onBrowserEvent(Element parent, C value, Object key,
NativeEvent event, ValueUpdater<C> valueUpdater) {
diff --git a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
index 52b7196..7d2c3a3 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
@@ -205,6 +205,10 @@
public boolean handlesSelection() {
return cell.handlesSelection();
}
+
+ public boolean isEditing(Element element, C value, Object key) {
+ return cell.isEditing(element, value, key);
+ }
public void onBrowserEvent(Element parent, C value, Object key,
NativeEvent event, ValueUpdater<C> valueUpdater) {
diff --git a/user/src/com/google/gwt/user/cellview/client/CellList.java b/user/src/com/google/gwt/user/cellview/client/CellList.java
index ec4e915..bc17dec 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellList.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellList.java
@@ -122,6 +122,9 @@
}
}
+ public void resetFocus() {
+ }
+
public void setLoadingState(LoadingState state) {
showOrHide(emptyMessageElem, state == LoadingState.EMPTY);
// TODO(jlabanca): Add a loading icon.
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTable.java b/user/src/com/google/gwt/user/cellview/client/CellTable.java
index 5678e35..ce3ccae 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.java
@@ -27,6 +27,7 @@
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
@@ -161,6 +162,16 @@
String hoveredRow();
/**
+ * Applied to the keyboard selected cell.
+ */
+ String keyboardSelectedCell();
+
+ /**
+ * Applied to the keyboard selected row.
+ */
+ String keyboardSelectedRow();
+
+ /**
* Applied to the last column.
*/
String lastColumn();
@@ -381,6 +392,19 @@
TABLE_IMPL.replaceAllRows(
CellTable.this, tbody, CellBasedWidgetImpl.get().processHtml(html));
}
+
+ public void resetFocus() {
+ int pageStart = getPageStart();
+ int offset = keyboardSelectedRow - pageStart;
+ if (offset >= 0 && offset < getPageSize()) {
+ TableRowElement tr = getRowElement(offset);
+ TableCellElement td = tr.getCells().getItem(keyboardSelectedColumn);
+ tr.addClassName(style.keyboardSelectedRow());
+ td.addClassName(style.keyboardSelectedCell());
+ td.setTabIndex(0);
+ td.focus(); // TODO (rice) only focus if we were focused previously
+ }
+ }
public void setLoadingState(LoadingState state) {
setLoadingIconVisible(state == LoadingState.LOADING);
@@ -416,11 +440,17 @@
return DEFAULT_RESOURCES;
}
+ private boolean cellIsEditing;
private final TableColElement colgroup;
+
private List<Column<T, ?>> columns = new ArrayList<Column<T, ?>>();
+
private boolean dependsOnSelection;
+
private List<Header<?>> footers = new ArrayList<Header<?>>();
+
private boolean handlesSelection;
+
private List<Header<?>> headers = new ArrayList<Header<?>>();
/**
@@ -430,6 +460,10 @@
private TableRowElement hoveringRow;
+ private int keyboardSelectedColumn = 0;
+
+ private int keyboardSelectedRow = 0;
+
/**
* The presenter.
*/
@@ -504,6 +538,7 @@
this.style.ensureInjected();
setElement(table = Document.get().createTableElement());
+
table.setCellSpacing(0);
colgroup = Document.get().createColGroupElement();
table.appendChild(colgroup);
@@ -530,7 +565,8 @@
setPageSize(pageSize);
// Sink events.
- sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT);
+ sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT |
+ Event.ONKEYUP | Event.ONKEYDOWN);
}
/**
@@ -670,14 +706,28 @@
public void onBrowserEvent(Event event) {
CellBasedWidgetImpl.get().onBrowserEvent(this, event);
super.onBrowserEvent(event);
-
+
+ String eventType = event.getType();
+ boolean keyUp = "keyup".equals(eventType);
+ boolean keyDown = "keydown".equals(eventType);
+
+ // Ignore keydown events unless the cell is in edit mode
+ if (keyDown && !cellIsEditing) {
+ return;
+ }
+ if (keyUp && !cellIsEditing) {
+ if (handleKey(event)) {
+ return;
+ }
+ }
+
// Find the cell where the event occurred.
EventTarget eventTarget = event.getEventTarget();
- TableCellElement cell = null;
+ TableCellElement tableCell = null;
if (eventTarget != null && Element.is(eventTarget)) {
- cell = findNearestParentCell(Element.as(eventTarget));
+ tableCell = findNearestParentCell(Element.as(eventTarget));
}
- if (cell == null) {
+ if (tableCell == null) {
return;
}
@@ -685,7 +735,7 @@
// the table has been refreshed before the current event fired (ex. change
// event refreshes before mouseup fires), so we need to check each parent
// element.
- Element trElem = cell.getParentElement();
+ Element trElem = tableCell.getParentElement();
if (trElem == null) {
return;
}
@@ -697,19 +747,18 @@
TableSectionElement section = TableSectionElement.as(sectionElem);
// Forward the event to the associated header, footer, or column.
- String eventType = event.getType();
- int col = cell.getCellIndex();
+ int col = tableCell.getCellIndex();
if (section == thead) {
Header<?> header = headers.get(col);
if (header != null
&& cellConsumesEventType(header.getCell(), eventType)) {
- header.onBrowserEvent(cell, event);
+ header.onBrowserEvent(tableCell, event);
}
} else if (section == tfoot) {
Header<?> footer = footers.get(col);
if (footer != null
&& cellConsumesEventType(footer.getCell(), eventType)) {
- footer.onBrowserEvent(cell, event);
+ footer.onBrowserEvent(tableCell, event);
}
} else if (section == tbody) {
// Update the hover state.
@@ -724,22 +773,17 @@
hoveringRow = null;
tr.removeClassName(style.hoveredRow());
}
-
+
// Update selection. Selection occurs before firing the event to the cell
// in case the cell operates on the currently selected item.
T value = presenter.getData().get(row);
SelectionModel<? super T> selectionModel = presenter.getSelectionModel();
- Column<T, ?> column = columns.get(col);
if (selectionModel != null && "click".equals(eventType)
&& !handlesSelection) {
selectionModel.setSelected(value, true);
}
-
- // Fire the event to the cell.
- if (cellConsumesEventType(column.getCell(), eventType)) {
- column.onBrowserEvent(
- cell, getPageStart() + row, value, event, providesKey);
- }
+
+ fireEventToCell(event, eventType, tableCell, value, col, row);
}
}
@@ -764,6 +808,20 @@
/**
* Remove a column.
*
+ * @param col the column to remove
+ */
+ public void removeColumn(Column<T, ?> col) {
+ int index = columns.indexOf(col);
+ if (index < 0) {
+ throw new IllegalArgumentException(
+ "The specified column is not part of this table.");
+ }
+ removeColumn(index);
+ }
+
+ /**
+ * Remove a column.
+ *
* @param index the column index
*/
public void removeColumn(int index) {
@@ -783,20 +841,6 @@
}
/**
- * Remove a column.
- *
- * @param col the column to remove
- */
- public void removeColumn(Column<T, ?> col) {
- int index = columns.indexOf(col);
- if (index < 0) {
- throw new IllegalArgumentException(
- "The specified column is not part of this table.");
- }
- removeColumn(index);
- }
-
- /**
* Remove a style from the {@link TableColElement} at the specified index.
*
* @param index the column index
@@ -886,7 +930,7 @@
}
/**
- * Render the header of footer.
+ * Render the header or footer.
*
* @param isFooter true if this is the footer table, false if the header table
*/
@@ -966,10 +1010,90 @@
return null;
}
+ @SuppressWarnings("unchecked")
+ private <C> void fireEventToCell(Event event, String eventType,
+ TableCellElement tableCell, T value, int col, int row) {
+ Column<T, C> column = (Column<T, C>) columns.get(col);
+ Cell<C> cell = column.getCell();
+ if (cellConsumesEventType(cell, eventType)) {
+ C cellValue = column.getValue(value);
+ Object key = providesKey == null ? value : providesKey.getKey(value);
+ boolean cellWasEditing = cell.isEditing(tableCell, cellValue, key);
+ column.onBrowserEvent(
+ tableCell, getPageStart() + row, value, event, providesKey);
+ cellIsEditing = cell.isEditing(tableCell, cellValue, key);
+ if (cellWasEditing && !cellIsEditing) {
+ view.resetFocus();
+ }
+ }
+ }
+
private native int getClientHeight(Element element) /*-{
return element.clientHeight;
}-*/;
+ private boolean handleKey(Event event) {
+ int keyCode = event.getKeyCode();
+ int oldColumn = keyboardSelectedColumn;
+ int oldRow = keyboardSelectedRow;
+ int numColumns = columns.size();
+ int numRows = getDataSize();
+ switch (keyCode) {
+ case KeyCodes.KEY_UP:
+ if (keyboardSelectedRow > 0) {
+ --keyboardSelectedRow;
+ }
+ break;
+ case KeyCodes.KEY_DOWN:
+ if (keyboardSelectedRow < numRows - 1) {
+ ++keyboardSelectedRow;
+ }
+ break;
+ case KeyCodes.KEY_LEFT:
+ if (keyboardSelectedColumn == 0 && keyboardSelectedRow > 0) {
+ keyboardSelectedColumn = numColumns - 1;
+ --keyboardSelectedRow;
+ } else if (keyboardSelectedColumn > 0) {
+ --keyboardSelectedColumn;
+ }
+ break;
+ case KeyCodes.KEY_RIGHT:
+ if (keyboardSelectedColumn == numColumns - 1
+ && keyboardSelectedRow < numRows - 1) {
+ keyboardSelectedColumn = 0;
+ ++keyboardSelectedRow;
+ } else if (keyboardSelectedColumn < numColumns - 1) {
+ ++keyboardSelectedColumn;
+ }
+ break;
+ }
+
+ if (keyboardSelectedColumn != oldColumn || keyboardSelectedRow != oldRow) {
+ int pageStart = getPageStart();
+ int pageSize = getPageSize();
+
+ // Remove old selection markers
+ TableRowElement row = getRowElement(oldRow - pageStart);
+ row.removeClassName(style.keyboardSelectedRow());
+ TableCellElement td = row.getCells().getItem(oldColumn);
+ td.removeClassName(style.keyboardSelectedCell());
+ td.removeAttribute("tabIndex");
+
+ // Move page start if needed
+ if (keyboardSelectedRow >= pageStart + pageSize) {
+ setPageStart(keyboardSelectedRow - pageSize + 1);
+ } else if (keyboardSelectedRow < pageStart) {
+ setPageStart(keyboardSelectedRow);
+ }
+
+ // Add new selection markers and re-establish focus
+ view.resetFocus();
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Schedule a redraw for the end of the event loop.
*/
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
index aca6e84..87350a7 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
@@ -222,6 +222,9 @@
loadChildState(values, 0, savedViews);
}
+ public void resetFocus() {
+ }
+
public void setLoadingState(LoadingState state) {
nodeView.updateImage(state == LoadingState.LOADING);
showOrHide(nodeView.emptyMessageElem, state == LoadingState.EMPTY);
diff --git a/user/src/com/google/gwt/user/cellview/client/Column.java b/user/src/com/google/gwt/user/cellview/client/Column.java
index d8ca946..6da6a4c 100644
--- a/user/src/com/google/gwt/user/cellview/client/Column.java
+++ b/user/src/com/google/gwt/user/cellview/client/Column.java
@@ -70,7 +70,6 @@
/**
* Render the object into the cell.
- *
* @param object the object to render
* @param keyProvider the {@link ProvidesKey} for the object
* @param sb the buffer to render into
@@ -85,7 +84,7 @@
}
/**
- * Get the view keu for the object given the {@link ProvidesKey}. If the
+ * Get the view key for the object given the {@link ProvidesKey}. If the
* {@link ProvidesKey} is null, the object is used as the key.
*
* @param object the row object
diff --git a/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java b/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java
index 40e0681..2af1f04 100644
--- a/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java
+++ b/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java
@@ -298,6 +298,11 @@
void replaceChildren(List<T> values, int start, String html);
/**
+ * Re-establish focus on an element within the view if desired.
+ */
+ void resetFocus();
+
+ /**
* Set the current loading state of the data.
*
* @param state the loading state
@@ -495,6 +500,9 @@
view.replaceChildren(
boundedValues, boundedStart - pageStart, sb.toString());
}
+
+ // Allow the view to reestablish focus after being re-rendered
+ view.resetFocus();
// Reset the pageStartChanged boolean.
pageStartChangedSinceRender = false;
diff --git a/user/test/com/google/gwt/cell/client/CheckboxCellTest.java b/user/test/com/google/gwt/cell/client/CheckboxCellTest.java
index 80efad7..073e7ce 100644
--- a/user/test/com/google/gwt/cell/client/CheckboxCellTest.java
+++ b/user/test/com/google/gwt/cell/client/CheckboxCellTest.java
@@ -63,7 +63,7 @@
@Override
protected String[] getConsumedEvents() {
- return new String[]{"change"};
+ return new String[]{"change", "keyup"};
}
@Override
diff --git a/user/test/com/google/gwt/cell/client/EditTextCellTest.java b/user/test/com/google/gwt/cell/client/EditTextCellTest.java
index ea94164..d4619fe 100644
--- a/user/test/com/google/gwt/cell/client/EditTextCellTest.java
+++ b/user/test/com/google/gwt/cell/client/EditTextCellTest.java
@@ -47,7 +47,7 @@
* Cancel and switch to read only mode.
*/
public void testOnBrowserEventCancel() {
- NativeEvent event = Document.get().createKeyDownEvent(
+ NativeEvent event = Document.get().createKeyUpEvent(
false, false, false, false, KeyCodes.KEY_ESCAPE);
ViewData viewData = new ViewData("originalValue");
viewData.setText("newValue");
@@ -63,7 +63,7 @@
* Cancel and switch to read only mode after committing once.
*/
public void testOnBrowserEventCancelSecondEdit() {
- NativeEvent event = Document.get().createKeyDownEvent(
+ NativeEvent event = Document.get().createKeyUpEvent(
false, false, false, false, KeyCodes.KEY_ESCAPE);
ViewData viewData = new ViewData("originalValue");
viewData.setText("newValue");
@@ -87,7 +87,7 @@
* Commit and switch to read only mode.
*/
public void testOnBrowserEventCommit() {
- NativeEvent event = Document.get().createKeyDownEvent(
+ NativeEvent event = Document.get().createKeyUpEvent(
false, false, false, false, KeyCodes.KEY_ENTER);
ViewData viewData = new ViewData("originalValue");
viewData.setText("newValue");
@@ -185,7 +185,7 @@
@Override
protected String[] getConsumedEvents() {
- return new String[]{"click", "keydown", "keyup", "blur"};
+ return new String[]{"click", "keyup", "keydown", "blur"};
}
@Override
diff --git a/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java b/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java
index 7d4d182..79cb95e 100644
--- a/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java
@@ -90,7 +90,6 @@
* @param <T> the data type
*/
private static class MockView<T> implements View<T> {
-
private int childCount;
private boolean dependsOnSelection;
private String lastHtml;
@@ -174,6 +173,9 @@
public void setDependsOnSelection(boolean dependsOnSelection) {
this.dependsOnSelection = dependsOnSelection;
}
+
+ public void resetFocus() {
+ }
public void setLoadingState(LoadingState state) {
this.loadingState = state;