diff options
author | Aleksi Hietanen <aleksi@vaadin.com> | 2017-02-23 09:34:59 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2017-02-23 09:34:59 +0200 |
commit | 6783bca88d4cf0c8944e84a0fef0a219c0b9a4d0 (patch) | |
tree | 4fe5a5752482f73c902beb4b5e05e647a2b6203b | |
parent | 813a99cfeff9d9cd70d77bd5d9ae75f5fa7b2ff5 (diff) | |
download | vaadin-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
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++; + } + } +} |