diff options
author | caalador <mikael.grankvist@gmail.com> | 2017-02-02 14:01:19 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2017-02-02 14:01:19 +0200 |
commit | 59157179b6e5099952f827485e16b67a88831ebf (patch) | |
tree | bef348f7ffb73103e25f69368a8294e48d16ae19 | |
parent | be2c4684cd43547c2785831a8b0aeda63ccbaebe (diff) | |
download | vaadin-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.
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"); + } +} |