* Grid editor: TAB now skips non-editable columns Pressing TAB would shift the focus to non-editable cells when the Grid was in edit mode. This patch makes DefaultEditorEventHandler to skip such columns. Closes #10970 * Add teststags/8.8.1
import com.google.gwt.user.client.ui.Widget; | import com.google.gwt.user.client.ui.Widget; | ||||
import com.vaadin.client.WidgetUtil; | import com.vaadin.client.WidgetUtil; | ||||
import com.vaadin.client.ui.FocusUtil; | import com.vaadin.client.ui.FocusUtil; | ||||
import com.vaadin.client.widgets.Grid; | |||||
import com.vaadin.client.widgets.Grid.Editor; | import com.vaadin.client.widgets.Grid.Editor; | ||||
import com.vaadin.client.widgets.Grid.EditorDomEvent; | import com.vaadin.client.widgets.Grid.EditorDomEvent; | ||||
import java.util.List; | |||||
/** | /** | ||||
* The default handler for Grid editor events. Offers several overridable | * The default handler for Grid editor events. Offers several overridable | ||||
* protected methods for easier customization. | * protected methods for easier customization. | ||||
int columnCount = event.getGrid().getVisibleColumns().size(); | int columnCount = event.getGrid().getVisibleColumns().size(); | ||||
int colIndex = event.getFocusedColumnIndex() + colDelta; | |||||
int colIndex = colDelta > 0 | |||||
? findNextEditableColumnIndex(event.getGrid(), | |||||
event.getFocusedColumnIndex() + colDelta) | |||||
: findPrevEditableColumnIndex(event.getGrid(), | |||||
event.getFocusedColumnIndex() + colDelta); | |||||
int rowIndex = event.getRowIndex(); | int rowIndex = event.getRowIndex(); | ||||
// Handle row change with horizontal move when column goes out | // Handle row change with horizontal move when column goes out | ||||
// of range. | // of range. | ||||
if (rowDelta == 0) { | |||||
if (colIndex >= columnCount | |||||
if (rowDelta == 0 && colIndex < 0) { | |||||
if (colDelta > 0 | |||||
&& rowIndex < event.getGrid().getDataSource().size() | && rowIndex < event.getGrid().getDataSource().size() | ||||
- 1) { | - 1) { | ||||
rowDelta = 1; | rowDelta = 1; | ||||
colIndex = 0; | |||||
} else if (colIndex < 0 && rowIndex > 0) { | |||||
colIndex = findNextEditableColumnIndex(event.getGrid(), | |||||
0); | |||||
} else if (colDelta < 0 && rowIndex > 0) { | |||||
rowDelta = -1; | rowDelta = -1; | ||||
colIndex = columnCount - 1; | |||||
colIndex = findPrevEditableColumnIndex(event.getGrid(), | |||||
columnCount - 1); | |||||
} | } | ||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
/** | |||||
* Finds index of the first editable column, starting at the specified | |||||
* index. | |||||
* | |||||
* @param grid | |||||
* the current grid, not null. | |||||
* @param startingWith | |||||
* start with this column. Index into the | |||||
* {@link Grid#getVisibleColumns()}. | |||||
* @return the index of the nearest visible column; may return the | |||||
* <code>startingWith</code> itself. Returns -1 if there is no such | |||||
* column. | |||||
*/ | |||||
private int findNextEditableColumnIndex(Grid<T> grid, int startingWith) { | |||||
final List<Grid.Column<?, T>> columns = grid.getVisibleColumns(); | |||||
for (int i = startingWith; i < columns.size(); i++) { | |||||
if (columns.get(i).isEditable()) { | |||||
return i; | |||||
} | |||||
} | |||||
return -1; | |||||
} | |||||
/** | |||||
* Finds index of the last editable column, searching backwards starting at | |||||
* the specified index. | |||||
* | |||||
* @param grid | |||||
* the current grid, not null. | |||||
* @param startingWith | |||||
* start with this column. Index into the | |||||
* {@link Grid#getVisibleColumns()}. | |||||
* @return the index of the nearest visible column; may return the | |||||
* <code>startingWith</code> itself. Returns -1 if there is no such | |||||
* column. | |||||
*/ | |||||
private int findPrevEditableColumnIndex(Grid<T> grid, int startingWith) { | |||||
final List<Grid.Column<?, T>> columns = grid.getVisibleColumns(); | |||||
for (int i = startingWith; i >= 0; i--) { | |||||
if (columns.get(i).isEditable()) { | |||||
return i; | |||||
} | |||||
} | |||||
return -1; | |||||
} | |||||
/** | /** | ||||
* Moves the editor to another column if the received event is a move event. | * Moves the editor to another column if the received event is a move event. | ||||
* By default the editor is moved on a keydown event with keycode | * By default the editor is moved on a keydown event with keycode | ||||
// Prevent tab out of Grid Editor | // Prevent tab out of Grid Editor | ||||
event.getDomEvent().preventDefault(); | event.getDomEvent().preventDefault(); | ||||
editRow(event, event.getRowIndex(), event.getFocusedColumnIndex() | |||||
+ (e.getShiftKey() ? -1 : +1)); | |||||
final int newColIndex = e.getShiftKey() | |||||
? findPrevEditableColumnIndex(event.getGrid(), | |||||
event.getFocusedColumnIndex() - 1) | |||||
: findNextEditableColumnIndex(event.getGrid(), | |||||
event.getFocusedColumnIndex() + 1); | |||||
if (newColIndex >= 0) { | |||||
editRow(event, event.getRowIndex(), newColIndex); | |||||
} | |||||
return true; | return true; | ||||
} else if (e.getType().equals(BrowserEvents.KEYDOWN) | } else if (e.getType().equals(BrowserEvents.KEYDOWN) |
package com.vaadin.tests.components.grid; | |||||
import com.vaadin.annotations.Widgetset; | |||||
import com.vaadin.data.provider.ListDataProvider; | |||||
import com.vaadin.server.VaadinRequest; | |||||
import com.vaadin.tests.components.AbstractTestUI; | |||||
import com.vaadin.ui.Button; | |||||
import com.vaadin.ui.Grid; | |||||
import com.vaadin.ui.TextField; | |||||
import java.util.ArrayList; | |||||
import java.util.List; | |||||
/** | |||||
* Shows a Grid with the only editable columns being the column 1 and 3. That | |||||
* will allow us to test that Tab/Shift+Tab skips cells that are not editable. | |||||
*/ | |||||
@Widgetset("com.vaadin.DefaultWidgetSet") | |||||
public class GridEditorTabSkipsNonEditableCells extends AbstractTestUI { | |||||
@Override | |||||
protected void setup(VaadinRequest request) { | |||||
final List<TestBean> items = new ArrayList<>(); | |||||
Grid<TestBean> grid = new Grid<TestBean>(TestBean.class); | |||||
for (int i = 0; i < 10; i++) { | |||||
items.add(new TestBean(i)); | |||||
} | |||||
grid.setDataProvider(new ListDataProvider<>(items)); | |||||
grid.setWidth("100%"); | |||||
grid.setHeight("400px"); | |||||
grid.getEditor().setEnabled(true); | |||||
grid.getColumn("col1").setEditorComponent(new TextField()); | |||||
grid.getColumn("col3").setEditorComponent(new TextField()); | |||||
grid.setColumnOrder("col0", "col1", "col2", "col3", "col4"); | |||||
getLayout().addComponent( | |||||
new Button("Set Editor Buffered Mode On", event -> { | |||||
grid.getEditor().cancel(); | |||||
grid.getEditor().setBuffered(true); | |||||
})); | |||||
getLayout().addComponent( | |||||
new Button("Set Editor Buffered Mode Off", event -> { | |||||
grid.getEditor().cancel(); | |||||
grid.getEditor().setBuffered(false); | |||||
})); | |||||
getLayout().addComponent(grid); | |||||
} | |||||
@Override | |||||
protected Integer getTicketNumber() { | |||||
return 11573; | |||||
} | |||||
@Override | |||||
protected String getTestDescription() { | |||||
return "Pressing TAB doesn't shift the focus to non-editable cells when the Grid is in edit mode."; | |||||
} | |||||
public class TestBean { | |||||
private final int row; | |||||
public TestBean(int row) { | |||||
this.row = row; | |||||
} | |||||
public String getCol0() { | |||||
return "col0_" + row; | |||||
} | |||||
public String getCol1() { | |||||
return "col1_" + row; | |||||
} | |||||
public String getCol2() { | |||||
return "col2_" + row; | |||||
} | |||||
public String getCol3() { | |||||
return "col3_" + row; | |||||
} | |||||
public String getCol4() { | |||||
return "col4_" + row; | |||||
} | |||||
public void setCol0(String value) { | |||||
} | |||||
public void setCol1(String value) { | |||||
} | |||||
public void setCol2(String value) { | |||||
} | |||||
public void setCol3(String value) { | |||||
} | |||||
public void setCol4(String value) { | |||||
} | |||||
} | |||||
} |
package com.vaadin.tests.components.grid; | |||||
import com.vaadin.testbench.elements.ButtonElement; | |||||
import com.vaadin.testbench.elements.GridElement; | |||||
import com.vaadin.testbench.elements.TextFieldElement; | |||||
import com.vaadin.testbench.parallel.TestCategory; | |||||
import com.vaadin.tests.tb3.MultiBrowserTest; | |||||
import org.junit.Assert; | |||||
import org.junit.Test; | |||||
import org.openqa.selenium.Keys; | |||||
import org.openqa.selenium.WebElement; | |||||
import org.openqa.selenium.interactions.Actions; | |||||
import static org.junit.Assert.fail; | |||||
/** | |||||
* Makes sure that pressing Tab when the Grid is in edit mode will make focus | |||||
* skip cells that are not editable. | |||||
*/ | |||||
@TestCategory("grid") | |||||
public class GridEditorTabSkipsNonEditableCellsTest extends MultiBrowserTest { | |||||
/** | |||||
* The grid with 5 columns. First, third and fifth columns are not editable. | |||||
*/ | |||||
private GridElement grid; | |||||
@Override | |||||
public void setup() throws Exception { | |||||
super.setup(); | |||||
openTestURL(); | |||||
grid = $(GridElement.class).first(); | |||||
} | |||||
@Test | |||||
public void tabSkipsOverNotEditableFieldBuffered() { | |||||
setBuffered(true); | |||||
openEditor(0, 1); | |||||
pressTab(); | |||||
Assert.assertEquals("col3_0", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void tabDoesNothingIfAlreadyOnLastEditableFieldBuffered() { | |||||
setBuffered(true); | |||||
openEditor(0, 3); | |||||
pressTab(); | |||||
Assert.assertEquals("col3_0", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void tabSkipsOverNotEditableFieldUnbuffered() { | |||||
setBuffered(false); | |||||
openEditor(0, 1); | |||||
pressTab(); | |||||
Assert.assertEquals("col3_0", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void tabMovesToNextRowFirstEditableFieldUnbuffered() { | |||||
setBuffered(false); | |||||
openEditor(0, 3); | |||||
pressTab(); | |||||
Assert.assertEquals("col1_1", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void shiftTabSkipsOverNotEditableFieldBuffered() { | |||||
setBuffered(true); | |||||
openEditor(0, 3); | |||||
pressShiftTab(); | |||||
Assert.assertEquals("col1_0", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void shiftTabDoesNothingIfAlreadyOnLastEditableFieldBuffered() { | |||||
setBuffered(true); | |||||
openEditor(0, 1); | |||||
pressShiftTab(); | |||||
Assert.assertEquals("col1_0", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void shiftTabSkipsOverNotEditableFieldUnbuffered() { | |||||
setBuffered(false); | |||||
openEditor(0, 3); | |||||
pressShiftTab(); | |||||
Assert.assertEquals("col1_0", getFocusedEditorCellContents()); | |||||
} | |||||
@Test | |||||
public void shiftTabMovesToNextRowFirstEditableFieldUnbuffered() { | |||||
setBuffered(false); | |||||
openEditor(1, 1); | |||||
pressShiftTab(); | |||||
Assert.assertEquals("col3_0", getFocusedEditorCellContents()); | |||||
} | |||||
private void openEditor(int rowIndex, int colIndex) { | |||||
grid.getCell(rowIndex, colIndex).doubleClick(); | |||||
} | |||||
private void pressTab() { | |||||
new Actions(getDriver()).sendKeys(Keys.TAB).perform(); | |||||
} | |||||
private void pressShiftTab() { | |||||
new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB) | |||||
.keyUp(Keys.SHIFT).perform(); | |||||
} | |||||
private void setBuffered(boolean buffered) { | |||||
$(ButtonElement.class).caption(buffered ? "Set Editor Buffered Mode On" | |||||
: "Set Editor Buffered Mode Off").first().click(); | |||||
} | |||||
private String getFocusedEditorCellContents() { | |||||
final GridElement.GridEditorElement editor = grid.getEditor(); | |||||
final WebElement focusedElement = getFocusedElement(); | |||||
for (int i = 0; i < 5; i++) { | |||||
if (editor.isEditable(i) | |||||
&& editor.getField(i).equals(focusedElement)) { | |||||
return (editor.getField(i).wrap(TextFieldElement.class)) | |||||
.getValue(); | |||||
} | |||||
} | |||||
fail("Currently focused element is not a cell editor: " | |||||
+ focusedElement); | |||||
return null; | |||||
} | |||||
} |