diff options
15 files changed, 564 insertions, 135 deletions
diff --git a/client/src/main/java/com/vaadin/client/connectors/treegrid/TreeGridConnector.java b/client/src/main/java/com/vaadin/client/connectors/treegrid/TreeGridConnector.java index 32b5a63840..d8e91ceaa0 100644 --- a/client/src/main/java/com/vaadin/client/connectors/treegrid/TreeGridConnector.java +++ b/client/src/main/java/com/vaadin/client/connectors/treegrid/TreeGridConnector.java @@ -33,6 +33,8 @@ import com.vaadin.client.widget.treegrid.TreeGrid; import com.vaadin.client.widget.treegrid.events.TreeGridClickEvent; import com.vaadin.client.widgets.Grid; import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.treegrid.FocusParentRpc; +import com.vaadin.shared.ui.treegrid.FocusRpc; import com.vaadin.shared.ui.treegrid.NodeCollapseRpc; import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants; import com.vaadin.shared.ui.treegrid.TreeGridState; @@ -48,6 +50,12 @@ import elemental.json.JsonObject; @Connect(com.vaadin.ui.TreeGrid.class) public class TreeGridConnector extends GridConnector { + public TreeGridConnector() { + registerRpc(FocusRpc.class, (rowIndex, cellIndex) -> { + getWidget().focusCell(rowIndex, cellIndex); + }); + } + private String hierarchyColumnId; private HierarchyRenderer hierarchyRenderer; @@ -143,7 +151,7 @@ public class TreeGridConnector extends GridConnector { GridEventHandler<?> eventHandler) /*-{ var browserEventHandlers = grid.@com.vaadin.client.widgets.Grid::browserEventHandlers; - + // FocusEventHandler is initially 5th in the list of browser event handlers browserEventHandlers.@java.util.List::set(*)(5, eventHandler); }-*/; @@ -229,40 +237,44 @@ public class TreeGridConnector extends GridConnector { return; } - // Navigate within hierarchy with ALT/OPTION + ARROW KEY when - // hierarchy column is selected - if (isHierarchyColumn(event.getCell()) && domEvent.getAltKey() - && (domEvent.getKeyCode() == KeyCodes.KEY_LEFT - || domEvent.getKeyCode() == KeyCodes.KEY_RIGHT)) { + // Navigate within hierarchy with ARROW KEYs + if (domEvent.getKeyCode() == KeyCodes.KEY_LEFT + || domEvent.getKeyCode() == KeyCodes.KEY_RIGHT) { + event.setHandled(true); + EventCellReference<JsonObject> cell = event.getCell(); // Hierarchy metadata - boolean collapsed, leaf; - JsonObject rowData = event.getCell().getRow(); - if (rowData.hasKey( + JsonObject rowData = cell.getRow(); + if (rowData == null) { + // Row data is lost from the cache, i.e. the row is at least outside the visual area, + // let's scroll the row into the view + getWidget().scrollToRow(cell.getRowIndex()); + } else if (rowData.hasKey( TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION)) { - collapsed = isCollapsed(rowData); JsonObject rowDescription = rowData.getObject( TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION); - leaf = rowDescription.getBoolean( + boolean leaf = rowDescription.getBoolean( TreeGridCommunicationConstants.ROW_LEAF); + boolean collapsed = isCollapsed(rowData); switch (domEvent.getKeyCode()) { - case KeyCodes.KEY_RIGHT: - if (!leaf && collapsed) { - setCollapsed(event.getCell().getRowIndex(), - !collapsed); - } - break; - case KeyCodes.KEY_LEFT: - if (!collapsed) { - // collapse node - setCollapsed(event.getCell().getRowIndex(), - !collapsed); - } - break; + case KeyCodes.KEY_RIGHT: + if (collapsed && !leaf) { + setCollapsed(cell.getRowIndex(), false); + } + break; + case KeyCodes.KEY_LEFT: + if (collapsed || leaf) { + // navigate up + int columnIndex = cell.getColumnIndex(); + getRpcProxy(FocusParentRpc.class).focusParent( + cell.getRowIndex(), columnIndex); + } else if (isCollapseAllowed(rowDescription)) { + setCollapsed(cell.getRowIndex(), true); + } + break; } + } - event.setHandled(true); - return; } } } @@ -277,13 +289,14 @@ public class TreeGridConnector extends GridConnector { .getBoolean(TreeGridCommunicationConstants.ROW_COLLAPSED); } - private static int getDepth(JsonObject rowData) { - assert rowData - .hasKey(TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION) : "missing hierarchy data for row " - + rowData.asString(); - return (int) rowData - .getObject( - TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION) - .getNumber(TreeGridCommunicationConstants.ROW_DEPTH); + /** + * Checks if the item can be collapsed + * + * @param row the item row + * @return {@code true} if the item is allowed to be collapsed, {@code false} otherwise. + */ + public static boolean isCollapseAllowed(JsonObject row) { + return row.getBoolean( + TreeGridCommunicationConstants.ROW_COLLAPSE_ALLOWED); } } diff --git a/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java b/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java index 206491f75b..c03c5c1b01 100644 --- a/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java +++ b/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java @@ -29,6 +29,7 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.WidgetUtil; +import com.vaadin.client.connectors.treegrid.TreeGridConnector; import com.vaadin.client.widget.grid.RendererCellReference; import com.vaadin.client.widget.treegrid.HierarchyRendererCellReferenceWrapper; import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants; @@ -72,7 +73,7 @@ public class HierarchyRenderer extends ClickableRenderer<Object, Widget> { JsonObject hierarchyData = getHierarchyData(row); if ((!isCollapsed(hierarchyData) - && !isCollapseAllowed(hierarchyData)) + && !TreeGridConnector.isCollapseAllowed(hierarchyData)) || isLeaf(hierarchyData)) { return; } @@ -107,7 +108,7 @@ public class HierarchyRenderer extends ClickableRenderer<Object, Widget> { leaf = isLeaf(rowDescription); if (!leaf) { collapsed = isCollapsed(rowDescription); - collapseAllowed = isCollapseAllowed(rowDescription); + collapseAllowed = TreeGridConnector.isCollapseAllowed(rowDescription); } } @@ -162,11 +163,6 @@ public class HierarchyRenderer extends ClickableRenderer<Object, Widget> { return leaf; } - private boolean isCollapseAllowed(JsonObject row) { - return row.getBoolean( - TreeGridCommunicationConstants.ROW_COLLAPSE_ALLOWED); - } - private boolean isCollapsed(JsonObject rowDescription) { boolean collapsed; collapsed = rowDescription diff --git a/client/src/main/java/com/vaadin/client/widget/treegrid/TreeGrid.java b/client/src/main/java/com/vaadin/client/widget/treegrid/TreeGrid.java index 911201b594..1e566faef6 100644 --- a/client/src/main/java/com/vaadin/client/widget/treegrid/TreeGrid.java +++ b/client/src/main/java/com/vaadin/client/widget/treegrid/TreeGrid.java @@ -21,11 +21,11 @@ import com.vaadin.client.widgets.Grid; import elemental.json.JsonObject; /** - * + * * @author Vaadin Ltd * @since 8.1 */ -public class TreeGrid extends Grid<JsonObject> { +public class TreeGrid extends Grid<JsonObject> { /** * Method for accessing the private {@link Grid#focusCell(int, int)} method @@ -42,4 +42,9 @@ public class TreeGrid extends Grid<JsonObject> { public native boolean isElementInChildWidget(Element e)/*-{ return this.@com.vaadin.client.widgets.Grid::isElementInChildWidget(*)(e); }-*/; + + @Override + protected String getFocusPrimaryStyleName() { + return super.getStylePrimaryName() + "-rowmode"; + } } diff --git a/client/src/main/java/com/vaadin/client/widgets/Escalator.java b/client/src/main/java/com/vaadin/client/widgets/Escalator.java index 11bfa113cb..7bb078a94a 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Escalator.java +++ b/client/src/main/java/com/vaadin/client/widgets/Escalator.java @@ -195,7 +195,6 @@ abstract class JsniWorkaround { * to Java code. * * @see #createScrollListenerFunction(Escalator) - * @see Escalator#onScroll() * @see Escalator.Scroller#onScroll() */ protected final JavaScriptObject scrollListenerFunction; @@ -205,7 +204,6 @@ abstract class JsniWorkaround { * it on to Java code. * * @see #createMousewheelListenerFunction(Escalator) - * @see Escalator#onScroll() * @see Escalator.Scroller#onScroll() */ protected final JavaScriptObject mousewheelListenerFunction; @@ -253,7 +251,7 @@ abstract class JsniWorkaround { * * @param esc * a reference to the current instance of {@link Escalator} - * @see Escalator#onScroll() + * @see Escalator.Scroller#onScroll() */ protected abstract JavaScriptObject createScrollListenerFunction( Escalator esc); @@ -264,7 +262,7 @@ abstract class JsniWorkaround { * * @param esc * a reference to the current instance of {@link Escalator} - * @see Escalator#onScroll() + * @see Escalator.Scroller#onScroll() */ protected abstract JavaScriptObject createMousewheelListenerFunction( Escalator esc); @@ -1139,10 +1137,10 @@ public class Escalator extends Widget * Usually {@code "th"} or {@code "td"}. * <p> * <em>Note:</em> To actually <em>create</em> such an element, use - * {@link #createCellElement(int, int)} instead. + * {@link #createCellElement(double)} instead. * * @return the tag name for the element to represent cells as - * @see #createCellElement(int, int) + * @see #createCellElement(double) */ protected abstract String getCellElementTagName(); @@ -1207,7 +1205,7 @@ public class Escalator extends Widget * range of logical indices. This may be fewer than {@code numberOfRows} * , even zero, if not all the removed rows are actually visible. * <p> - * The implementation must call {@link #paintRemoveRow(Element, int)} + * The implementation must call {@link #paintRemoveRow(TableRowElement, int)} * for each row that is removed from the DOM. * * @param index @@ -3111,44 +3109,26 @@ public class Escalator extends Widget */ int rowsLeft = getRowCount(); if (rowsLeft < escalatorRowCount) { - int escalatorRowsToRemove = escalatorRowCount - rowsLeft; - for (int i = 0; i < escalatorRowsToRemove; i++) { - final TableRowElement tr = visualRowOrder - .remove(removedVisualInside.getStart()); - - paintRemoveRow(tr, index); + /* + * Remove extra DOM rows and refresh contents. + */ + for (int i = escalatorRowCount - 1; i >= rowsLeft; i--) { + final TableRowElement tr = visualRowOrder.remove(i); + paintRemoveRow(tr, i); removeRowPosition(tr); } - escalatorRowCount -= escalatorRowsToRemove; - /* - * Because we're removing escalator rows, we don't have - * anything to scroll by. Let's make sure the viewport is - * scrolled to top, to render any rows possibly left above. - */ - body.setBodyScrollPosition(tBodyScrollLeft, 0); + // Move rest of the rows to the Escalator's top + Range visualRange = Range.withLength(0, visualRowOrder.size()); + moveAndUpdateEscalatorRows(visualRange, 0, 0); - /* - * We might have removed some rows from the middle, so let's - * make sure we're not left with any holes. Also remember: - * visualIndex == logicalIndex applies now. - */ - final int dirtyRowsStart = removedLogicalInside.getStart(); - double y = getRowTop(dirtyRowsStart); - for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { - final TableRowElement tr = visualRowOrder.get(i); - setRowPosition(tr, 0, y); - y += getDefaultRowHeight(); - y += spacerContainer.getSpacerHeight(i); - } + sortDomElements(); + setTopRowLogicalIndex(0); - // #8825 update data starting from the first moved row - final int start = dirtyRowsStart; - final int end = escalatorRowCount; - for (int i = start; i < end; i++) { - final TableRowElement tr = visualRowOrder.get(i); - refreshRow(tr, i); - } + scroller.recalculateScrollbarsForVirtualViewport(); + + fireRowVisibilityChangeEvent(); + return; } else { @@ -6444,7 +6424,7 @@ public class Escalator extends Widget * @param rows * the number of rows that should be visible in Escalator's body * @throws IllegalArgumentException - * if {@code rows} is ≤ 0, {@link Double#isInifinite(double) + * if {@code rows} is ≤ 0, {@link Double#isInfinite(double) * infinite} or {@link Double#isNaN(double) NaN}. * @see #setHeightMode(HeightMode) */ diff --git a/client/src/main/java/com/vaadin/client/widgets/Grid.java b/client/src/main/java/com/vaadin/client/widgets/Grid.java index eb5e38773b..a3b808e237 100755 --- a/client/src/main/java/com/vaadin/client/widgets/Grid.java +++ b/client/src/main/java/com/vaadin/client/widgets/Grid.java @@ -6198,8 +6198,8 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, rowSelectedStyleName = rowStyle + "-selected"; rowStripeStyleName = rowStyle + "-stripe"; - cellFocusStyleName = getStylePrimaryName() + "-cell-focused"; - rowFocusStyleName = getStylePrimaryName() + "-row-focused"; + cellFocusStyleName = getFocusPrimaryStyleName() + "-cell-focused"; + rowFocusStyleName = getFocusPrimaryStyleName() + "-row-focused"; if (isAttached()) { refreshHeader(); @@ -6208,6 +6208,10 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, } } + protected String getFocusPrimaryStyleName() { + return getStylePrimaryName(); + } + /** * Creates the escalator updater used to update the header rows in this * grid. The updater is invoked when header rows or columns are added or diff --git a/documentation/components/components-treegrid.asciidoc b/documentation/components/components-treegrid.asciidoc index f3c2058f03..98d3d17839 100644 --- a/documentation/components/components-treegrid.asciidoc +++ b/documentation/components/components-treegrid.asciidoc @@ -93,6 +93,7 @@ treeGrid.addColumn(Project::getHoursDone).setCaption("Hours Done"); treeGrid.setHierarchyColumn("name"); ---- +[[components.treegrid.node.collapsing]] == Prevent Node Collapsing [classname]#TreeGrid# supports setting a callback method that can allow or prevent the user from collapsing an expanded node. @@ -105,10 +106,11 @@ Example using a predefined set of persons that can not be collapsed: [source, java] ---- Set<Person> alwaysExpanded; -personTreeGrid.setItemCollapseAllowedProvider(person -> +personTreeGrid.setItemCollapseAllowedProvider(person -> !alwaysExpanded.contains(person)); ---- +[[components.treegrid.events]] == Listening to Events In addition to supporting all the listeners of the standard [classname]#Grid#, [classname]#TreeGrid# supports listening to the expansion and collapsing of items in its hierarchy. @@ -122,3 +124,10 @@ treeGrid.addCollapseListener(event -> log("Item collapsed: " + event.getCollapse The return types of the methods `getExpandedItem` and `getCollapsedItem` are the same as the type of the [classname]#TreeGrid# the events originated from. Note that collapse listeners will not be triggered for any expanded subtrees of the collapsed item. + +[[components.treegrid.keyboard]] +== Keyboard Navigation and Focus Handling in TreeGrid + +As opposed to [classname]#Grid#, individual cells are not focusable in [classname]#TreeGrid#, and only whole rows +receive focus. The user can navigate through rows with kbd:[Up] and kbd:[Down], collapse rows with kbd:[Left], +and expand them with kbd:[Right]. diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java index b93d0c3952..b986f74ea3 100644 --- a/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java +++ b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java @@ -335,14 +335,16 @@ public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> { } /** - * Collapses given row, removing all its subtrees. + * Collapses given row, removing all its subtrees. Calling this method will + * have no effect if the row is already collapsed. * * @param collapsedRowKey * the key of the row, not {@code null} * @param collapsedRowIndex * the index of row to collapse + * @return {@code true} if the row was collapsed, {@code false} otherwise */ - public void doCollapse(String collapsedRowKey, int collapsedRowIndex) { + public boolean doCollapse(String collapsedRowKey, int collapsedRowIndex) { if (collapsedRowIndex < 0 | collapsedRowIndex >= mapper.getTreeSize()) { throw new IllegalArgumentException("Invalid row index " + collapsedRowIndex + " when tree grid size of " @@ -353,24 +355,30 @@ public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> { Objects.requireNonNull(collapsedItem, "Cannot find item for given key " + collapsedItem); + if (mapper.isCollapsed(collapsedRowKey)) { + return false; + } int collapsedSubTreeSize = mapper.collapse(collapsedRowKey, collapsedRowIndex); - - getClientRpc().removeRows(collapsedRowIndex + 1, collapsedSubTreeSize); + getClientRpc().removeRows(collapsedRowIndex + 1, + collapsedSubTreeSize); // FIXME seems like a slight overkill to do this just for refreshing // expanded status refresh(collapsedItem); + return true; } /** - * Expands the given row. + * Expands the given row. Calling this method will have no effect if the row + * is already expanded. * * @param expandedRowKey * the key of the row, not {@code null} * @param expandedRowIndex * the index of the row to expand + * @return {@code true} if the row was expanded, {@code false} otherwise */ - public void doExpand(String expandedRowKey, final int expandedRowIndex) { + public boolean doExpand(String expandedRowKey, final int expandedRowIndex) { if (expandedRowIndex < 0 | expandedRowIndex >= mapper.getTreeSize()) { throw new IllegalArgumentException("Invalid row index " + expandedRowIndex + " when tree grid size of " @@ -388,16 +396,21 @@ public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> { + " returned no child nodes."); } + if (!mapper.isCollapsed(expandedRowKey)) { + return false; + } mapper.expand(expandedRowKey, expandedRowIndex, expandedNodeSize); - getClientRpc().insertRows(expandedRowIndex + 1, expandedNodeSize); - // TODO optimize by sending "just enough" of the expanded items directly - doPushRows(Range.withLength(expandedRowIndex + 1, expandedNodeSize)); + // TODO optimize by sending "just enough" of the expanded items + // directly + doPushRows( + Range.withLength(expandedRowIndex + 1, expandedNodeSize)); // expanded node needs to be updated to be marked as expanded // FIXME seems like a slight overkill to do this just for refreshing // expanded status refresh(expandedItem); + return true; } /** @@ -419,4 +432,13 @@ public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> { getActiveDataHandler().getActiveData().forEach(this::refresh); } + /** + * Returns parent index for the row or {@code null} + * + * @param rowIndex the row index + * @return the parent index or {@code null} for top-level items + */ + public Integer getParentIndex(int rowIndex) { + return mapper.getParentIndex(rowIndex); + } } diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java b/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java index 040be6cf2f..6c6f8fdaef 100644 --- a/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java +++ b/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java @@ -442,4 +442,20 @@ class HierarchyMapper implements Serializable { } } + /** + * Returns parent index for the row or {@code null} + * + * @param rowIndex the row index + * @return the parent index or {@code null} for top-level items + */ + public Integer getParentIndex(int rowIndex) { + return nodes.stream() + .filter(treeNode -> treeNode.getParentKey() != null + && treeNode.getStartIndex() <= rowIndex + && treeNode.getEndIndex() >= rowIndex) + .min((a, b) -> Math.min(a.getEndIndex() - a.getStartIndex(), + b.getEndIndex() - b.getStartIndex())) + .map(treeNode -> treeNode.getStartIndex() - 1) + .orElse(null); + } } diff --git a/server/src/main/java/com/vaadin/ui/TreeGrid.java b/server/src/main/java/com/vaadin/ui/TreeGrid.java index 6f961990de..d91a0f9af1 100644 --- a/server/src/main/java/com/vaadin/ui/TreeGrid.java +++ b/server/src/main/java/com/vaadin/ui/TreeGrid.java @@ -37,6 +37,8 @@ import com.vaadin.data.provider.HierarchicalQuery; import com.vaadin.data.provider.InMemoryHierarchicalDataProvider; import com.vaadin.server.SerializablePredicate; import com.vaadin.shared.Registration; +import com.vaadin.shared.ui.treegrid.FocusParentRpc; +import com.vaadin.shared.ui.treegrid.FocusRpc; import com.vaadin.shared.ui.treegrid.NodeCollapseRpc; import com.vaadin.shared.ui.treegrid.TreeGridState; import com.vaadin.ui.declarative.DesignAttributeHandler; @@ -120,7 +122,7 @@ public class TreeGrid<T> extends Grid<T> { * * @param source * the tree grid this event originated from - * @param item + * @param expandedItem * the item that was expanded */ public ExpandEvent(TreeGrid<T> source, T expandedItem) { @@ -156,7 +158,7 @@ public class TreeGrid<T> extends Grid<T> { * * @param source * the tree grid this event originated from - * @param item + * @param collapsedItem * the item that was collapsed */ public CollapseEvent(TreeGrid<T> source, T collapsedItem) { @@ -182,13 +184,24 @@ public class TreeGrid<T> extends Grid<T> { public void setNodeCollapsed(String rowKey, int rowIndex, boolean collapse) { if (collapse) { - getDataCommunicator().doCollapse(rowKey, rowIndex); - fireCollapseEvent( - getDataCommunicator().getKeyMapper().get(rowKey)); + if (getDataCommunicator().doCollapse(rowKey, rowIndex)) { + fireCollapseEvent(getDataCommunicator().getKeyMapper() + .get(rowKey)); + } } else { - getDataCommunicator().doExpand(rowKey, rowIndex); - fireExpandEvent( - getDataCommunicator().getKeyMapper().get(rowKey)); + if (getDataCommunicator().doExpand(rowKey, rowIndex)) { + fireExpandEvent(getDataCommunicator().getKeyMapper() + .get(rowKey)); + } + } + } + }); + registerRpc(new FocusParentRpc() { + @Override + public void focusParent(int rowIndex, int cellIndex) { + Integer parentIndex = getDataCommunicator().getParentIndex(rowIndex); + if (parentIndex != null) { + getRpcProxy(FocusRpc.class).focusCell(parentIndex, cellIndex); } } }); diff --git a/shared/src/main/java/com/vaadin/shared/ui/treegrid/FocusParentRpc.java b/shared/src/main/java/com/vaadin/shared/ui/treegrid/FocusParentRpc.java new file mode 100644 index 0000000000..856b3369db --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/treegrid/FocusParentRpc.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * 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.vaadin.shared.ui.treegrid; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * RPC to handle client originated parent focusing in TreeGrid. + * + * @author Vaadin Ltd + * @since 8.1 + */ +public interface FocusParentRpc extends ServerRpc { + + /** + * Focuses cell in the row parent + * + * @param rowIndex + * the row index + * @param cellIndex + * the cell index + */ + void focusParent(int rowIndex, int cellIndex); +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/treegrid/FocusRpc.java b/shared/src/main/java/com/vaadin/shared/ui/treegrid/FocusRpc.java new file mode 100644 index 0000000000..fa02a0d712 --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/treegrid/FocusRpc.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * 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.vaadin.shared.ui.treegrid; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * RPC to handle focusing in TreeGrid. + * + * @author Vaadin Ltd + * @since 8.1 + */ +@FunctionalInterface +public interface FocusRpc extends ClientRpc { + + /** + * Focuses a cell + * + * @param rowIndex + * the row index + * @param columnIndex + * the cell index + */ + void focusCell(int rowIndex, int columnIndex); +} diff --git a/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss b/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss index 8255d830eb..07c8f71d0d 100644 --- a/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss +++ b/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss @@ -10,6 +10,9 @@ $tg-expander-width: 10px !default; /** Expander button right side padding */ $tg-expander-padding: 10px !default; +$v-tree-grid-row-focused-border: $v-grid-cell-focused-border !default; + + @mixin treegrid { // Expander with and item indentation constants @@ -62,5 +65,29 @@ $tg-expander-padding: 10px !default; .v-tree-grid-cell-content { display: inline-block; } + + .v-grid-rowmode-row-focused { + + &:before { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border: $v-grid-cell-focused-border; + display: none; + pointer-events: none; + } + } + .v-grid:focus .v-grid-rowmode-row-focused:before { + display: block; + } + + .v-grid.v-disabled:focus .v-grid-rowmode-row-focused:before { + // Disabled Grid should not show cell focus outline + display: none; + } + } diff --git a/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridHugeTreeNavigation.java b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridHugeTreeNavigation.java new file mode 100644 index 0000000000..fc550a9570 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridHugeTreeNavigation.java @@ -0,0 +1,59 @@ +package com.vaadin.tests.components.treegrid; + +import com.vaadin.annotations.Theme; +import com.vaadin.annotations.Widgetset; +import com.vaadin.data.HierarchyData; +import com.vaadin.data.provider.InMemoryHierarchicalDataProvider; +import com.vaadin.tests.components.AbstractComponentTest; +import com.vaadin.ui.TreeGrid; + +@Theme("valo") +@Widgetset("com.vaadin.DefaultWidgetSet") +public class TreeGridHugeTreeNavigation extends AbstractComponentTest<TreeGrid> { + + private TreeGrid<String> treeGrid; + private InMemoryHierarchicalDataProvider<String> inMemoryDataProvider; + + @Override + public TreeGrid getComponent() { + return treeGrid; + } + + @Override + protected Class<TreeGrid> getTestClass() { + return TreeGrid.class; + } + + @Override + protected void initializeComponents() { + initializeDataProvider(); + treeGrid = new TreeGrid<>(); + treeGrid.setDataProvider(inMemoryDataProvider); + treeGrid.setSizeFull(); + treeGrid.addColumn(String::toString).setCaption("String") + .setId("string"); + treeGrid.addColumn((i) -> "--").setCaption("Nothing"); + treeGrid.setHierarchyColumn("string"); + treeGrid.setId("testComponent"); + treeGrid.setItemCollapseAllowedProvider(s -> !"Dad 2/1".equals(s)); + addTestComponent(treeGrid); + } + + private void initializeDataProvider() { + HierarchyData<String> data = new HierarchyData<>(); + for (int i = 0; i < 3; i++) { + String granddad = "Granddad " + i; + data.addItem(null, granddad); + for (int j = 0; j < 3; j++) { + String dad = "Dad " + i + "/" + j; + data.addItem(granddad, dad); + for (int k = 0; k < 300; k++) { + String son = "Son " + i + "/" + j + "/" + k; + data.addItem(dad, son); + } + } + } + inMemoryDataProvider = new InMemoryHierarchicalDataProvider<>(data); + } + +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java index 8f8a224049..7fb7ddf3f3 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java @@ -2,8 +2,9 @@ package com.vaadin.tests.components.treegrid; import java.util.Arrays; import java.util.Collection; +import java.util.List; -import org.junit.Assert; +import com.vaadin.testbench.parallel.Browser; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -17,6 +18,10 @@ import com.vaadin.testbench.elements.TreeGridElement; import com.vaadin.tests.tb3.MultiBrowserTest; import com.vaadin.tests.tb3.ParameterizedTB3Runner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + @RunWith(ParameterizedTB3Runner.class) public class TreeGridBasicFeaturesTest extends MultiBrowserTest { @@ -35,6 +40,7 @@ public class TreeGridBasicFeaturesTest extends MultiBrowserTest { @Before public void before() { + setDebug(true); openTestURL("theme=valo"); grid = $(TreeGridElement.class).first(); } @@ -43,79 +49,114 @@ public class TreeGridBasicFeaturesTest extends MultiBrowserTest { @Ignore // currently no implementation exists for toggling from the server // side public void toggle_collapse_server_side() { - Assert.assertEquals(3, grid.getRowCount()); + assertEquals(3, grid.getRowCount()); assertCellTexts(0, 0, new String[] { "0 | 0", "0 | 1", "0 | 2" }); selectMenuPath("Component", "Features", "Toggle expand", "0 | 0"); - Assert.assertEquals(6, grid.getRowCount()); + assertEquals(6, grid.getRowCount()); assertCellTexts(1, 0, new String[] { "1 | 0", "1 | 1", "1 | 2" }); selectMenuPath("Component", "Features", "Toggle expand", "0 | 0"); - Assert.assertEquals(3, grid.getRowCount()); + assertEquals(3, grid.getRowCount()); assertCellTexts(0, 0, new String[] { "0 | 0", "0 | 1", "0 | 2" }); // collapsing a leaf should have no effect selectMenuPath("Component", "Features", "Toggle expand", "1 | 0"); - Assert.assertEquals(3, grid.getRowCount()); + assertEquals(3, grid.getRowCount()); } @Test public void non_leaf_collapse_on_click() { - Assert.assertEquals(3, grid.getRowCount()); + assertEquals(3, grid.getRowCount()); assertCellTexts(0, 0, new String[] { "0 | 0", "0 | 1", "0 | 2" }); // Should expand "0 | 0" grid.getRow(0).getCell(0) .findElement(By.className("v-tree-grid-expander")).click(); - Assert.assertEquals(6, grid.getRowCount()); + assertEquals(6, grid.getRowCount()); assertCellTexts(1, 0, new String[] { "1 | 0", "1 | 1", "1 | 2" }); // Should collapse "0 | 0" grid.getRow(0).getCell(0) .findElement(By.className("v-tree-grid-expander")).click(); - Assert.assertEquals(3, grid.getRowCount()); + assertEquals(3, grid.getRowCount()); assertCellTexts(0, 0, new String[] { "0 | 0", "0 | 1", "0 | 2" }); } @Test - @Ignore // FIXME: remove ignore annotation once #8758 is done public void keyboard_navigation() { grid.getRow(0).getCell(0).click(); - // Should expand "0 | 0" - new Actions(getDriver()).keyDown(Keys.ALT).sendKeys(Keys.RIGHT) - .keyUp(Keys.ALT).perform(); - Assert.assertEquals(6, grid.getRowCount()); + // Should expand "0 | 0" without moving focus + new Actions(getDriver()).sendKeys(Keys.RIGHT).perform(); + assertEquals(6, grid.getRowCount()); + assertCellTexts(1, 0, new String[] { "1 | 0", "1 | 1", "1 | 2" }); + assertTrue(grid.getRow(0).hasClassName("v-grid-rowmode-row-focused")); + assertFalse(grid.getRow(1).hasClassName("v-grid-rowmode-row-focused")); + + // Should navigate 2 times down to "1 | 1" + new Actions(getDriver()).sendKeys(Keys.DOWN, Keys.DOWN).perform(); + assertEquals(6, grid.getRowCount()); assertCellTexts(1, 0, new String[] { "1 | 0", "1 | 1", "1 | 2" }); + assertFalse(grid.getRow(0).hasClassName("v-grid-rowmode-row-focused")); + assertFalse(grid.getRow(1).hasClassName("v-grid-rowmode-row-focused")); + assertTrue(grid.getRow(2).hasClassName("v-grid-rowmode-row-focused")); + + // Should expand "1 | 1" without moving focus + new Actions(getDriver()).sendKeys(Keys.RIGHT).perform(); + assertEquals(9, grid.getRowCount()); + assertCellTexts(2, 0, new String[] { "1 | 1", "2 | 0", "2 | 1", "2 | 2", "1 | 2"}); + assertTrue(grid.getRow(2).hasClassName("v-grid-rowmode-row-focused")); + + // Should collapse "1 | 1" + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertEquals(6, grid.getRowCount()); + assertCellTexts(2, 0, new String[] { "1 | 1", "1 | 2", "0 | 1"}); + assertTrue(grid.getRow(2).hasClassName("v-grid-rowmode-row-focused")); + + + // Should navigate to "0 | 0" + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertEquals(6, grid.getRowCount()); + assertCellTexts(0, 0, new String[] { "0 | 0", "1 | 0", "1 | 1", "1 | 2" , "0 | 1" }); + assertTrue(grid.getRow(0).hasClassName("v-grid-rowmode-row-focused")); // Should collapse "0 | 0" - new Actions(getDriver()).keyDown(Keys.ALT).sendKeys(Keys.LEFT) - .keyUp(Keys.ALT).perform(); - Assert.assertEquals(3, grid.getRowCount()); + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertEquals(3, grid.getRowCount()); assertCellTexts(0, 0, new String[] { "0 | 0", "0 | 1", "0 | 2" }); + assertTrue(grid.getRow(0).hasClassName("v-grid-rowmode-row-focused")); + + // Nothing should happen + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertEquals(3, grid.getRowCount()); + assertCellTexts(0, 0, new String[] { "0 | 0", "0 | 1", "0 | 2" }); + assertTrue(grid.getRow(0).hasClassName("v-grid-rowmode-row-focused")); + + assertNoErrorNotifications(); } @Test public void changing_hierarchy_column() { - Assert.assertTrue(grid.getRow(0).getCell(0) + assertTrue(grid.getRow(0).getCell(0) .isElementPresent(By.className("v-tree-grid-expander"))); - Assert.assertFalse(grid.getRow(0).getCell(1) + assertFalse(grid.getRow(0).getCell(1) .isElementPresent(By.className("v-tree-grid-expander"))); selectMenuPath("Component", "Features", "Set hierarchy column", "depth"); - Assert.assertFalse(grid.getRow(0).getCell(0) + assertFalse(grid.getRow(0).getCell(0) .isElementPresent(By.className("v-tree-grid-expander"))); - Assert.assertTrue(grid.getRow(0).getCell(1) + assertTrue(grid.getRow(0).getCell(1) .isElementPresent(By.className("v-tree-grid-expander"))); selectMenuPath("Component", "Features", "Set hierarchy column", "string"); - Assert.assertTrue(grid.getRow(0).getCell(0) + assertTrue(grid.getRow(0).getCell(0) .isElementPresent(By.className("v-tree-grid-expander"))); - Assert.assertFalse(grid.getRow(0).getCell(1) + assertFalse(grid.getRow(0).getCell(1) .isElementPresent(By.className("v-tree-grid-expander"))); } @@ -130,18 +171,18 @@ public class TreeGridBasicFeaturesTest extends MultiBrowserTest { selectMenuPath("Component", "State", "Expand listener"); selectMenuPath("Component", "State", "Collapse listener"); - Assert.assertFalse(logContainsText("Item expanded: 0 | 0")); - Assert.assertFalse(logContainsText("Item collapsed: 0 | 0")); + assertFalse(logContainsText("Item expanded: 0 | 0")); + assertFalse(logContainsText("Item collapsed: 0 | 0")); grid.expandWithClick(0); - Assert.assertTrue(logContainsText("Item expanded: 0 | 0")); - Assert.assertFalse(logContainsText("Item collapsed: 0 | 0")); + assertTrue(logContainsText("Item expanded: 0 | 0")); + assertFalse(logContainsText("Item collapsed: 0 | 0")); grid.collapseWithClick(0); - Assert.assertTrue(logContainsText("Item expanded: 0 | 0")); - Assert.assertTrue(logContainsText("Item collapsed: 0 | 0")); + assertTrue(logContainsText("Item expanded: 0 | 0")); + assertTrue(logContainsText("Item collapsed: 0 | 0")); selectMenuPath("Component", "State", "Expand listener"); selectMenuPath("Component", "State", "Collapse listener"); @@ -149,15 +190,15 @@ public class TreeGridBasicFeaturesTest extends MultiBrowserTest { grid.expandWithClick(1); grid.collapseWithClick(1); - Assert.assertFalse(logContainsText("Item expanded: 0 | 1")); - Assert.assertFalse(logContainsText("Item collapsed: 0 | 1")); + assertFalse(logContainsText("Item expanded: 0 | 1")); + assertFalse(logContainsText("Item collapsed: 0 | 1")); } private void assertCellTexts(int startRowIndex, int cellIndex, String[] cellTexts) { int index = startRowIndex; for (String cellText : cellTexts) { - Assert.assertEquals(cellText, + assertEquals(cellText, grid.getRow(index).getCell(cellIndex).getText()); index++; } diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridHugeTreeNavigationTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridHugeTreeNavigationTest.java new file mode 100644 index 0000000000..f37118a24b --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridHugeTreeNavigationTest.java @@ -0,0 +1,169 @@ +package com.vaadin.tests.components.treegrid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.TreeGridElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class TreeGridHugeTreeNavigationTest extends MultiBrowserTest { + + private TreeGridElement grid; + + @Before + public void before() { + setDebug(true); + openTestURL(); + grid = $(TreeGridElement.class).first(); + } + + @Test + public void keyboard_navigation() { + grid.getRow(0).getCell(0).click(); + + // Should navigate to "Granddad 1" and expand it + new Actions(getDriver()).sendKeys(Keys.DOWN, Keys.RIGHT).perform(); + assertEquals(6, grid.getRowCount()); + assertCellTexts(0, 0, "Granddad 0", "Granddad 1", + "Dad 1/0", "Dad 1/1", "Dad 1/2", "Granddad 2"); + checkRowFocused(1); + + // Should navigate to and expand "Dad 1/1" + new Actions(getDriver()).sendKeys(Keys.DOWN, Keys.DOWN, Keys.RIGHT) + .perform(); + assertCellTexts(0, 0, + "Granddad 0", "Granddad 1", "Dad 1/0", "Dad 1/1", + "Son 1/1/0", "Son 1/1/1", "Son 1/1/2", "Son 1/1/3"); + checkRowFocused(3); + + // Should navigate 100 items down + Keys downKeyArr[] = new Keys[100]; + for (int i = 0; i < 100; i++) { + downKeyArr[i] = Keys.DOWN; + } + new Actions(getDriver()).sendKeys(downKeyArr).perform(); + + WebElement son1_1_99 = findFocusedRow(); + assertEquals("Son 1/1/99 --", son1_1_99.getText()); + + // Should navigate to "Dad 1/1" back + new Actions(getDriver()) + .sendKeys(Keys.HOME, Keys.DOWN, Keys.DOWN, Keys.DOWN).perform(); + WebElement dad1_1 = findFocusedRow(); + assertEquals("Dad 1/1 --", dad1_1.getText()); + + // Should collapse "Dad 1/1" + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertCellTexts(0, 0, "Granddad 0", "Granddad 1", + "Dad 1/0", "Dad 1/1", "Dad 1/2", "Granddad 2"); + checkRowFocused(3); + + // Should navigate to "Granddad 1" + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertCellTexts(0, 0, "Granddad 0", "Granddad 1", + "Dad 1/0", "Dad 1/1", "Dad 1/2", "Granddad 2"); + checkRowFocused(1); + + // Should collapse "Granddad 1" + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertCellTexts(0, 0, "Granddad 0", "Granddad 1", "Granddad 2"); + checkRowFocused(1); + + // Nothing should happen + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + assertCellTexts(0, 0, "Granddad 0", "Granddad 1", "Granddad 2"); + checkRowFocused(1); + assertNoErrorNotifications(); + } + + @Test + public void no_exception_when_calling_expand_or_collapse_twice() { + + // Currently the collapsed state is updated in a round trip to the + // server, thus it is possible to trigger an expand on the same row + // multiple times through the UI. This should not cause exceptions, but + // rather ignore the redundant calls. + + grid.getRow(0).getCell(0).click(); + new Actions(getDriver()).sendKeys(Keys.RIGHT, Keys.RIGHT).perform(); + assertNoErrorNotifications(); + new Actions(getDriver()).sendKeys(Keys.LEFT, Keys.LEFT).perform(); + assertNoErrorNotifications(); + } + + @Test + public void uncollapsible_item() { + grid.getRow(0).getCell(0).click(); + new Actions(getDriver()).sendKeys(Keys.DOWN, Keys.DOWN, Keys.RIGHT).perform(); + grid.waitForVaadin(); + //expand Dad 2/1 + new Actions(getDriver()).sendKeys(Keys.DOWN, Keys.DOWN, Keys.RIGHT).perform(); + grid.waitForVaadin(); + assertNoErrorNotifications(); + assertCellTexts(5,0,"Son 2/1/0"); + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + grid.waitForVaadin(); + assertNoErrorNotifications(); + assertCellTexts(5,0,"Son 2/1/0"); + } + @Test + public void can_toggle_collapse_on_row_that_is_no_longer_in_cache() { + grid.getRow(0).getCell(0).click(); + + // Expand 2 levels + new Actions(getDriver()).sendKeys(Keys.RIGHT).perform(); + grid.waitForVaadin(); + new Actions(getDriver()).sendKeys(Keys.DOWN, Keys.RIGHT).perform(); + grid.waitForVaadin(); + grid.scrollToRow(200); + grid.waitForVaadin(); + //Jump into view + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + grid.waitForVaadin(); + //Collapse + new Actions(getDriver()).sendKeys(Keys.LEFT).perform(); + grid.waitForVaadin(); + assertEquals(6, grid.getRowCount()); + + // Expand + new Actions(getDriver()).sendKeys(Keys.RIGHT, Keys.UP).perform(); + grid.waitForVaadin(); + grid.scrollToRow(200); + new Actions(getDriver()).sendKeys(Keys.RIGHT).perform(); + grid.waitForVaadin(); + assertEquals(306, grid.getRowCount()); + } + + private WebElement findFocusedRow() { + return grid.findElement(By.className("v-grid-rowmode-row-focused")); + } + + private void checkRowFocused(int index) { + if (index > 0) { + assertFalse(grid.getRow(index - 1) + .hasClassName("v-grid-rowmode-row-focused")); + } + assertTrue( + grid.getRow(index).hasClassName("v-grid-rowmode-row-focused")); + assertFalse(grid.getRow(index + 1) + .hasClassName("v-grid-rowmode-row-focused")); + } + + private void assertCellTexts(int startRowIndex, int cellIndex, + String... cellTexts) { + int index = startRowIndex; + for (String cellText : cellTexts) { + assertEquals(cellText, + grid.getRow(index).getCell(cellIndex).getText()); + index++; + } + } +} |