summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcaalador <mikael.grankvist@gmail.com>2017-02-02 14:01:19 +0200
committerPekka Hyvönen <pekka@vaadin.com>2017-02-02 14:01:19 +0200
commit59157179b6e5099952f827485e16b67a88831ebf (patch)
treebef348f7ffb73103e25f69368a8294e48d16ae19
parentbe2c4684cd43547c2785831a8b0aeda63ccbaebe (diff)
downloadvaadin-framework-59157179b6e5099952f827485e16b67a88831ebf.tar.gz
vaadin-framework-59157179b6e5099952f827485e16b67a88831ebf.zip
Add scrollTo methods to Grid (#8203) (#8410)
* Add scroll methods to serverside grid (#8203) Added scrollToTop(), scrollToEnd() and scrollTo(int row) * Fix scrolling to view of opened details (#8203) Removed dependency for DetailsManagerConnector from GridConnector. GridConnector now handles one off listeners. * Rename detailsRefresh to better show that it's one-off. Add missing copyright header.
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java7
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java91
-rw-r--r--server/src/main/java/com/vaadin/ui/Grid.java55
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/grid/GridClientRpc.java53
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java73
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java169
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java67
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java303
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java151
9 files changed, 969 insertions, 0 deletions
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
index 009de4e99e..a1f9336c40 100644
--- a/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
@@ -20,6 +20,7 @@ import java.util.Map;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.user.client.ui.Widget;
+
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.LayoutManager;
@@ -68,6 +69,9 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
int index = firstRowIndex + i;
detachIfNeeded(index, getDetailsComponentConnectorId(index));
}
+ if (numberOfRows == 1) {
+ getParent().singleDetailsOpened(firstRowIndex);
+ }
// Deferred opening of new ones.
refreshDetails();
}
@@ -205,6 +209,7 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
}
private void refreshDetailsVisibility() {
+ boolean shownDetails = false;
for (int i = 0; i < getWidget().getDataSource().size(); ++i) {
String id = getDetailsComponentConnectorId(i);
@@ -216,7 +221,9 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
indexToDetailConnectorId.put(i, id);
getWidget().setDetailsVisible(i, true);
+ shownDetails = true;
}
refreshing = false;
+ getParent().detailsRefreshed(shownDetails);
}
}
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
index 2e48e3a64a..91ad40c233 100644
--- a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
@@ -18,16 +18,19 @@ package com.vaadin.client.connectors.grid;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
+import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
+
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
@@ -56,10 +59,12 @@ import com.vaadin.client.widgets.Grid.HeaderRow;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.GridClientRpc;
import com.vaadin.shared.ui.grid.GridConstants;
import com.vaadin.shared.ui.grid.GridConstants.Section;
import com.vaadin.shared.ui.grid.GridServerRpc;
import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.ui.grid.SectionState;
import com.vaadin.shared.ui.grid.SectionState.CellState;
import com.vaadin.shared.ui.grid.SectionState.RowState;
@@ -76,6 +81,8 @@ import elemental.json.JsonObject;
public class GridConnector extends AbstractListingConnector
implements HasComponentsConnector, SimpleManagedLayout, DeferredWorker {
+ private Set<Runnable> refreshDetailsCallbacks = new HashSet<>();
+
private class ItemClickHandler
implements BodyClickHandler, BodyDoubleClickHandler {
@@ -139,10 +146,94 @@ public class GridConnector extends AbstractListingConnector
return (Grid<JsonObject>) super.getWidget();
}
+ /**
+ * Method called for a row details refresh. Runs all callbacks if any
+ * details were shown and clears the callbacks.
+ *
+ * @param detailsShown
+ * True if any details were set visible
+ */
+ protected void detailsRefreshed(boolean detailsShown) {
+ if (detailsShown) {
+ refreshDetailsCallbacks.forEach(Runnable::run);
+ }
+ refreshDetailsCallbacks.clear();
+ }
+
+ /**
+ * Method target for when one single details has been updated and we might
+ * need to scroll it into view.
+ *
+ * @param rowIndex
+ * index of updated row
+ */
+ protected void singleDetailsOpened(int rowIndex) {
+ addDetailsRefreshCallback(() -> {
+ if (rowHasDetails(rowIndex)) {
+ getWidget().scrollToRow(rowIndex);
+ }
+ });
+ }
+
+ /**
+ * Add a single use details runnable callback for when we get a call to
+ * {@link #detailsRefreshed(boolean)}.
+ *
+ * @param refreshCallback
+ * Details refreshed callback
+ */
+ private void addDetailsRefreshCallback(Runnable refreshCallback) {
+ refreshDetailsCallbacks.add(refreshCallback);
+ }
+
+ /**
+ * Check if we have details for given row.
+ *
+ * @param rowIndex
+ * @return
+ */
+ private boolean rowHasDetails(int rowIndex) {
+ JsonObject row = getWidget().getDataSource().getRow(rowIndex);
+
+ return row != null && row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
+ && !row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty();
+ }
+
@Override
protected void init() {
super.init();
+ registerRpc(GridClientRpc.class, new GridClientRpc() {
+
+ @Override
+ public void scrollToRow(int row, ScrollDestination destination) {
+ Scheduler.get().scheduleFinally(
+ () -> getWidget().scrollToRow(row, destination));
+ // Add details refresh listener and handle possible detail for
+ // scrolled row.
+ addDetailsRefreshCallback(() -> {
+ if (rowHasDetails(row))
+ getWidget().scrollToRow(row, destination);
+ });
+ }
+
+ @Override
+ public void scrollToStart() {
+ Scheduler.get()
+ .scheduleFinally(() -> getWidget().scrollToStart());
+ }
+
+ @Override
+ public void scrollToEnd() {
+ Scheduler.get()
+ .scheduleFinally(() -> getWidget().scrollToEnd());
+ addDetailsRefreshCallback(() -> {
+ if (rowHasDetails(getWidget().getDataSource().size() - 1))
+ getWidget().scrollToEnd();
+ });
+ }
+ });
+
getWidget().addSortHandler(this::handleSortEvent);
getWidget().setRowStyleGenerator(rowRef -> {
JsonObject json = rowRef.getRow();
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
index bab686ed7f..8e12b4a20b 100644
--- a/server/src/main/java/com/vaadin/ui/Grid.java
+++ b/server/src/main/java/com/vaadin/ui/Grid.java
@@ -78,12 +78,14 @@ import com.vaadin.shared.ui.grid.AbstractGridExtensionState;
import com.vaadin.shared.ui.grid.ColumnResizeMode;
import com.vaadin.shared.ui.grid.ColumnState;
import com.vaadin.shared.ui.grid.DetailsManagerState;
+import com.vaadin.shared.ui.grid.GridClientRpc;
import com.vaadin.shared.ui.grid.GridConstants;
import com.vaadin.shared.ui.grid.GridConstants.Section;
import com.vaadin.shared.ui.grid.GridServerRpc;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.GridStaticCellType;
import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.ui.grid.SectionState;
import com.vaadin.ui.components.grid.ColumnReorderListener;
import com.vaadin.ui.components.grid.ColumnResizeListener;
@@ -3179,6 +3181,59 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
return Collections.unmodifiableList(sortOrder);
}
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param row
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(int row) throws IllegalArgumentException {
+ scrollTo(row, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ * <p>
+ * If the row has visible details, its size will also be taken into account.
+ *
+ * @param row
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row, not
+ * {@code null}
+ * @throws IllegalArgumentException
+ * if the provided row is outside the item range
+ */
+ public void scrollTo(int row, ScrollDestination destination) {
+ Objects.requireNonNull(destination,
+ "ScrollDestination can not be null");
+
+ if (row > getDataProvider().size(new Query())) {
+ throw new IllegalArgumentException("Row outside dataProvider size");
+ }
+
+ getRpcProxy(GridClientRpc.class).scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ getRpcProxy(GridClientRpc.class).scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ getRpcProxy(GridClientRpc.class).scrollToEnd();
+ }
+
@Override
protected GridState getState() {
return getState(true);
diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/main/java/com/vaadin/shared/ui/grid/GridClientRpc.java
new file mode 100644
index 0000000000..b560d6a14e
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/ui/grid/GridClientRpc.java
@@ -0,0 +1,53 @@
+/*
+ * 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.grid;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+/**
+ * Server-to-client RPC interface for the Grid component.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface GridClientRpc extends ClientRpc {
+
+ /**
+ * Command client Grid to scroll to a specific data row and its (optional)
+ * details.
+ *
+ * @param row
+ * zero-based row index. If the row index is below zero or above
+ * the row count of the client-side data source, a client-side
+ * exception will be triggered. Since this exception has no
+ * handling by default, an out-of-bounds value will cause a
+ * client-side crash.
+ * @param destination
+ * desired placement of scrolled-to row. See the documentation
+ * for {@link ScrollDestination} for more information.
+ */
+ public void scrollToRow(int row, ScrollDestination destination);
+
+ /**
+ * Command client Grid to scroll to the first row.
+ */
+ public void scrollToStart();
+
+ /**
+ * Command client Grid to scroll to the last row.
+ */
+ public void scrollToEnd();
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java
new file mode 100644
index 0000000000..74e8f44b1f
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java
@@ -0,0 +1,73 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.util.Person;
+import com.vaadin.tests.util.PersonContainer;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+public class GridDetailsLocation extends AbstractTestUI {
+
+ private TextField numberTextField;
+ private Grid<Person> grid;
+ private List<Person> testData;
+
+ @Override
+ protected void setup(VaadinRequest request) {
+
+ grid = new Grid<>();
+ testData = new ArrayList<>(PersonContainer.createTestData(1000));
+ grid.setItems(testData);
+ grid.addColumn(item -> item.getFirstName()).setCaption("First Name");
+ grid.addColumn(item -> item.getLastName()).setCaption("Last Name");
+
+ grid.setSelectionMode(Grid.SelectionMode.NONE);
+ addComponent(grid);
+
+ final CheckBox checkbox = new CheckBox("Details generator");
+ checkbox.addValueChangeListener(event -> {
+ if (checkbox.getValue()) {
+ grid.setDetailsGenerator(person -> {
+ Label label = new Label(
+ person.getFirstName() + " " + person.getLastName());
+ // currently the decorator row doesn't change its height
+ // when the content height is different.
+ label.setHeight("30px");
+ return label;
+ });
+ } else {
+ grid.setDetailsGenerator(null);
+ }
+ });
+ addComponent(checkbox);
+
+ numberTextField = new TextField("Row");
+ addComponent(numberTextField);
+
+ addComponent(new Button("Toggle and scroll", clickEvent -> {
+ toggle();
+ scrollTo();
+ }));
+ addComponent(new Button("Scroll and toggle", clickEvent -> {
+ scrollTo();
+ toggle();
+ }));
+ }
+
+ private void toggle() {
+ Person itemId = testData.get(Integer.parseInt(numberTextField.getValue()));
+ boolean isVisible = grid.isDetailsVisible(itemId);
+ grid.setDetailsVisible(itemId, !isVisible);
+ }
+
+ private void scrollTo() {
+ grid.scrollTo(Integer.parseInt(numberTextField.getValue()));
+ }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java
new file mode 100644
index 0000000000..3f4391d504
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java
@@ -0,0 +1,169 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.Calendar;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+
+import com.vaadin.annotations.Push;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.VerticalLayout;
+
+@Push
+public class GridFastAsyncUpdate extends AbstractTestUI {
+
+ private final Runnable addRowsTask = new Runnable() {
+ @Override
+ public void run() {
+ System.out.println("Logging...");
+ try {
+ Random random = new Random();
+ while (!Thread.currentThread().isInterrupted()) {
+ Thread.sleep(random.nextInt(100));
+
+ GridFastAsyncUpdate.this.access(() -> {
+ ++counter;
+ Item item = new Item(counter,
+ (Calendar.getInstance().getTimeInMillis()
+ - loggingStart),
+ Level.INFO.toString(), "Message");
+ items.add(item);
+ grid.setItems(items);
+
+ if (grid != null && !scrollLock) {
+ grid.scrollToEnd();
+ }
+ });
+ }
+ } catch (InterruptedException e) {
+ System.out.println("logging thread interrupted");
+ }
+ }
+ };
+
+ private int counter;
+ private List<Item> items = new LinkedList<>();
+
+ private Grid<Item> grid;
+ private long loggingStart;
+ private volatile boolean scrollLock = false;
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ final VerticalLayout layout = new VerticalLayout();
+ layout.setSizeFull();
+ layout.setMargin(true);
+ addComponent(layout);
+
+ HorizontalLayout buttons = new HorizontalLayout();
+ layout.addComponent(buttons);
+
+ final ExecutorService logExecutor = Executors.newSingleThreadExecutor();
+
+ final Button logButton = new Button("Start logging");
+ logButton.addClickListener(clickEvent -> {
+ if ("Start logging".equals(logButton.getCaption())) {
+ loggingStart = Calendar.getInstance().getTimeInMillis();
+ logExecutor.submit(addRowsTask);
+ logButton.setCaption("Stop logging");
+ } else {
+ System.out.println("Stop logging...");
+ try {
+ logExecutor.shutdownNow();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ logButton.setCaption("Start logging");
+ }
+ });
+ buttons.addComponent(logButton);
+
+ final Button scrollButton = new Button("Stop scrolling");
+ scrollButton.addClickListener(clickEvent -> {
+ if (!scrollLock) {
+ System.out.println("Stop scrolling");
+ scrollButton.setCaption("Start scrolling");
+ scrollLock = true;
+ } else {
+ System.out.println("Start scrolling");
+ scrollButton.setCaption("Stop scrolling");
+ scrollLock = false;
+ }
+ });
+ buttons.addComponent(scrollButton);
+
+ grid = new Grid<>();
+ grid.addColumn(Item::getSequenceNumber).setCaption("");
+ grid.addColumn(Item::getMillis).setCaption("");
+ grid.addColumn(Item::getLevel).setCaption("");
+ grid.addColumn(Item::getMessage).setCaption("");
+
+ grid.setWidth("100%");
+ grid.setSelectionMode(Grid.SelectionMode.SINGLE);
+ grid.addSelectionListener(selectionEvent -> {
+ if (!selectionEvent.getAllSelectedItems().isEmpty()) {
+ disableScroll();
+ }
+ });
+
+ layout.addComponent(grid);
+ layout.setExpandRatio(grid, 1.0f);
+ }
+
+ protected void disableScroll() {
+ scrollLock = true;
+ }
+
+ protected class Item {
+ Integer sequenceNumber;
+ Long millis;
+ String level, message;
+
+ public Item(Integer sequanceNumber, Long millis, String level,
+ String message) {
+ this.sequenceNumber = sequanceNumber;
+ this.millis = millis;
+ this.level = level;
+ this.message = message;
+ }
+
+ public Integer getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ public void setSequenceNumber(Integer sequenceNumber) {
+ this.sequenceNumber = sequenceNumber;
+ }
+
+ public Long getMillis() {
+ return millis;
+ }
+
+ public void setMillis(Long millis) {
+ this.millis = millis;
+ }
+
+ public String getLevel() {
+ return level;
+ }
+
+ public void setLevel(String level) {
+ this.level = level;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+ }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java
new file mode 100644
index 0000000000..9f83436a11
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java
@@ -0,0 +1,67 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.data.ValueProvider;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+
+public class GridScrollTo extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ List<String> data = new ArrayList<>();
+ for (int i = 0; i < 200; i++) {
+ data.add("Name " + i);
+ }
+
+ Grid<String> grid = new Grid<>();
+ grid.setItems(data);
+
+ grid.setSelectionMode(Grid.SelectionMode.NONE);
+
+ grid.addColumn(ValueProvider.identity()).setId("Name")
+ .setCaption("Name");
+
+ grid.setDetailsGenerator(item -> {
+ final HorizontalLayout detailsLayout = new HorizontalLayout();
+ detailsLayout.setWidth(100, Unit.PERCENTAGE);
+ detailsLayout.setHeightUndefined();
+
+ final Label lbl1 = new Label(item + " details");
+ detailsLayout.addComponent(lbl1);
+ return detailsLayout;
+ });
+
+ grid.addItemClickListener(event -> {
+ final String itemId = event.getItem();
+ grid.setDetailsVisible(itemId, !grid.isDetailsVisible(itemId));
+ });
+
+ Button scrollToTop = new Button("Scroll to top",
+ clickEvent -> grid.scrollToStart());
+ scrollToTop.setId("top");
+
+ Button scrollToEnd = new Button("Scroll to end",
+ clickEvent -> grid.scrollToEnd());
+ scrollToEnd.setId("end");
+
+ TextField rowField = new TextField("Target row");
+ rowField.setId("row-field");
+
+ Button scrollToRow = new Button("Scroll to row", clickEvent -> grid
+ .scrollTo(Integer.parseInt(rowField.getValue())));
+ scrollToRow.setId("row");
+
+ addComponent(grid);
+
+ addComponent(new HorizontalLayout(scrollToTop, scrollToEnd, rowField,
+ scrollToRow));
+ }
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java
new file mode 100644
index 0000000000..e54fe8b0d3
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java
@@ -0,0 +1,303 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.StaleElementReferenceException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.CheckBoxElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.TextFieldElement;
+import com.vaadin.testbench.parallel.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridDetailsLocationTest extends MultiBrowserTest {
+
+ private static final int detailsDefaultHeight = 51;
+ private static final int detailsDefinedHeight = 33;
+
+ private static class Param {
+ private final int rowIndex;
+ private final boolean useGenerator;
+ private final boolean scrollFirstToBottom;
+
+ public Param(int rowIndex, boolean useGenerator,
+ boolean scrollFirstToBottom) {
+ this.rowIndex = rowIndex;
+ this.useGenerator = useGenerator;
+ this.scrollFirstToBottom = scrollFirstToBottom;
+ }
+
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ public boolean useGenerator() {
+ return useGenerator;
+ }
+
+ public boolean scrollFirstToBottom() {
+ return scrollFirstToBottom;
+ }
+
+ @Override
+ public String toString() {
+ return "Param [rowIndex=" + getRowIndex() + ", useGenerator="
+ + useGenerator() + ", scrollFirstToBottom="
+ + scrollFirstToBottom() + "]";
+ }
+
+ }
+
+ public static Collection<Param> parameters() {
+ List<Param> data = new ArrayList<>();
+
+ int[] params = new int[] { 0, 500, 999 };
+
+ for (int rowIndex : params) {
+
+ data.add(new Param(rowIndex, true, false));
+ data.add(new Param(rowIndex, true, true));
+ }
+
+ return data;
+ }
+
+ @Before
+ public void setUp() {
+ setDebug(true);
+ }
+
+ @Test
+ public void toggleAndScroll() throws Throwable {
+ for (Param param : parameters()) {
+ try {
+ openTestURL();
+ useGenerator(param.useGenerator());
+ scrollToBottom(param.scrollFirstToBottom());
+
+ // the tested method
+ toggleAndScroll(param.getRowIndex());
+
+ verifyLocation(param);
+ } catch (Throwable t) {
+ throw new Throwable("" + param, t);
+ }
+ }
+ }
+
+ @Test
+ public void scrollAndToggle() throws Throwable {
+ for (Param param : parameters()) {
+ try {
+ openTestURL();
+ useGenerator(param.useGenerator());
+ scrollToBottom(param.scrollFirstToBottom());
+
+ // the tested method
+ scrollAndToggle(param.getRowIndex());
+
+ verifyLocation(param);
+
+ } catch (Throwable t) {
+ throw new Throwable("" + param, t);
+ }
+ }
+ }
+
+ @Test
+ public void testDetailsHeightWithGenerator() {
+ openTestURL();
+ useGenerator(true);
+ toggleAndScroll(5);
+
+ verifyDetailsRowHeight(5, detailsDefinedHeight, 0);
+ verifyDetailsDecoratorLocation(5, 0, 0);
+
+ toggleAndScroll(0);
+
+ verifyDetailsRowHeight(0, detailsDefinedHeight, 0);
+ // decorator elements are in DOM in the order they have been added
+ verifyDetailsDecoratorLocation(0, 0, 1);
+
+ verifyDetailsRowHeight(5, detailsDefinedHeight, 1);
+ verifyDetailsDecoratorLocation(5, 1, 0);
+ }
+
+ private void verifyDetailsRowHeight(int rowIndex, int expectedHeight,
+ int visibleIndexOfSpacer) {
+ waitForDetailsVisible();
+ WebElement details = getDetailsElement(visibleIndexOfSpacer);
+ Assert.assertEquals("Wrong details row height", expectedHeight,
+ details.getSize().getHeight());
+ }
+
+ private void verifyDetailsDecoratorLocation(int row,
+ int visibleIndexOfSpacer, int visibleIndexOfDeco) {
+ WebElement detailsElement = getDetailsElement(visibleIndexOfSpacer);
+ WebElement detailsDecoElement = getDetailsDecoElement(
+ visibleIndexOfDeco);
+ GridElement.GridRowElement rowElement = getGrid().getRow(row);
+
+ Assert.assertEquals(
+ "Details deco top position does not match row top pos",
+ rowElement.getLocation().getY(),
+ detailsDecoElement.getLocation().getY());
+ Assert.assertEquals(
+ "Details deco bottom position does not match details bottom pos",
+ detailsElement.getLocation().getY()
+ + detailsElement.getSize().getHeight(),
+ detailsDecoElement.getLocation().getY()
+ + detailsDecoElement.getSize().getHeight());
+ }
+
+ private void verifyLocation(Param param) {
+ Assert.assertFalse("Notification was present",
+ isElementPresent(By.className("v-Notification")));
+
+ TestBenchElement headerRow = getGrid().getHeaderRow(0);
+ final int topBoundary = headerRow.getLocation().getX()
+ + headerRow.getSize().height;
+ final int bottomBoundary = getGrid().getLocation().getX()
+ + getGrid().getSize().getHeight()
+ - getHorizontalScrollbar().getSize().height;
+
+ GridElement.GridRowElement row = getGrid().getRow(param.getRowIndex());
+ final int rowTop = row.getLocation().getX();
+
+ waitForDetailsVisible();
+ WebElement details = getDetailsElement();
+ final int detailsBottom = details.getLocation().getX()
+ + details.getSize().getHeight();
+
+ assertGreaterOrEqual("Row top should be inside grid, gridTop:"
+ + topBoundary + " rowTop" + rowTop, topBoundary, rowTop);
+ assertLessThanOrEqual(
+ "Decorator bottom should be inside grid, gridBottom:"
+ + bottomBoundary + " decoratorBotton:" + detailsBottom,
+ detailsBottom, bottomBoundary);
+
+ verifyDetailsRowHeight(param.getRowIndex(), param.useGenerator()
+ ? detailsDefinedHeight : detailsDefaultHeight, 0);
+ verifyDetailsDecoratorLocation(param.getRowIndex(), 0, 0);
+
+ Assert.assertFalse("Notification was present",
+ isElementPresent(By.className("v-Notification")));
+ }
+
+ private final By locator = By.className("v-grid-spacer");
+
+ private WebElement getDetailsElement() {
+ return getDetailsElement(0);
+ }
+
+ private WebElement getDetailsElement(int index) {
+ return findElements(locator).get(index);
+ }
+
+ private WebElement getDetailsDecoElement(int index) {
+ return findElements(By.className("v-grid-spacer-deco")).get(index);
+ }
+
+ private void waitForDetailsVisible() {
+ waitUntil(new ExpectedCondition<WebElement>() {
+
+ @Override
+ public WebElement apply(WebDriver driver) {
+ try {
+ WebElement detailsElement = getDetailsElement();
+ return detailsElement.isDisplayed()
+ && detailsElement.getSize().getHeight() > 3
+ ? detailsElement : null;
+ } catch (StaleElementReferenceException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "visibility of element located by " + locator;
+ }
+
+ }, 5);
+ waitForElementVisible(By.className("v-grid-spacer"));
+ }
+
+ private void scrollToBottom(boolean scrollFirstToBottom) {
+ if (scrollFirstToBottom) {
+ executeScript("arguments[0].scrollTop = 9999999",
+ getVerticalScrollbar());
+ }
+ }
+
+ private void useGenerator(boolean use) {
+ CheckBoxElement checkBox = $(CheckBoxElement.class).first();
+ boolean isChecked = isCheckedValo(checkBox);
+ if (use != isChecked) {
+ clickValo(checkBox);
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ private boolean isCheckedValo(CheckBoxElement checkBoxElement) {
+ WebElement checkbox = checkBoxElement.findElement(By.tagName("input"));
+ Object value = executeScript("return arguments[0].checked;", checkbox);
+ return (Boolean) value;
+ }
+
+ private void clickValo(CheckBoxElement checkBoxElement) {
+ checkBoxElement.click(5, 5);
+ }
+
+ private void scrollAndToggle(int row) {
+ setRow(row);
+ getScrollAndToggle().click();
+ }
+
+ private void toggleAndScroll(int row) {
+ setRow(row);
+ getToggleAndScroll().click();
+ }
+
+ private ButtonElement getScrollAndToggle() {
+ return $(ButtonElement.class).caption("Scroll and toggle").first();
+ }
+
+ private ButtonElement getToggleAndScroll() {
+ return $(ButtonElement.class).caption("Toggle and scroll").first();
+ }
+
+ private void setRow(int row) {
+ $(TextFieldElement.class).first().clear();
+ $(TextFieldElement.class).first().sendKeys(String.valueOf(row),
+ Keys.ENTER, Keys.TAB);
+ }
+
+ private GridElement getGrid() {
+ return $(GridElement.class).first();
+ }
+
+ private WebElement getVerticalScrollbar() {
+ WebElement scrollBar = getGrid()
+ .findElement(By.className("v-grid-scroller-vertical"));
+ return scrollBar;
+ }
+
+ private WebElement getHorizontalScrollbar() {
+ WebElement scrollBar = getGrid()
+ .findElement(By.className("v-grid-scroller-horizontal"));
+ return scrollBar;
+ }
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java
new file mode 100644
index 0000000000..8bdb246d81
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java
@@ -0,0 +1,151 @@
+package com.vaadin.tests.components.grid;
+
+import java.util.Optional;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.TextFieldElement;
+import com.vaadin.testbench.parallel.TestCategory;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+import com.vaadin.ui.Button;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@TestCategory("grid")
+public class GridScrollToTest extends SingleBrowserTest {
+
+ @Test
+ public void testScrollToEnd() {
+ openTestURL();
+ String finalCellText = "199";
+
+ assertEquals("Found final element even though should be at top of list",
+ 0, cellsContaining(finalCellText));
+
+ $(ButtonElement.class).id("end").click();
+
+ assertEquals("Could not find final element", 1,
+ cellsContaining(finalCellText));
+ }
+
+ @Test
+ public void testScrollToRow() {
+ openTestURL();
+ String row = "50";
+
+ assertEquals("Found row element even though should be at top of list",
+ 0, cellsContaining(row));
+
+ $(TextFieldElement.class).id("row-field").setValue(row);
+
+ $(ButtonElement.class).id("row").click();
+
+ assertEquals("Could not find row element", 1, cellsContaining(row));
+ }
+
+ @Test
+ public void testScrollTop() {
+ openTestURL();
+ String row = "Name 0";
+
+ assertEquals("Couldn't find first element", 1, cellsContaining(row));
+
+ getGrid().getVerticalScroller().scroll(800);
+
+ assertEquals(
+ "Found first element even though we should have scrolled down",
+ 0, cellsContaining(row));
+
+ $(ButtonElement.class).id("top").click();
+
+ assertEquals("Couldn't find first element", 1, cellsContaining(row));
+ }
+
+ @Test
+ public void scrollToLastWithDetailsShowDetails() {
+ openTestURL();
+
+ // Scroll to end
+ $(ButtonElement.class).id("end").click();
+
+ // Open details
+ clickCellContaining("199");
+
+ waitForElementPresent(By.className("v-grid-spacer"));
+ waitForElementPresent(By.className("v-label"));
+ assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream()
+ .filter(element -> element.getText().contains("Name 199 details")).findFirst().get().isDisplayed());
+ // scroll away
+ $(ButtonElement.class).id("top").click();
+
+ assertEquals("Found final element even though should be at top of list",
+ 0, cellsContaining("199"));
+
+ assertFalse("Found final element details even though should be at top of list", getGrid().findElements(By.className("v-label")).stream()
+ .filter(element -> element.getText().contains("Name 199 details")).findFirst().isPresent());
+
+ // Scroll to end
+ $(ButtonElement.class).id("end").click();
+
+ assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream()
+ .filter(element -> element.getText().contains("Name 199 details")).findFirst().get().isDisplayed());
+ }
+
+ @Test
+ public void scrollToRowShowsDetails() {
+ openTestURL();
+
+ // Scroll to 50
+ $(TextFieldElement.class).id("row-field").setValue("50");
+
+ $(ButtonElement.class).id("row").click();
+
+ clickCellContaining("50");
+
+ waitForElementPresent(By.className("v-grid-spacer"));
+ waitForElementPresent(By.className("v-label"));
+ assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream()
+ .filter(element -> element.getText().contains("Name 50 details")).findFirst().get().isDisplayed());
+ // scroll away
+ $(ButtonElement.class).id("top").click();
+
+ assertEquals("Found final element even though should be at top of list",
+ 0, cellsContaining("50"));
+
+ assertFalse("Found final element details even though should be at top of list", getGrid().findElements(By.className("v-label")).stream()
+ .filter(element -> element.getText().contains("Name 50 details")).findFirst().isPresent());
+
+ // Scroll to end
+ $(ButtonElement.class).id("row").click();
+
+ assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream()
+ .filter(element -> element.getText().contains("Name 50 details")).findFirst().get().isDisplayed());
+ }
+
+ private GridElement getGrid() {
+ return $(GridElement.class).first();
+ }
+
+ private long cellsContaining(String text) {
+ return getGrid().findElements(By.className("v-grid-cell")).stream()
+ .filter(element -> element.getText().contains(text)).count();
+ }
+
+ private void clickCellContaining(String text) {
+ Optional<WebElement> first = getGrid()
+ .findElements(By.className("v-grid-cell")).stream()
+ .filter(element -> element.getText().contains(text))
+ .findFirst();
+ if (first.isPresent())
+ first.get().click();
+ else
+ Assert.fail("Cell not present");
+ }
+}