summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksi Hietanen <aleksi@vaadin.com>2017-02-23 09:34:59 +0200
committerPekka Hyvönen <pekka@vaadin.com>2017-02-23 09:34:59 +0200
commit6783bca88d4cf0c8944e84a0fef0a219c0b9a4d0 (patch)
tree4fe5a5752482f73c902beb4b5e05e647a2b6203b
parent813a99cfeff9d9cd70d77bd5d9ae75f5fa7b2ff5 (diff)
downloadvaadin-framework-6783bca88d4cf0c8944e84a0fef0a219c0b9a4d0.tar.gz
vaadin-framework-6783bca88d4cf0c8944e84a0fef0a219c0b9a4d0.zip
Add initial implementation of TreeGrid (#8572)
* Add initial implementation of TreeGrid * Refactor TreeGrid and related classes * Fix potential class cast exception in TreeGrid#getDataProvider * Add smoke tests for TreeGrid * Add communication constants for TreeGrid Use shared constant values for hierarchy data serialization and deserialization * Fix event ordering in TreeGrid, add javadocs, keyboard navigation test * TreeGrid improvements * Add TreeGrid.getDataProvider to StateGetDoesNotMarkDirtyTest exclude list * Merge remote-tracking branch 'github/master' into tree-grid * Remove getEscalator override from TreeGrid
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/treegrid/TreeGridConnector.java286
-rw-r--r--client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java223
-rw-r--r--client/src/main/java/com/vaadin/client/widget/treegrid/HierarchyRendererCellReferenceWrapper.java58
-rw-r--r--client/src/main/java/com/vaadin/client/widget/treegrid/TreeGrid.java45
-rw-r--r--client/src/main/java/com/vaadin/client/widget/treegrid/events/TreeGridClickEvent.java82
-rw-r--r--server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java39
-rw-r--r--server/src/main/java/com/vaadin/ui/Grid.java2
-rw-r--r--server/src/main/java/com/vaadin/ui/TreeGrid.java164
-rw-r--r--server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java1
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/treegrid/NodeCollapseRpc.java31
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridCommunicationConstants.java32
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridState.java32
-rw-r--r--testbench-api/src/main/java/com/vaadin/testbench/elements/TreeGridElement.java25
-rw-r--r--themes/src/main/themes/VAADIN/themes/valo/components/_all.scss3
-rw-r--r--themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss61
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeatures.java269
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java109
17 files changed, 1461 insertions, 1 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
new file mode 100644
index 0000000000..7f11683bbe
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/connectors/treegrid/TreeGridConnector.java
@@ -0,0 +1,286 @@
+/*
+ * 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.client.connectors.treegrid;
+
+import java.util.Collection;
+import java.util.logging.Logger;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.Event;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.connectors.grid.GridConnector;
+import com.vaadin.client.renderers.ClickableRenderer;
+import com.vaadin.client.renderers.HierarchyRenderer;
+import com.vaadin.client.widget.grid.EventCellReference;
+import com.vaadin.client.widget.grid.GridEventHandler;
+import com.vaadin.client.widget.grid.events.GridClickEvent;
+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.data.DataCommunicatorConstants;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.treegrid.NodeCollapseRpc;
+import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
+import com.vaadin.shared.ui.treegrid.TreeGridState;
+
+import elemental.json.JsonObject;
+
+/**
+ * A connector class for the TreeGrid component.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+@Connect(com.vaadin.ui.TreeGrid.class)
+public class TreeGridConnector extends GridConnector {
+
+ private String hierarchyColumnId;
+
+ private HierarchyRenderer hierarchyRenderer;
+
+ // Expander click event handling
+ private HandlerRegistration expanderClickHandlerRegistration;
+
+ @Override
+ public TreeGrid getWidget() {
+ return (TreeGrid) super.getWidget();
+ }
+
+ @Override
+ public TreeGridState getState() {
+ return (TreeGridState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ if (stateChangeEvent.hasPropertyChanged("hierarchyColumnId")
+ || stateChangeEvent.hasPropertyChanged("columns")) {
+
+ // Id of old hierarchy column
+ String oldHierarchyColumnId = this.hierarchyColumnId;
+
+ // Id of new hierarchy column. Choose first when nothing explicitly
+ // set
+ String newHierarchyColumnId = getState().hierarchyColumnId;
+ if (newHierarchyColumnId == null) {
+ newHierarchyColumnId = getState().columnOrder.get(0);
+ }
+
+ // Columns
+ Grid.Column<?, ?> newColumn = getColumn(newHierarchyColumnId);
+ Grid.Column<?, ?> oldColumn = getColumn(oldHierarchyColumnId);
+
+ // Unwrap renderer of old column
+ if (oldColumn != null
+ && oldColumn.getRenderer() instanceof HierarchyRenderer) {
+ oldColumn.setRenderer(
+ ((HierarchyRenderer) oldColumn.getRenderer())
+ .getInnerRenderer());
+ }
+
+ // Wrap renderer of new column
+ if (newColumn != null) {
+ HierarchyRenderer wrapperRenderer = getHierarchyRenderer();
+ wrapperRenderer.setInnerRenderer(newColumn.getRenderer());
+ newColumn.setRenderer(wrapperRenderer);
+
+ // Set frozen columns again after setting hierarchy column as
+ // setRenderer() replaces DOM elements
+ getWidget().setFrozenColumnCount(getState().frozenColumnCount);
+
+ this.hierarchyColumnId = newHierarchyColumnId;
+ } else {
+ Logger.getLogger(TreeGridConnector.class.getName()).warning(
+ "Couldn't find column: " + newHierarchyColumnId);
+ }
+ }
+ }
+
+ private HierarchyRenderer getHierarchyRenderer() {
+ if (hierarchyRenderer == null) {
+ hierarchyRenderer = new HierarchyRenderer();
+ }
+ return hierarchyRenderer;
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+
+ expanderClickHandlerRegistration = getHierarchyRenderer()
+ .addClickHandler(
+ new ClickableRenderer.RendererClickHandler<JsonObject>() {
+ @Override
+ public void onClick(
+ ClickableRenderer.RendererClickEvent<JsonObject> event) {
+ toggleCollapse(getRowKey(event.getRow()));
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ });
+
+ // Swap Grid's CellFocusEventHandler to this custom one
+ // The handler is identical to the original one except for the child
+ // widget check
+ replaceCellFocusEventHandler(getWidget(), new CellFocusEventHandler());
+
+ getWidget().addBrowserEventHandler(5, new NavigationEventHandler());
+
+ // Swap Grid#clickEvent field
+ // The event is identical to the original one except for the child
+ // widget check
+ replaceClickEvent(getWidget(),
+ new TreeGridClickEvent(getWidget(), getEventCell(getWidget())));
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+
+ expanderClickHandlerRegistration.removeHandler();
+ }
+
+ private native void replaceCellFocusEventHandler(Grid<?> grid,
+ 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);
+ }-*/;
+
+ private native void replaceClickEvent(Grid<?> grid, GridClickEvent event)/*-{
+ grid.@com.vaadin.client.widgets.Grid::clickEvent = event;
+ }-*/;
+
+ private native EventCellReference<?> getEventCell(Grid<?> grid)/*-{
+ return grid.@com.vaadin.client.widgets.Grid::eventCell;
+ }-*/;
+
+ private boolean isHierarchyColumn(EventCellReference<JsonObject> cell) {
+ return cell.getColumn().getRenderer() instanceof HierarchyRenderer;
+ }
+
+ private void toggleCollapse(String rowKey) {
+ getRpcProxy(NodeCollapseRpc.class).toggleCollapse(rowKey);
+ }
+
+ /**
+ * Class to replace
+ * {@link com.vaadin.client.widgets.Grid.CellFocusEventHandler}. The only
+ * difference is that it handles events originated from widgets in hierarchy
+ * cells.
+ */
+ private class CellFocusEventHandler
+ implements GridEventHandler<JsonObject> {
+ @Override
+ public void onEvent(Grid.GridEvent<JsonObject> event) {
+ Element target = Element.as(event.getDomEvent().getEventTarget());
+ boolean elementInChildWidget = getWidget()
+ .isElementInChildWidget(target);
+
+ // Ignore if event was handled by keyboard navigation handler
+ if (event.isHandled() && !elementInChildWidget) {
+ return;
+ }
+
+ // Ignore target in child widget but handle hierarchy widget
+ if (elementInChildWidget
+ && !HierarchyRenderer.isElementInHierarchyWidget(target)) {
+ return;
+ }
+
+ Collection<String> navigation = getNavigationEvents(getWidget());
+ if (navigation.contains(event.getDomEvent().getType())) {
+ handleNavigationEvent(getWidget(), event);
+ }
+ }
+
+ private native Collection<String> getNavigationEvents(Grid<?> grid)/*-{
+ return grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
+ .@com.vaadin.client.widgets.Grid.CellFocusHandler::getNavigationEvents()();
+ }-*/;
+
+ private native void handleNavigationEvent(Grid<?> grid,
+ Grid.GridEvent<JsonObject> event)/*-{
+ grid.@com.vaadin.client.widgets.Grid::cellFocusHandler
+ .@com.vaadin.client.widgets.Grid.CellFocusHandler::handleNavigationEvent(*)(
+ event.@com.vaadin.client.widgets.Grid.GridEvent::getDomEvent()(),
+ event.@com.vaadin.client.widgets.Grid.GridEvent::getCell()())
+ }-*/;
+ }
+
+ private class NavigationEventHandler
+ implements GridEventHandler<JsonObject> {
+
+ @Override
+ public void onEvent(Grid.GridEvent<JsonObject> event) {
+ if (event.isHandled()) {
+ return;
+ }
+
+ Event domEvent = event.getDomEvent();
+ if (!domEvent.getType().equals(BrowserEvents.KEYDOWN)) {
+ 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)) {
+
+ // Hierarchy metadata
+ boolean collapsed, leaf;
+ if (event.getCell().getRow().hasKey(
+ TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION)) {
+ JsonObject rowDescription = event.getCell().getRow()
+ .getObject(
+ TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION);
+ collapsed = rowDescription.getBoolean(
+ TreeGridCommunicationConstants.ROW_COLLAPSED);
+ leaf = rowDescription.getBoolean(
+ TreeGridCommunicationConstants.ROW_LEAF);
+
+ switch (domEvent.getKeyCode()) {
+ case KeyCodes.KEY_RIGHT:
+ if (!leaf) {
+ if (collapsed) {
+ toggleCollapse(
+ event.getCell().getRow().getString(
+ DataCommunicatorConstants.KEY));
+ }
+ }
+ break;
+ case KeyCodes.KEY_LEFT:
+ if (!collapsed) {
+ // collapse node
+ toggleCollapse(event.getCell().getRow()
+ .getString(DataCommunicatorConstants.KEY));
+ }
+ break;
+ }
+ }
+ event.setHandled(true);
+ return;
+ }
+ }
+ }
+}
diff --git a/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java b/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java
new file mode 100644
index 0000000000..205f7929e4
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/renderers/HierarchyRenderer.java
@@ -0,0 +1,223 @@
+/*
+ * 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.client.renderers;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.HasClickHandlers;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Composite;
+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.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.treegrid.HierarchyRendererCellReferenceWrapper;
+import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
+
+import elemental.json.JsonObject;
+
+/**
+ * A renderer for displaying hierarchical columns in TreeGrid.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+public class HierarchyRenderer extends ClickableRenderer<Object, Widget> {
+
+ private static final String CLASS_TREE_GRID_NODE = "v-tree-grid-node";
+ private static final String CLASS_TREE_GRID_EXPANDER = "v-tree-grid-expander";
+ private static final String CLASS_TREE_GRID_CELL_CONTENT = "v-tree-grid-cell-content";
+ private static final String CLASS_COLLAPSED = "collapsed";
+ private static final String CLASS_EXPANDED = "expanded";
+ private static final String CLASS_DEPTH = "depth-";
+
+ private Renderer innerRenderer;
+
+ @Override
+ public Widget createWidget() {
+ return new HierarchyItem(CLASS_TREE_GRID_NODE);
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Object data, Widget widget) {
+
+ JsonObject row = (JsonObject) cell.getRow();
+
+ int depth = 0;
+ boolean leaf = false;
+ boolean collapsed = false;
+ if (row.hasKey(
+ TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION)) {
+ JsonObject rowDescription = row.getObject(
+ TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION);
+
+ depth = (int) rowDescription
+ .getNumber(TreeGridCommunicationConstants.ROW_DEPTH);
+ leaf = rowDescription
+ .getBoolean(TreeGridCommunicationConstants.ROW_LEAF);
+ if (!leaf) {
+ collapsed = rowDescription.getBoolean(
+ TreeGridCommunicationConstants.ROW_COLLAPSED);
+ }
+ }
+
+ HierarchyItem cellWidget = (HierarchyItem) widget;
+ cellWidget.setDepth(depth);
+
+ if (leaf) {
+ cellWidget.setExpanderState(ExpanderState.LEAF);
+ } else if (collapsed) {
+ cellWidget.setExpanderState(ExpanderState.COLLAPSED);
+ } else {
+ cellWidget.setExpanderState(ExpanderState.EXPANDED);
+ }
+
+ // Render the contents of the inner renderer. For non widget renderers
+ // the cell reference needs to be wrapped so that its getElement method
+ // returns the correct element we want to render.
+ if (innerRenderer instanceof WidgetRenderer) {
+ ((WidgetRenderer) innerRenderer).render(cell, data, ((HierarchyItem) widget).content);
+ } else {
+ innerRenderer.render(new HierarchyRendererCellReferenceWrapper(cell,
+ ((HierarchyItem) widget).content.getElement()), data);
+ }
+ }
+
+ /**
+ * Sets the renderer to be wrapped. This is the original renderer before hierarchy is applied.
+ *
+ * @param innerRenderer
+ * Renderer to be wrapped.
+ */
+ public void setInnerRenderer(Renderer innerRenderer) {
+ this.innerRenderer = innerRenderer;
+ }
+
+ /**
+ * Returns the wrapped renderer.
+ *
+ * @return Wrapped renderer.
+ */
+ public Renderer getInnerRenderer() {
+ return this.innerRenderer;
+ }
+
+ /**
+ * Decides whether the element was rendered by {@link HierarchyRenderer}
+ */
+ public static boolean isElementInHierarchyWidget(Element element) {
+ Widget w = WidgetUtil.findWidget(element, null);
+
+ while (w != null) {
+ if (w instanceof HierarchyItem) {
+ return true;
+ }
+ w = w.getParent();
+ }
+
+ return false;
+ }
+
+ private class HierarchyItem extends Composite {
+
+ private FlowPanel panel;
+ private Expander expander;
+ private Widget content;
+
+ private HierarchyItem(String className) {
+ panel = new FlowPanel();
+ panel.getElement().addClassName(className);
+
+ expander = new Expander();
+ expander.getElement().addClassName(CLASS_TREE_GRID_EXPANDER);
+
+ if (innerRenderer instanceof WidgetRenderer) {
+ content = ((WidgetRenderer) innerRenderer).createWidget();
+ } else {
+ // TODO: 20/09/16 create more general widget?
+ content = GWT.create(HTML.class);
+ }
+
+ content.getElement().addClassName(CLASS_TREE_GRID_CELL_CONTENT);
+
+ panel.add(expander);
+ panel.add(content);
+
+ expander.addClickHandler(HierarchyRenderer.this);
+
+ initWidget(panel);
+ }
+
+ private void setDepth(int depth) {
+ String classNameToBeReplaced = getFullClassName(CLASS_DEPTH, panel.getElement().getClassName());
+ if (classNameToBeReplaced == null) {
+ panel.getElement().addClassName(CLASS_DEPTH + depth);
+ } else {
+ panel.getElement().replaceClassName(classNameToBeReplaced, CLASS_DEPTH + depth);
+ }
+ }
+
+ private String getFullClassName(String prefix, String classNameList) {
+ int start = classNameList.indexOf(prefix);
+ int end = start + prefix.length();
+ if (start > -1) {
+ while (end < classNameList.length() && classNameList.charAt(end) != ' ') {
+ end++;
+ }
+ return classNameList.substring(start, end);
+ }
+ return null;
+ }
+
+ private void setExpanderState(ExpanderState state) {
+ switch (state) {
+ case EXPANDED:
+ expander.getElement().removeClassName(CLASS_COLLAPSED);
+ expander.getElement().addClassName(CLASS_EXPANDED);
+ break;
+ case COLLAPSED:
+ expander.getElement().removeClassName(CLASS_EXPANDED);
+ expander.getElement().addClassName(CLASS_COLLAPSED);
+ break;
+ case LEAF:
+ default:
+ expander.getElement().removeClassName(CLASS_COLLAPSED);
+ expander.getElement().removeClassName(CLASS_EXPANDED);
+ }
+ }
+
+ private class Expander extends Widget implements HasClickHandlers {
+
+ private Expander() {
+ Element span = DOM.createSpan();
+ setElement(span);
+ }
+
+ @Override
+ public HandlerRegistration addClickHandler(ClickHandler handler) {
+ return addDomHandler(handler, ClickEvent.getType());
+ }
+ }
+ }
+
+ enum ExpanderState {
+ EXPANDED, COLLAPSED, LEAF;
+ }
+}
diff --git a/client/src/main/java/com/vaadin/client/widget/treegrid/HierarchyRendererCellReferenceWrapper.java b/client/src/main/java/com/vaadin/client/widget/treegrid/HierarchyRendererCellReferenceWrapper.java
new file mode 100644
index 0000000000..9982ca4757
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/widget/treegrid/HierarchyRendererCellReferenceWrapper.java
@@ -0,0 +1,58 @@
+/*
+ * 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.client.widget.treegrid;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.grid.RowReference;
+
+/**
+ * Wrapper for cell references. Used by HierarchyRenderer to get the correct
+ * inner element to render.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+public class HierarchyRendererCellReferenceWrapper
+ extends RendererCellReference {
+
+ private Element element;
+
+ public HierarchyRendererCellReferenceWrapper(RendererCellReference cell,
+ Element element) {
+ super(getRowReference(cell));
+ set(getFlyweightCell(cell), cell.getColumnIndex(), cell.getColumn());
+
+ this.element = element;
+ }
+
+ @Override
+ public TableCellElement getElement() {
+ return (TableCellElement) element;
+ }
+
+ private native static RowReference<Object> getRowReference(
+ RendererCellReference cell)/*-{
+ return cell.@com.vaadin.client.widget.grid.CellReference::getRowReference();
+ }-*/;
+
+ private native static FlyweightCell getFlyweightCell(
+ RendererCellReference cell)/*-{
+ return cell.@com.vaadin.client.widget.grid.RendererCellReference::cell;
+ }-*/;
+}
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
new file mode 100644
index 0000000000..911201b594
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/widget/treegrid/TreeGrid.java
@@ -0,0 +1,45 @@
+/*
+ * 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.client.widget.treegrid;
+
+import com.google.gwt.dom.client.Element;
+import com.vaadin.client.widgets.Grid;
+
+import elemental.json.JsonObject;
+
+/**
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+public class TreeGrid extends Grid<JsonObject> {
+
+ /**
+ * Method for accessing the private {@link Grid#focusCell(int, int)} method
+ * from this package
+ */
+ public native void focusCell(int rowIndex, int columnIndex)/*-{
+ this.@com.vaadin.client.widgets.Grid::focusCell(II)(rowIndex, columnIndex);
+ }-*/;
+
+ /**
+ * Method for accessing the private
+ * {@link Grid#isElementInChildWidget(Element)} method from this package
+ */
+ public native boolean isElementInChildWidget(Element e)/*-{
+ return this.@com.vaadin.client.widgets.Grid::isElementInChildWidget(*)(e);
+ }-*/;
+}
diff --git a/client/src/main/java/com/vaadin/client/widget/treegrid/events/TreeGridClickEvent.java b/client/src/main/java/com/vaadin/client/widget/treegrid/events/TreeGridClickEvent.java
new file mode 100644
index 0000000000..b598f90446
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/widget/treegrid/events/TreeGridClickEvent.java
@@ -0,0 +1,82 @@
+/*
+ * 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.client.widget.treegrid.events;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.vaadin.client.renderers.HierarchyRenderer;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler;
+import com.vaadin.client.widget.grid.events.GridClickEvent;
+import com.vaadin.client.widget.treegrid.TreeGrid;
+import com.vaadin.shared.ui.grid.GridConstants;
+
+/**
+ * Class to set as value of {@link com.vaadin.client.widgets.Grid#clickEvent}.
+ * <br/>
+ * Differs from {@link GridClickEvent} only in allowing events to originate form
+ * hierarchy widget.
+ *
+ * @since 8.1
+ * @author Vaadin Ltd
+ */
+public class TreeGridClickEvent extends GridClickEvent {
+
+ public TreeGridClickEvent(TreeGrid grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ public TreeGrid getGrid() {
+ return (TreeGrid) super.getGrid();
+ }
+
+ @Override
+ protected void dispatch(
+ AbstractGridMouseEventHandler.GridClickHandler handler) {
+ EventTarget target = getNativeEvent().getEventTarget();
+ if (!Element.is(target)) {
+ // Target is not an element
+ return;
+ }
+
+ // Ignore event if originated from child widget
+ // except when from hierarchy widget
+ Element targetElement = Element.as(target);
+ if (getGrid().isElementInChildWidget(targetElement)
+ && !HierarchyRenderer
+ .isElementInHierarchyWidget(targetElement)) {
+ return;
+ }
+
+ final RowContainer container = getGrid().getEscalator()
+ .findRowContainer(targetElement);
+ if (container == null) {
+ // No container for given element
+ return;
+ }
+
+ GridConstants.Section section = GridConstants.Section.FOOTER;
+ if (container == getGrid().getEscalator().getHeader()) {
+ section = GridConstants.Section.HEADER;
+ } else if (container == getGrid().getEscalator().getBody()) {
+ section = GridConstants.Section.BODY;
+ }
+
+ doDispatch(handler, section);
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java
new file mode 100644
index 0000000000..ec54a3a138
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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.data.provider;
+
+/**
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ *
+ * @param <T>
+ * @param <F>
+ */
+public interface HierarchicalDataProvider<T, F> extends DataProvider<T, F> {
+
+ public int getDepth(T item);
+
+ public boolean isRoot(T item);
+
+ public T getParent(T item);
+
+ public boolean isCollapsed(T item);
+
+ public boolean hasChildren(T item);
+
+ public void setCollapsed(T item, boolean b);
+}
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
index 186be328f4..c3456075c3 100644
--- a/server/src/main/java/com/vaadin/ui/Grid.java
+++ b/server/src/main/java/com/vaadin/ui/Grid.java
@@ -646,7 +646,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
Type type = null;
try {
type = getState(false).getClass()
- .getDeclaredField(diffStateKey).getGenericType();
+ .getField(diffStateKey).getGenericType();
} catch (NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
diff --git a/server/src/main/java/com/vaadin/ui/TreeGrid.java b/server/src/main/java/com/vaadin/ui/TreeGrid.java
new file mode 100644
index 0000000000..f438df4e8f
--- /dev/null
+++ b/server/src/main/java/com/vaadin/ui/TreeGrid.java
@@ -0,0 +1,164 @@
+/*
+ * 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.ui;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import com.vaadin.data.provider.DataProvider;
+import com.vaadin.data.provider.HierarchicalDataProvider;
+import com.vaadin.shared.ui.treegrid.NodeCollapseRpc;
+import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
+import com.vaadin.shared.ui.treegrid.TreeGridState;
+
+import elemental.json.Json;
+import elemental.json.JsonObject;
+
+/**
+ * A grid component for displaying hierarchical tabular data.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ *
+ * @param <T>
+ * the grid bean type
+ */
+public class TreeGrid<T> extends Grid<T> {
+
+ public TreeGrid() {
+ super();
+
+ // Attaches hierarchy data to the row
+ addDataGenerator((item, rowData) -> {
+
+ JsonObject hierarchyData = Json.createObject();
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_DEPTH,
+ getDataProvider().getDepth(item));
+
+ boolean isLeaf = !getDataProvider().hasChildren(item);
+ if (isLeaf) {
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_LEAF,
+ true);
+ } else {
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_COLLAPSED,
+ getDataProvider().isCollapsed(item));
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_LEAF,
+ false);
+ }
+
+ // add hierarchy information to row as metadata
+ rowData.put(
+ TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION,
+ hierarchyData);
+ });
+
+ registerRpc(new NodeCollapseRpc() {
+ @Override
+ public void toggleCollapse(String rowKey) {
+ T item = getDataCommunicator().getKeyMapper().get(rowKey);
+ TreeGrid.this.toggleCollapse(item);
+ }
+ });
+ }
+
+ // TODO: construct a "flat" in memory hierarchical data provider?
+ @Override
+ public void setItems(Collection<T> items) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void setItems(Stream<T> items) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void setItems(T... items) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void setDataProvider(DataProvider<T, ?> dataProvider) {
+ if (!(dataProvider instanceof HierarchicalDataProvider)) {
+ throw new IllegalArgumentException(
+ "TreeGrid only accepts hierarchical data providers");
+ }
+ super.setDataProvider(dataProvider);
+ }
+
+ /**
+ * Set the column that displays the hierarchy of this grid's data. By
+ * default the hierarchy will be displayed in the first column.
+ * <p>
+ * Setting a hierarchy column by calling this method also sets the column to
+ * be visible and not hidable.
+ *
+ * @see Column#setId(String)
+ *
+ * @param id
+ * id of the column to use for displaying hierarchy
+ */
+ public void setHierarchyColumn(String id) {
+ Objects.requireNonNull(id, "id may not be null");
+ if (getColumn(id) == null) {
+ throw new IllegalArgumentException("No column found for given id");
+ }
+ getColumn(id).setHidden(false);
+ getColumn(id).setHidable(false);
+ getState().hierarchyColumnId = getInternalIdForColumn(getColumn(id));
+ }
+
+ @Override
+ protected TreeGridState getState() {
+ return (TreeGridState) super.getState();
+ }
+
+ @Override
+ protected TreeGridState getState(boolean markAsDirty) {
+ return (TreeGridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Toggle the expansion of an item in this grid. If the item is already
+ * expanded, it will be collapsed.
+ * <p>
+ * Toggling expansion on a leaf item in the hierarchy will have no effect.
+ *
+ * @param item
+ * the item to toggle expansion for
+ */
+ public void toggleCollapse(T item) {
+ getDataProvider().setCollapsed(item,
+ !getDataProvider().isCollapsed(item));
+ getDataCommunicator().reset();
+ }
+
+ @Override
+ public HierarchicalDataProvider<T, ?> getDataProvider() {
+ DataProvider<T, ?> dataProvider = super.getDataProvider();
+ // FIXME DataCommunicator by default has a CallbackDataProvider if no
+ // DataProvider is set, resulting in a class cast exception if we don't
+ // check it here.
+
+ // Once fixed, remove this method from the exclude list in
+ // StateGetDoesNotMarkDirtyTest
+ if (!(dataProvider instanceof HierarchicalDataProvider)) {
+ throw new IllegalStateException("No data provider has been set.");
+ }
+ return (HierarchicalDataProvider<T, ?>) dataProvider;
+ }
+}
diff --git a/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java b/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java
index 7a11cf4df6..3b4c2413f9 100644
--- a/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java
+++ b/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java
@@ -31,6 +31,7 @@ public class StateGetDoesNotMarkDirtyTest {
excludedMethods.add("getConnectorId");
excludedMethods.add("getContent");
excludedMethods.add("com.vaadin.ui.Grid:getSelectAllCheckBoxVisible");
+ excludedMethods.add("com.vaadin.ui.TreeGrid:getDataProvider");
}
@Test
diff --git a/shared/src/main/java/com/vaadin/shared/ui/treegrid/NodeCollapseRpc.java b/shared/src/main/java/com/vaadin/shared/ui/treegrid/NodeCollapseRpc.java
new file mode 100644
index 0000000000..719e5ba183
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/ui/treegrid/NodeCollapseRpc.java
@@ -0,0 +1,31 @@
+/*
+ * 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 collapse and expand actions on hierarchical
+ * rows in TreeGrid.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+@FunctionalInterface
+public interface NodeCollapseRpc extends ServerRpc {
+
+ void toggleCollapse(String rowKey);
+}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridCommunicationConstants.java b/shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridCommunicationConstants.java
new file mode 100644
index 0000000000..c3bd0bbd91
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridCommunicationConstants.java
@@ -0,0 +1,32 @@
+/*
+ * 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 java.io.Serializable;
+
+/**
+ * Set of contants used by TreeGrid. These are commonly used JsonObject keys
+ * which are considered to be reserved for internal use.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ */
+public class TreeGridCommunicationConstants implements Serializable {
+ public static final String ROW_HIERARCHY_DESCRIPTION = "rhd";
+ public static final String ROW_DEPTH = "d";
+ public static final String ROW_COLLAPSED = "c";
+ public static final String ROW_LEAF = "l";
+}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridState.java b/shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridState.java
new file mode 100644
index 0000000000..a1008405fc
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/ui/treegrid/TreeGridState.java
@@ -0,0 +1,32 @@
+/*
+ * 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.ui.grid.GridState;
+
+/**
+ * The shared state for the {@link com.vaadin.ui.TreeGrid} component.
+ *
+ * @since 8.1
+ * @author Vaadin Ltd
+ */
+public class TreeGridState extends GridState {
+
+ /**
+ * Contains ID of the hierarchy column set by the developer.
+ */
+ public String hierarchyColumnId;
+}
diff --git a/testbench-api/src/main/java/com/vaadin/testbench/elements/TreeGridElement.java b/testbench-api/src/main/java/com/vaadin/testbench/elements/TreeGridElement.java
new file mode 100644
index 0000000000..71c47bcc6e
--- /dev/null
+++ b/testbench-api/src/main/java/com/vaadin/testbench/elements/TreeGridElement.java
@@ -0,0 +1,25 @@
+/*
+ * 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.testbench.elements;
+
+/**
+ * TestBench Element API for TreeGrid
+ *
+ * @author Vaadin Ltd
+ */
+public class TreeGridElement extends GridElement {
+
+}
diff --git a/themes/src/main/themes/VAADIN/themes/valo/components/_all.scss b/themes/src/main/themes/VAADIN/themes/valo/components/_all.scss
index 52f1d696aa..6211d39a92 100644
--- a/themes/src/main/themes/VAADIN/themes/valo/components/_all.scss
+++ b/themes/src/main/themes/VAADIN/themes/valo/components/_all.scss
@@ -34,6 +34,7 @@
@import "textfield";
@import "textarea";
@import "tree";
+@import "treegrid";
@import "treetable";
@import "twincolselect";
@import "upload";
@@ -144,6 +145,8 @@
@if v-is-included(tree) {
@include valo-tree;
}
+
+ @include treegrid;
@if v-is-included(table) or v-is-included(treetable) {
@include valo-table;
diff --git a/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss b/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss
new file mode 100644
index 0000000000..15c53f8cc9
--- /dev/null
+++ b/themes/src/main/themes/VAADIN/themes/valo/components/_treegrid.scss
@@ -0,0 +1,61 @@
+/** Expander button visual - expanded */
+$tg-expander-char-expanded: '\f0d7' !default;
+
+/** Expander button visual - collapsed */
+$tg-expander-char-collapsed: '\f0da' !default;
+
+/** Expander button width */
+$tg-expander-width: 10px !default;
+
+/** Expander button right side padding */
+$tg-expander-padding: 10px !default;
+
+@mixin treegrid {
+
+ // Expander with and item indentation constants
+ $indent: $tg-expander-width + $tg-expander-padding;
+
+ // Classname for depth styling
+ $class-depth: depth !default;
+
+ .v-tree-grid-expander {
+ display: inline-block;
+ width: $tg-expander-width;
+ padding-right: $tg-expander-padding;
+
+ &::before {
+ display: inline-block;
+ padding-right: 4px;
+ font-family: FontAwesome;
+ }
+
+ // Expander for expanded item
+ &.expanded {
+ &::before {
+ content: $tg-expander-char-expanded;
+ }
+ }
+
+ // Expander for collapsed item
+ &.collapsed {
+ &::before {
+ content: $tg-expander-char-collapsed;
+ }
+ }
+ }
+
+ // Hierarchy depth styling
+ .v-tree-grid-node {
+ @for $i from 0 through 15 {
+ &.#{$class-depth}-#{$i} {
+ padding-left: $indent * $i;
+ }
+ }
+ }
+
+ // Expander and cell content in same line
+ .v-tree-grid-cell-content {
+ display: inline-block;
+ }
+}
+
diff --git a/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeatures.java b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeatures.java
new file mode 100644
index 0000000000..77e1e2eefa
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeatures.java
@@ -0,0 +1,269 @@
+package com.vaadin.tests.components.treegrid;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.vaadin.data.provider.DataProviderListener;
+import com.vaadin.data.provider.HierarchicalDataProvider;
+import com.vaadin.data.provider.Query;
+import com.vaadin.shared.Registration;
+import com.vaadin.tests.components.AbstractComponentTest;
+import com.vaadin.ui.MenuBar.MenuItem;
+import com.vaadin.ui.TreeGrid;
+
+public class TreeGridBasicFeatures extends AbstractComponentTest<TreeGrid> {
+
+ private TreeGrid<TestBean> grid;
+ private TestDataProvider dataProvider = new TestDataProvider();
+
+ @Override
+ public TreeGrid getComponent() {
+ return grid;
+ }
+
+ @Override
+ protected Class<TreeGrid> getTestClass() {
+ return TreeGrid.class;
+ }
+
+ @Override
+ protected void initializeComponents() {
+ grid = new TreeGrid<>();
+ grid.setSizeFull();
+ grid.addColumn(TestBean::getStringValue).setId("First column");
+ grid.addColumn(TestBean::getStringValue).setId("Second column");
+ grid.setHierarchyColumn("First column");
+ grid.setDataProvider(dataProvider);
+
+ grid.setId("testComponent");
+ addTestComponent(grid);
+ }
+
+ @Override
+ protected void createActions() {
+ super.createActions();
+
+ createHierarchyColumnSelect();
+ createToggleCollapseSelect();
+ }
+
+ private void createHierarchyColumnSelect() {
+ LinkedHashMap<String, String> options = new LinkedHashMap<>();
+ grid.getColumns().stream()
+ .forEach(column -> options.put(column.getId(), column.getId()));
+
+ createSelectAction("Set hierarchy column", CATEGORY_FEATURES, options,
+ grid.getColumns().get(0).getId(),
+ (treeGrid, value, data) -> treeGrid.setHierarchyColumn(value));
+ }
+
+ private void createToggleCollapseSelect() {
+ MenuItem menu = createCategory("Toggle expand", CATEGORY_FEATURES);
+ dataProvider.getAllItems().forEach(testBean -> {
+ createClickAction(testBean.getStringValue(), "Toggle expand",
+ (grid, bean, data) -> grid.toggleCollapse(bean), testBean);
+ });
+ }
+
+ private static class TestBean {
+
+ private String stringValue;
+
+ public TestBean(String stringValue) {
+ this.stringValue = stringValue;
+ }
+
+ public String getStringValue() {
+ return stringValue;
+ }
+
+ public void setStringValue(String stringValue) {
+ this.stringValue = stringValue;
+ }
+ }
+
+ private static class TestDataProvider
+ implements HierarchicalDataProvider<TestBean, Void> {
+
+ private static class HierarchyWrapper<T> {
+ private T item;
+ private T parent;
+ private Set<T> children;
+ private boolean collapsed;
+
+ public HierarchyWrapper(T item, T parent, boolean collapsed) {
+ this.item = item;
+ this.parent = parent;
+ this.collapsed = collapsed;
+ children = new LinkedHashSet<>();
+ }
+
+ public T getItem() {
+ return item;
+ }
+
+ public void setItem(T item) {
+ this.item = item;
+ }
+
+ public T getParent() {
+ return parent;
+ }
+
+ public void setParent(T parent) {
+ this.parent = parent;
+ }
+
+ public Set<T> getChildren() {
+ return children;
+ }
+
+ public void setChildren(Set<T> children) {
+ this.children = children;
+ }
+
+ public boolean isCollapsed() {
+ return collapsed;
+ }
+
+ public void setCollapsed(boolean collapsed) {
+ this.collapsed = collapsed;
+ }
+ }
+
+ private Map<TestBean, HierarchyWrapper<TestBean>> itemToWrapperMap;
+ private Map<HierarchyWrapper<TestBean>, TestBean> wrapperToItemMap;
+ private Map<TestBean, HierarchyWrapper<TestBean>> rootNodes;
+
+ public TestDataProvider() {
+ itemToWrapperMap = new LinkedHashMap<>();
+ wrapperToItemMap = new LinkedHashMap<>();
+ rootNodes = new LinkedHashMap<>();
+
+ List<String> strings = Arrays.asList("a", "b", "c");
+
+ strings.stream().forEach(string -> {
+ TestBean rootBean = new TestBean(string);
+
+ HierarchyWrapper<TestBean> wrappedParent = new HierarchyWrapper<>(
+ rootBean, null, true);
+ itemToWrapperMap.put(rootBean, wrappedParent);
+ wrapperToItemMap.put(wrappedParent, rootBean);
+
+ List<TestBean> children = strings.stream().map(string2 -> {
+ TestBean childBean = new TestBean(string + "/" + string2);
+ HierarchyWrapper<TestBean> wrappedChild = new HierarchyWrapper<>(
+ new TestBean(string + "/" + string2), rootBean,
+ true);
+ itemToWrapperMap.put(childBean, wrappedChild);
+ wrapperToItemMap.put(wrappedChild, childBean);
+ return childBean;
+ }).collect(Collectors.toList());
+
+ wrappedParent.setChildren(new LinkedHashSet<>(children));
+
+ rootNodes.put(rootBean, wrappedParent);
+ });
+ }
+
+ @Override
+ public int getDepth(TestBean item) {
+ int depth = 0;
+ while (getItem(item) != null) {
+ item = getItem(item).getParent();
+ depth++;
+ }
+ return depth;
+ }
+
+ @Override
+ public boolean isInMemory() {
+ return true;
+ }
+
+ @Override
+ public void refreshItem(TestBean item) {
+ // NO-OP
+ }
+
+ @Override
+ public void refreshAll() {
+ // NO-OP
+ }
+
+ @Override
+ public Registration addDataProviderListener(
+ DataProviderListener<TestBean> listener) {
+ return () -> {
+ };
+ }
+
+ private List<TestBean> getAllItems() {
+ return new ArrayList<>(itemToWrapperMap.keySet());
+ }
+
+ private List<TestBean> getVisibleItemsRecursive(
+ Collection<HierarchyWrapper<TestBean>> wrappedItems) {
+ List<TestBean> items = new ArrayList<>();
+
+ wrappedItems.forEach(wrappedItem -> {
+ items.add(wrapperToItemMap.get(wrappedItem));
+ if (!wrappedItem.isCollapsed()) {
+ List<HierarchyWrapper<TestBean>> wrappedChildren = wrappedItem
+ .getChildren().stream()
+ .map(childItem -> getItem(childItem))
+ .collect(Collectors.toList());
+ items.addAll(getVisibleItemsRecursive(wrappedChildren));
+ }
+ });
+ return items;
+ }
+
+ @Override
+ public int size(Query<TestBean, Void> query) {
+ return getVisibleItemsRecursive(rootNodes.values()).size();
+ }
+
+ @Override
+ public Stream<TestBean> fetch(Query<TestBean, Void> query) {
+ return getVisibleItemsRecursive(rootNodes.values()).stream();
+ }
+
+ @Override
+ public boolean isRoot(TestBean item) {
+ return getItem(item).getParent() == null;
+ }
+
+ @Override
+ public TestBean getParent(TestBean item) {
+ return getItem(item).getParent();
+ }
+
+ @Override
+ public boolean isCollapsed(TestBean item) {
+ return getItem(item).isCollapsed();
+ }
+
+ @Override
+ public boolean hasChildren(TestBean item) {
+ return !getItem(item).getChildren().isEmpty();
+ }
+
+ @Override
+ public void setCollapsed(TestBean item, boolean b) {
+ getItem(item).setCollapsed(b);
+ }
+
+ private HierarchyWrapper<TestBean> getItem(TestBean item) {
+ return itemToWrapperMap.get(item);
+ }
+ }
+}
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
new file mode 100644
index 0000000000..c84c96232c
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java
@@ -0,0 +1,109 @@
+package com.vaadin.tests.components.treegrid;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+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 TreeGridBasicFeaturesTest extends MultiBrowserTest {
+
+ private TreeGridElement grid;
+
+ @Before
+ public void before() {
+ openTestURL("theme=valo");
+ grid = $(TreeGridElement.class).first();
+ }
+
+ @Test
+ public void toggle_collapse_server_side() {
+ Assert.assertEquals(3, grid.getRowCount());
+ assertCellTexts(0, 0, new String[] { "a", "b", "c" });
+
+ selectMenuPath("Component", "Features", "Toggle expand", "a");
+ Assert.assertEquals(6, grid.getRowCount());
+ assertCellTexts(1, 0, new String[] { "a/a", "a/b", "a/c" });
+
+ selectMenuPath("Component", "Features", "Toggle expand", "a");
+ Assert.assertEquals(3, grid.getRowCount());
+ assertCellTexts(0, 0, new String[] { "a", "b", "c" });
+
+ // collapsing a leaf should have no effect
+ selectMenuPath("Component", "Features", "Toggle expand", "a/a");
+ Assert.assertEquals(3, grid.getRowCount());
+ }
+
+ @Test
+ public void non_leaf_collapse_on_click() {
+ Assert.assertEquals(3, grid.getRowCount());
+ assertCellTexts(0, 0, new String[] { "a", "b", "c" });
+
+ // click the expander corresponding to "a"
+ grid.getRow(0).getCell(0)
+ .findElement(By.className("v-tree-grid-expander")).click();
+ Assert.assertEquals(6, grid.getRowCount());
+ assertCellTexts(1, 0, new String[] { "a/a", "a/b", "a/c" });
+
+ // click the expander corresponding to "a"
+ grid.getRow(0).getCell(0)
+ .findElement(By.className("v-tree-grid-expander")).click();
+ Assert.assertEquals(3, grid.getRowCount());
+ assertCellTexts(0, 0, new String[] { "a", "b", "c" });
+ }
+
+ @Test
+ public void keyboard_navigation() {
+ grid.getRow(0).getCell(0).click();
+
+ // Should expand "a"
+ new Actions(getDriver()).keyDown(Keys.ALT).sendKeys(Keys.RIGHT)
+ .keyUp(Keys.ALT).perform();
+ Assert.assertEquals(6, grid.getRowCount());
+ assertCellTexts(1, 0, new String[] { "a/a", "a/b", "a/c" });
+
+ // Should collapse "a"
+ new Actions(getDriver()).keyDown(Keys.ALT).sendKeys(Keys.LEFT)
+ .keyUp(Keys.ALT).perform();
+ Assert.assertEquals(3, grid.getRowCount());
+ assertCellTexts(0, 0, new String[] { "a", "b", "c" });
+ }
+
+ @Test
+ public void changing_hierarchy_column() {
+ Assert.assertTrue(grid.getRow(0).getCell(0)
+ .isElementPresent(By.className("v-tree-grid-expander")));
+ Assert.assertFalse(grid.getRow(0).getCell(1)
+ .isElementPresent(By.className("v-tree-grid-expander")));
+
+ selectMenuPath("Component", "Features", "Set hierarchy column",
+ "Second column");
+
+ Assert.assertFalse(grid.getRow(0).getCell(0)
+ .isElementPresent(By.className("v-tree-grid-expander")));
+ Assert.assertTrue(grid.getRow(0).getCell(1)
+ .isElementPresent(By.className("v-tree-grid-expander")));
+
+ selectMenuPath("Component", "Features", "Set hierarchy column",
+ "First column");
+
+ Assert.assertTrue(grid.getRow(0).getCell(0)
+ .isElementPresent(By.className("v-tree-grid-expander")));
+ Assert.assertFalse(grid.getRow(0).getCell(1)
+ .isElementPresent(By.className("v-tree-grid-expander")));
+ }
+
+ private void assertCellTexts(int startRowIndex, int cellIndex,
+ String[] cellTexts) {
+ int index = startRowIndex;
+ for (String cellText : cellTexts) {
+ Assert.assertEquals(cellText,
+ grid.getRow(index).getCell(cellIndex).getText());
+ index++;
+ }
+ }
+}