Updated row and spacer handling for Escalator. Main changes: - Spacers are only maintained and checked for rows that have DOM representation, and not at all if there is no details generator. This gives notable performance improvements to some particularly large Grids - Escalator no longer tries to trim away any rows that don't fit within the viewport just because a details row gets opened in Grid. This leads to some increase in simultaneous DOM elements, but simplifies the logic considerably. For example opening or closing details rows doesn't require checking the overall content validity beyond the details row itself anymore, but some repositioning at most. There are also no longer any orphaned spacers without corresponding DOM rows. - Spacers are better integrated into the overall position calculations. - Some public methods that are no longer used by Escalator or have changed functionality or order of operations. Any extending classes that tap into row, spacer, or scroll position handling are likely to need reworking after this update. - Auto-detecting row height is delayed until Escalator is both attached and displayed.tags/8.10.0.alpha1
@@ -29,6 +29,7 @@ import com.vaadin.client.ConnectorMap; | |||
import com.vaadin.client.LayoutManager; | |||
import com.vaadin.client.ServerConnector; | |||
import com.vaadin.client.WidgetUtil; | |||
import com.vaadin.client.connectors.data.DataCommunicatorConnector; | |||
import com.vaadin.client.data.DataChangeHandler; | |||
import com.vaadin.client.extensions.AbstractExtensionConnector; | |||
import com.vaadin.client.ui.layout.ElementResizeListener; | |||
@@ -36,7 +37,9 @@ import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent; | |||
import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler; | |||
import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator; | |||
import com.vaadin.client.widgets.Grid; | |||
import com.vaadin.shared.Range; | |||
import com.vaadin.shared.Registration; | |||
import com.vaadin.shared.data.DataCommunicatorClientRpc; | |||
import com.vaadin.shared.ui.Connect; | |||
import com.vaadin.shared.ui.grid.DetailsManagerState; | |||
import com.vaadin.shared.ui.grid.GridState; | |||
@@ -55,17 +58,12 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
/* Map for tracking which details are open on which row */ | |||
private TreeMap<Integer, String> indexToDetailConnectorId = new TreeMap<>(); | |||
/* Boolean flag to avoid multiple refreshes */ | |||
private boolean refreshing; | |||
/* For listening data changes that originate from DataSource. */ | |||
private Registration dataChangeRegistration; | |||
/* For listening spacer index changes that originate from Escalator. */ | |||
private HandlerRegistration spacerIndexChangedHandlerRegistration; | |||
/** | |||
* Handle for the spacer visibility change handler. | |||
*/ | |||
private HandlerRegistration spacerVisibilityChangeRegistration; | |||
/* For listening when Escalator's visual range is changed. */ | |||
private HandlerRegistration rowVisibilityChangeHandlerRegistration; | |||
private final Map<Element, ScheduledCommand> elementToResizeCommand = new HashMap<Element, Scheduler.ScheduledCommand>(); | |||
private final ElementResizeListener detailsRowResizeListener = event -> { | |||
@@ -75,53 +73,199 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
} | |||
}; | |||
/* for delayed alert if Grid needs to run or cancel pending operations */ | |||
private boolean delayedDetailsAddedOrUpdatedAlertTriggered = false; | |||
private boolean delayedDetailsAddedOrUpdated = false; | |||
/* for delayed re-positioning of Escalator contents to prevent gaps */ | |||
/* -1 is a possible spacer index in Escalator so can't be used as default */ | |||
private boolean delayedRepositioningTriggered = false; | |||
private Integer delayedRepositioningStart = null; | |||
private Integer delayedRepositioningEnd = null; | |||
/* calculated when the first details row is opened */ | |||
private Double spacerCellBorderHeights = null; | |||
private Range availableRowRange = Range.emptyRange(); | |||
private Range latestVisibleRowRange = Range.emptyRange(); | |||
/** | |||
* DataChangeHandler for updating the visibility of detail widgets. | |||
*/ | |||
private final class DetailsChangeHandler implements DataChangeHandler { | |||
@Override | |||
public void resetDataAndSize(int estimatedNewDataSize) { | |||
// Full clean up | |||
indexToDetailConnectorId.clear(); | |||
// No need to do anything, dataUpdated and dataAvailable take care | |||
// of cleanup. | |||
} | |||
@Override | |||
public void dataUpdated(int firstRowIndex, int numberOfRows) { | |||
for (int i = 0; i < numberOfRows; ++i) { | |||
int index = firstRowIndex + i; | |||
detachIfNeeded(index, getDetailsComponentConnectorId(index)); | |||
if (!getState().hasDetailsGenerator) { | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(false); | |||
return; | |||
} | |||
Range updatedRange = Range.withLength(firstRowIndex, numberOfRows); | |||
// NOTE: this relies on Escalator getting updated first | |||
Range newVisibleRowRange = getWidget().getEscalator() | |||
.getVisibleRowRange(); | |||
if (updatedRange.partitionWith(availableRowRange)[1] | |||
.length() != updatedRange.length() | |||
|| availableRowRange.partitionWith(newVisibleRowRange)[1] | |||
.length() != newVisibleRowRange.length()) { | |||
// full visible range not available yet or full refresh coming | |||
// up anyway, leave updating to dataAvailable | |||
if (numberOfRows == 1 | |||
&& latestVisibleRowRange.contains(firstRowIndex)) { | |||
// A single details row has been opened or closed within | |||
// visual range, trigger scrollTo after dataAvailable has | |||
// done its thing. Do not attempt to scroll to details rows | |||
// that are opened outside of the visual range. | |||
Scheduler.get().scheduleFinally(() -> { | |||
getParent().singleDetailsOpened(firstRowIndex); | |||
// we don't know yet whether there are details or not, | |||
// mark them added or updated just in case, so that | |||
// the potential scrolling attempt gets triggered after | |||
// another layout phase is finished | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(true); | |||
}); | |||
} | |||
return; | |||
} | |||
if (numberOfRows == 1) { | |||
// only trigger scrolling attempt if the single updated row is | |||
// within existing visual range | |||
boolean scrollToFirst = numberOfRows == 1 | |||
&& latestVisibleRowRange.contains(firstRowIndex); | |||
if (!newVisibleRowRange.equals(latestVisibleRowRange)) { | |||
// update visible range | |||
latestVisibleRowRange = newVisibleRowRange; | |||
// do full refresh | |||
detachOldAndRefreshCurrentDetails(); | |||
} else { | |||
// refresh only the updated range | |||
refreshDetailsVisibilityWithRange(updatedRange); | |||
// the update may have affected details row contents and size, | |||
// recalculation and triggering of any pending navigation | |||
// confirmations etc. is needed | |||
triggerDelayedRepositioning(firstRowIndex, numberOfRows); | |||
} | |||
if (scrollToFirst) { | |||
// scroll to opened row (if it got closed instead, nothing | |||
// happens) | |||
getParent().singleDetailsOpened(firstRowIndex); | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(true); | |||
} | |||
// Deferred opening of new ones. | |||
refreshDetails(); | |||
} | |||
/* The remaining methods will do a full refresh for now */ | |||
@Override | |||
public void dataRemoved(int firstRowIndex, int numberOfRows) { | |||
refreshDetails(); | |||
if (!getState().hasDetailsGenerator) { | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(false); | |||
return; | |||
} | |||
Range removing = Range.withLength(firstRowIndex, numberOfRows); | |||
// update the handled range to only contain rows that fall before | |||
// the removed range | |||
latestVisibleRowRange = Range | |||
.between(latestVisibleRowRange.getStart(), | |||
Math.max(latestVisibleRowRange.getStart(), | |||
Math.min(firstRowIndex, | |||
latestVisibleRowRange.getEnd()))); | |||
// reduce the available range accordingly | |||
Range[] partitions = availableRowRange.partitionWith(removing); | |||
Range removedAbove = partitions[0]; | |||
Range removedAvailable = partitions[1]; | |||
availableRowRange = Range.withLength( | |||
Math.max(0, | |||
availableRowRange.getStart() | |||
- removedAbove.length()), | |||
Math.max(0, availableRowRange.length() | |||
- removedAvailable.length())); | |||
for (int i = 0; i < numberOfRows; ++i) { | |||
int rowIndex = firstRowIndex + i; | |||
if (indexToDetailConnectorId.containsKey(rowIndex)) { | |||
String id = indexToDetailConnectorId.get(rowIndex); | |||
ComponentConnector connector = (ComponentConnector) ConnectorMap | |||
.get(getConnection()).getConnector(id); | |||
if (connector != null) { | |||
Element element = connector.getWidget().getElement(); | |||
elementToResizeCommand.remove(element); | |||
getLayoutManager().removeElementResizeListener(element, | |||
detailsRowResizeListener); | |||
} | |||
indexToDetailConnectorId.remove(rowIndex); | |||
} | |||
} | |||
// Grid and Escalator take care of their own cleanup at removal, no | |||
// need to clear details from those. Because this removal happens | |||
// instantly any pending scroll to row or such should not need | |||
// another attempt and unless something else causes such need the | |||
// pending operations should be cleared out. | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(false); | |||
} | |||
@Override | |||
public void dataAvailable(int firstRowIndex, int numberOfRows) { | |||
refreshDetails(); | |||
if (!getState().hasDetailsGenerator) { | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(false); | |||
return; | |||
} | |||
// update available range | |||
availableRowRange = Range.withLength(firstRowIndex, numberOfRows); | |||
// NOTE: this relies on Escalator getting updated first | |||
Range newVisibleRowRange = getWidget().getEscalator() | |||
.getVisibleRowRange(); | |||
// only process the section that is actually available | |||
newVisibleRowRange = availableRowRange | |||
.partitionWith(newVisibleRowRange)[1]; | |||
if (newVisibleRowRange.equals(latestVisibleRowRange)) { | |||
// no need to update | |||
return; | |||
} | |||
// check whether the visible range has simply got shortened | |||
// (e.g. by changing the default row height) | |||
boolean subsectionOfOld = latestVisibleRowRange | |||
.partitionWith(newVisibleRowRange)[1] | |||
.length() == newVisibleRowRange.length(); | |||
// update visible range | |||
latestVisibleRowRange = newVisibleRowRange; | |||
if (subsectionOfOld) { | |||
// only detach extra rows | |||
detachExcludingRange(latestVisibleRowRange); | |||
} else { | |||
// there are completely new visible rows, full refresh | |||
detachOldAndRefreshCurrentDetails(); | |||
} | |||
} | |||
@Override | |||
public void dataAdded(int firstRowIndex, int numberOfRows) { | |||
refreshDetails(); | |||
refreshDetailsVisibilityWithRange( | |||
Range.withLength(firstRowIndex, numberOfRows)); | |||
} | |||
} | |||
/** | |||
* Height aware details generator for client-side Grid. | |||
*/ | |||
@SuppressWarnings("deprecation") | |||
private class CustomDetailsGenerator | |||
implements HeightAwareDetailsGenerator { | |||
@@ -129,8 +273,24 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
public Widget getDetails(int rowIndex) { | |||
String id = getDetailsComponentConnectorId(rowIndex); | |||
if (id == null) { | |||
detachIfNeeded(rowIndex, id); | |||
return null; | |||
} | |||
String oldId = indexToDetailConnectorId.get(rowIndex); | |||
if (oldId != null && !oldId.equals(id)) { | |||
// remove outdated connector | |||
ComponentConnector connector = (ComponentConnector) ConnectorMap | |||
.get(getConnection()).getConnector(oldId); | |||
if (connector != null) { | |||
Element element = connector.getWidget().getElement(); | |||
elementToResizeCommand.remove(element); | |||
getLayoutManager().removeElementResizeListener(element, | |||
detailsRowResizeListener); | |||
} | |||
} | |||
indexToDetailConnectorId.put(rowIndex, id); | |||
getWidget().setDetailsVisible(rowIndex, true); | |||
Widget widget = getConnector(id).getWidget(); | |||
getLayoutManager().addElementResizeListener(widget.getElement(), | |||
@@ -169,7 +329,10 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
ComponentConnector componentConnector = getConnector(id); | |||
getLayoutManager().setNeedsMeasureRecursively(componentConnector); | |||
getLayoutManager().layoutNow(); | |||
if (!getLayoutManager().isLayoutRunning() | |||
&& !getConnection().getMessageHandler().isUpdatingState()) { | |||
getLayoutManager().layoutNow(); | |||
} | |||
Element element = componentConnector.getWidget().getElement(); | |||
if (spacerCellBorderHeights == null) { | |||
@@ -209,20 +372,144 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
dataChangeRegistration = getWidget().getDataSource() | |||
.addDataChangeHandler(new DetailsChangeHandler()); | |||
// When details element is shown, remeasure it in the layout phase | |||
spacerVisibilityChangeRegistration = getParent().getWidget() | |||
.addSpacerVisibilityChangedHandler(event -> { | |||
if (event.isSpacerVisible()) { | |||
String id = indexToDetailConnectorId | |||
.get(event.getRowIndex()); | |||
ComponentConnector connector = (ComponentConnector) ConnectorMap | |||
.get(getConnection()).getConnector(id); | |||
getLayoutManager() | |||
.setNeedsMeasureRecursively(connector); | |||
rowVisibilityChangeHandlerRegistration = getWidget() | |||
.addRowVisibilityChangeHandler(event -> { | |||
if (getConnection().getMessageHandler().isUpdatingState()) { | |||
// don't update in the middle of state changes, | |||
// leave to dataAvailable | |||
return; | |||
} | |||
Range newVisibleRowRange = event.getVisibleRowRange(); | |||
if (newVisibleRowRange.equals(latestVisibleRowRange)) { | |||
// no need to update | |||
return; | |||
} | |||
Range availableAndVisible = availableRowRange | |||
.partitionWith(newVisibleRowRange)[1]; | |||
if (availableAndVisible.isEmpty()) { | |||
// nothing to update yet, leave to dataAvailable | |||
return; | |||
} | |||
if (!availableAndVisible.equals(latestVisibleRowRange)) { | |||
// check whether the visible range has simply got | |||
// shortened | |||
// (e.g. by changing the default row height) | |||
boolean subsectionOfOld = latestVisibleRowRange | |||
.partitionWith(newVisibleRowRange)[1] | |||
.length() == newVisibleRowRange | |||
.length(); | |||
// update visible range | |||
latestVisibleRowRange = availableAndVisible; | |||
if (subsectionOfOld) { | |||
// only detach extra rows | |||
detachExcludingRange(latestVisibleRowRange); | |||
} else { | |||
// there are completely new visible rows, full | |||
// refresh | |||
detachOldAndRefreshCurrentDetails(); | |||
} | |||
} else { | |||
// refresh only the visible range, nothing to detach | |||
refreshDetailsVisibilityWithRange(availableAndVisible); | |||
// the update may have affected details row contents and | |||
// size, recalculation is needed | |||
triggerDelayedRepositioning( | |||
availableAndVisible.getStart(), | |||
availableAndVisible.length()); | |||
} | |||
}); | |||
} | |||
/** | |||
* Triggers repositioning of the the contents from the first affected row | |||
* downwards if any of the rows fall within the visual range. If any other | |||
* delayed repositioning has been triggered within this round trip the | |||
* affected range is expanded as needed. The processing is delayed to make | |||
* sure all updates have time to get in, otherwise the repositioning will be | |||
* calculated separately for each details row addition or removal from the | |||
* server side (see | |||
* {@link DataCommunicatorClientRpc#updateData(elemental.json.JsonArray)} | |||
* implementation within {@link DataCommunicatorConnector}). | |||
* | |||
* @param firstRowIndex | |||
* the index of the first changed row | |||
* @param numberOfRows | |||
* the number of changed rows | |||
*/ | |||
private void triggerDelayedRepositioning(int firstRowIndex, | |||
int numberOfRows) { | |||
if (delayedRepositioningStart == null | |||
|| delayedRepositioningStart > firstRowIndex) { | |||
delayedRepositioningStart = firstRowIndex; | |||
} | |||
if (delayedRepositioningEnd == null | |||
|| delayedRepositioningEnd < firstRowIndex + numberOfRows) { | |||
delayedRepositioningEnd = firstRowIndex + numberOfRows; | |||
} | |||
if (!delayedRepositioningTriggered) { | |||
delayedRepositioningTriggered = true; | |||
Scheduler.get().scheduleFinally(() -> { | |||
// refresh the positions of all affected rows and those | |||
// below them, unless all affected rows are outside of the | |||
// visual range | |||
if (getWidget().getEscalator().getVisibleRowRange() | |||
.intersects(Range.between(delayedRepositioningStart, | |||
delayedRepositioningEnd))) { | |||
getWidget().getEscalator().getBody().updateRowPositions( | |||
delayedRepositioningStart, | |||
getWidget().getEscalator().getBody().getRowCount() | |||
- delayedRepositioningStart); | |||
} | |||
delayedRepositioningTriggered = false; | |||
delayedRepositioningStart = null; | |||
delayedRepositioningEnd = null; | |||
}); | |||
} | |||
} | |||
/** | |||
* Makes sure that after the layout phase has finished Grid will be informed | |||
* whether any details rows were added or updated. This delay is needed to | |||
* allow the details row(s) to get their final size, and it's possible that | |||
* more than one operation that might affect that size or details row | |||
* existence will be performed (and consequently this method called) before | |||
* the check can actually be made. | |||
* <p> | |||
* If this method is called with value {@code true} at least once within the | |||
* delay phase Grid will be told to run any pending position-sensitive | |||
* operations it might have in store. | |||
* <p> | |||
* If this method is only called with value {@code false} within the delay | |||
* period Grid will be told to cancel the pending operations. | |||
* <p> | |||
* If this method isn't called at all, Grid won't be instructed to either | |||
* trigger the pending operations or cancel them and hence they remain in a | |||
* pending state. | |||
* | |||
* @param newOrUpdatedDetails | |||
* {@code true} if the calling operation added or updated | |||
* details, {@code false} otherwise | |||
*/ | |||
private void markDetailsAddedOrUpdatedForDelayedAlertToGrid( | |||
boolean newOrUpdatedDetails) { | |||
if (newOrUpdatedDetails) { | |||
delayedDetailsAddedOrUpdated = true; | |||
} | |||
if (!delayedDetailsAddedOrUpdatedAlertTriggered) { | |||
delayedDetailsAddedOrUpdatedAlertTriggered = true; | |||
Scheduler.get().scheduleFinally(() -> { | |||
getParent().detailsRefreshed(delayedDetailsAddedOrUpdated); | |||
delayedDetailsAddedOrUpdatedAlertTriggered = false; | |||
delayedDetailsAddedOrUpdated = false; | |||
}); | |||
} | |||
} | |||
private void detachIfNeeded(int rowIndex, String id) { | |||
if (indexToDetailConnectorId.containsKey(rowIndex)) { | |||
if (indexToDetailConnectorId.get(rowIndex).equals(id)) { | |||
@@ -256,8 +543,8 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
dataChangeRegistration.remove(); | |||
dataChangeRegistration = null; | |||
spacerVisibilityChangeRegistration.removeHandler(); | |||
spacerIndexChangedHandlerRegistration.removeHandler(); | |||
rowVisibilityChangeHandlerRegistration.removeHandler(); | |||
indexToDetailConnectorId.clear(); | |||
} | |||
@@ -284,8 +571,7 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
* @return connector id; {@code null} if row or id is not found | |||
*/ | |||
private String getDetailsComponentConnectorId(int rowIndex) { | |||
JsonObject row = getParent().getWidget().getDataSource() | |||
.getRow(rowIndex); | |||
JsonObject row = getWidget().getDataSource().getRow(rowIndex); | |||
if (row == null || !row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE) | |||
|| row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty()) { | |||
@@ -300,20 +586,29 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
} | |||
/** | |||
* Schedules a deferred opening for new details components. | |||
* Refreshes the existence of details components within the given range, and | |||
* gives a delayed notice to Grid if any got added or updated. | |||
*/ | |||
private void refreshDetails() { | |||
if (refreshing) { | |||
private void refreshDetailsVisibilityWithRange(Range rangeToRefresh) { | |||
if (!getState().hasDetailsGenerator) { | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(false); | |||
return; | |||
} | |||
boolean newOrUpdatedDetails = false; | |||
refreshing = true; | |||
Scheduler.get().scheduleFinally(this::refreshDetailsVisibility); | |||
} | |||
// Don't update the latestVisibleRowRange class variable here, the | |||
// calling method should take care of that if relevant. | |||
Range currentVisibleRowRange = getWidget().getEscalator() | |||
.getVisibleRowRange(); | |||
Range[] partitions = currentVisibleRowRange | |||
.partitionWith(rangeToRefresh); | |||
// only inspect the range where visible and refreshed rows overlap | |||
Range intersectingRange = partitions[1]; | |||
private void refreshDetailsVisibility() { | |||
boolean shownDetails = false; | |||
for (int i = 0; i < getWidget().getDataSource().size(); ++i) { | |||
for (int i = intersectingRange.getStart(); i < intersectingRange | |||
.getEnd(); ++i) { | |||
String id = getDetailsComponentConnectorId(i); | |||
detachIfNeeded(i, id); | |||
@@ -324,9 +619,98 @@ public class DetailsManagerConnector extends AbstractExtensionConnector { | |||
indexToDetailConnectorId.put(i, id); | |||
getWidget().setDetailsVisible(i, true); | |||
shownDetails = true; | |||
newOrUpdatedDetails = true; | |||
} | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(newOrUpdatedDetails); | |||
} | |||
private void detachOldAndRefreshCurrentDetails() { | |||
Range[] partitions = availableRowRange | |||
.partitionWith(latestVisibleRowRange); | |||
Range availableAndVisible = partitions[1]; | |||
detachExcludingRange(availableAndVisible); | |||
boolean newOrUpdatedDetails = refreshRange(availableAndVisible); | |||
markDetailsAddedOrUpdatedForDelayedAlertToGrid(newOrUpdatedDetails); | |||
} | |||
private void detachExcludingRange(Range keep) { | |||
// remove all spacers that are no longer in range | |||
for (Integer existingIndex : indexToDetailConnectorId.keySet()) { | |||
if (!keep.contains(existingIndex)) { | |||
detachDetails(existingIndex); | |||
} | |||
} | |||
} | |||
private boolean refreshRange(Range rangeToRefresh) { | |||
// make sure all spacers that are currently in range are up to date | |||
boolean newOrUpdatedDetails = false; | |||
for (int i = rangeToRefresh.getStart(); i < rangeToRefresh | |||
.getEnd(); ++i) { | |||
int rowIndex = i; | |||
if (refreshDetails(rowIndex)) { | |||
newOrUpdatedDetails = true; | |||
} | |||
} | |||
return newOrUpdatedDetails; | |||
} | |||
private void detachDetails(int rowIndex) { | |||
String id = indexToDetailConnectorId.remove(rowIndex); | |||
if (id != null) { | |||
ComponentConnector connector = (ComponentConnector) ConnectorMap | |||
.get(getConnection()).getConnector(id); | |||
if (connector != null) { | |||
Element element = connector.getWidget().getElement(); | |||
elementToResizeCommand.remove(element); | |||
getLayoutManager().removeElementResizeListener(element, | |||
detailsRowResizeListener); | |||
} | |||
} | |||
getWidget().setDetailsVisible(rowIndex, false); | |||
} | |||
private boolean refreshDetails(int rowIndex) { | |||
String id = getDetailsComponentConnectorId(rowIndex); | |||
String oldId = indexToDetailConnectorId.get(rowIndex); | |||
if ((oldId == null && id == null) | |||
|| (oldId != null && oldId.equals(id))) { | |||
// nothing to update, move along | |||
return false; | |||
} | |||
boolean newOrUpdatedDetails = false; | |||
if (oldId != null) { | |||
// Details have been hidden or updated, listeners attached | |||
// to the old component need to be removed | |||
ComponentConnector connector = (ComponentConnector) ConnectorMap | |||
.get(getConnection()).getConnector(oldId); | |||
if (connector != null) { | |||
Element element = connector.getWidget().getElement(); | |||
elementToResizeCommand.remove(element); | |||
getLayoutManager().removeElementResizeListener(element, | |||
detailsRowResizeListener); | |||
} | |||
if (id == null) { | |||
// hidden, clear reference | |||
getWidget().setDetailsVisible(rowIndex, false); | |||
indexToDetailConnectorId.remove(rowIndex); | |||
} else { | |||
// updated, replace reference | |||
indexToDetailConnectorId.put(rowIndex, id); | |||
newOrUpdatedDetails = true; | |||
} | |||
} else { | |||
// new Details content, listeners will get attached to the connector | |||
// when Escalator requests for the Details through | |||
// CustomDetailsGenerator#getDetails(int) | |||
indexToDetailConnectorId.put(rowIndex, id); | |||
newOrUpdatedDetails = true; | |||
getWidget().setDetailsVisible(rowIndex, true); | |||
} | |||
refreshing = false; | |||
getParent().detailsRefreshed(shownDetails); | |||
return newOrUpdatedDetails; | |||
} | |||
} |
@@ -87,6 +87,61 @@ public class GridConnector extends AbstractListingConnector | |||
private Set<Runnable> refreshDetailsCallbacks = new HashSet<>(); | |||
/** | |||
* Server-to-client RPC implementation for GridConnector. | |||
* <p> | |||
* The scrolling methods must trigger the scrolling only after any potential | |||
* resizing or other similar action triggered from the server side within | |||
* the same round trip has had a chance to happen, so there needs to be a | |||
* delay. The delay is done with <code>scheduleFinally</code> rather than | |||
* <code>scheduleDeferred</code> because the latter has been known to cause | |||
* flickering in Grid. | |||
* | |||
*/ | |||
private class GridConnectorClientRpc implements GridClientRpc { | |||
private final Grid<JsonObject> grid; | |||
private GridConnectorClientRpc(Grid<JsonObject> grid) { | |||
this.grid = grid; | |||
} | |||
@Override | |||
public void scrollToRow(int row, ScrollDestination destination) { | |||
Scheduler.get().scheduleFinally(() -> { | |||
grid.scrollToRow(row, destination); | |||
// Add details refresh listener and handle possible detail | |||
// for scrolled row. | |||
addDetailsRefreshCallback(() -> { | |||
if (rowHasDetails(row)) { | |||
grid.scrollToRow(row, destination); | |||
} | |||
}); | |||
}); | |||
} | |||
@Override | |||
public void scrollToStart() { | |||
Scheduler.get().scheduleFinally(() -> grid.scrollToStart()); | |||
} | |||
@Override | |||
public void scrollToEnd() { | |||
Scheduler.get().scheduleFinally(() -> { | |||
grid.scrollToEnd(); | |||
addDetailsRefreshCallback(() -> { | |||
if (rowHasDetails(grid.getDataSource().size() - 1)) { | |||
grid.scrollToEnd(); | |||
} | |||
}); | |||
}); | |||
} | |||
@Override | |||
public void recalculateColumnWidths() { | |||
grid.recalculateColumnWidths(); | |||
} | |||
} | |||
private class ItemClickHandler | |||
implements BodyClickHandler, BodyDoubleClickHandler { | |||
@@ -217,41 +272,7 @@ public class GridConnector extends AbstractListingConnector | |||
grid.setHeaderVisible(!grid.isHeaderVisible()); | |||
grid.setFooterVisible(!grid.isFooterVisible()); | |||
registerRpc(GridClientRpc.class, new GridClientRpc() { | |||
@Override | |||
public void scrollToRow(int row, ScrollDestination destination) { | |||
Scheduler.get().scheduleFinally( | |||
() -> grid.scrollToRow(row, destination)); | |||
// Add details refresh listener and handle possible detail for | |||
// scrolled row. | |||
addDetailsRefreshCallback(() -> { | |||
if (rowHasDetails(row)) { | |||
grid.scrollToRow(row, destination); | |||
} | |||
}); | |||
} | |||
@Override | |||
public void scrollToStart() { | |||
Scheduler.get().scheduleFinally(() -> grid.scrollToStart()); | |||
} | |||
@Override | |||
public void scrollToEnd() { | |||
Scheduler.get().scheduleFinally(() -> grid.scrollToEnd()); | |||
addDetailsRefreshCallback(() -> { | |||
if (rowHasDetails(grid.getDataSource().size() - 1)) { | |||
grid.scrollToEnd(); | |||
} | |||
}); | |||
} | |||
@Override | |||
public void recalculateColumnWidths() { | |||
grid.recalculateColumnWidths(); | |||
} | |||
}); | |||
registerRpc(GridClientRpc.class, new GridConnectorClientRpc(grid)); | |||
grid.addSortHandler(this::handleSortEvent); | |||
grid.setRowStyleGenerator(rowRef -> { |
@@ -131,6 +131,20 @@ public interface RowContainer { | |||
public void removeRows(int index, int numberOfRows) | |||
throws IndexOutOfBoundsException, IllegalArgumentException; | |||
/** | |||
* Recalculates and updates the positions of rows and spacers within the | |||
* given range and ensures there is no gap below the rows if there are | |||
* enough rows to fill the space. Recalculates the scrollbars for | |||
* virtual viewport. | |||
* | |||
* @param index | |||
* logical index of the first row to reposition | |||
* @param numberOfRows | |||
* the number of rows to reposition | |||
* @since 8.9 | |||
*/ | |||
public void updateRowPositions(int index, int numberOfRows); | |||
/** | |||
* Sets a callback function that is executed when new rows are added to | |||
* the escalator. |
@@ -3829,7 +3829,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
* height, but the spacer cell (td) has the borders, which | |||
* should go on top of the previous row and next row. | |||
*/ | |||
double contentHeight; | |||
final double contentHeight; | |||
if (detailsGenerator instanceof HeightAwareDetailsGenerator) { | |||
HeightAwareDetailsGenerator sadg = (HeightAwareDetailsGenerator) detailsGenerator; | |||
contentHeight = sadg.getDetailsHeight(rowIndex); | |||
@@ -3839,8 +3839,32 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
} | |||
double borderTopAndBottomHeight = WidgetUtil | |||
.getBorderTopAndBottomThickness(spacerElement); | |||
double measuredHeight = contentHeight | |||
+ borderTopAndBottomHeight; | |||
double measuredHeight = 0d; | |||
if (contentHeight > 0) { | |||
measuredHeight = contentHeight + borderTopAndBottomHeight; | |||
} else { | |||
Scheduler.get().scheduleFinally(() -> { | |||
// make sure the spacer hasn't got removed | |||
if (spacer.getElement().getParentElement() != null) { | |||
// re-check the height | |||
double confirmedContentHeight = WidgetUtil | |||
.getRequiredHeightBoundingClientRectDouble( | |||
element); | |||
if (confirmedContentHeight > 0) { | |||
double confirmedMeasuredHeight = confirmedContentHeight | |||
+ WidgetUtil | |||
.getBorderTopAndBottomThickness( | |||
spacer.getElement()); | |||
escalator.getBody().setSpacer(spacer.getRow(), | |||
confirmedMeasuredHeight); | |||
if (getHeightMode() == HeightMode.UNDEFINED) { | |||
setHeightByRows(getEscalator().getBody() | |||
.getRowCount()); | |||
} | |||
} | |||
} | |||
}); | |||
} | |||
assert getElement().isOrHasChild( | |||
spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be."; | |||
spacerHeight = measuredHeight; | |||
@@ -7208,6 +7232,9 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
@Override | |||
public void dataRemoved(int firstIndex, int numberOfItems) { | |||
for (int i = 0; i < numberOfItems; ++i) { | |||
visibleDetails.remove(firstIndex + i); | |||
} | |||
escalator.getBody().removeRows(firstIndex, | |||
numberOfItems); | |||
Range removed = Range.withLength(firstIndex, | |||
@@ -9213,17 +9240,6 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
recalculateColumnWidths(); | |||
} | |||
if (getEscalatorInnerHeight() != autoColumnWidthsRecalculator.lastCalculatedInnerHeight) { | |||
Scheduler.get().scheduleFinally(() -> { | |||
// Trigger re-calculation of all row positions. | |||
RowContainer.BodyRowContainer body = getEscalator() | |||
.getBody(); | |||
if (!body.isAutodetectingRowHeightLater()) { | |||
body.setDefaultRowHeight(body.getDefaultRowHeight()); | |||
} | |||
}); | |||
} | |||
// Vertical resizing could make editor positioning invalid so it | |||
// needs to be recalculated on resize | |||
if (isEditorActive()) { |
@@ -737,6 +737,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
if (this.generator != generator) { | |||
removeAllComponents(); | |||
} | |||
getState().hasDetailsGenerator = generator != null; | |||
this.generator = generator; | |||
visibleDetails.forEach(this::refresh); | |||
} |
@@ -22,4 +22,12 @@ package com.vaadin.shared.ui.grid; | |||
*/ | |||
public class DetailsManagerState extends AbstractGridExtensionState { | |||
/** | |||
* For informing the connector when details handling can be skipped | |||
* altogether as it's not possible to have any details rows without a | |||
* generator. | |||
* | |||
* @since 8.9 | |||
*/ | |||
public boolean hasDetailsGenerator = false; | |||
} |
@@ -48,7 +48,7 @@ public class TreeGridBigDetailsManager extends AbstractTestUI { | |||
treeGrid.setSizeFull(); | |||
treeGrid.addColumn(String::toString).setCaption("String") | |||
.setId("string"); | |||
treeGrid.addColumn((i) -> "--").setCaption("Nothing"); | |||
treeGrid.addColumn((i) -> items.indexOf(i)).setCaption("Index"); | |||
treeGrid.setHierarchyColumn("string"); | |||
treeGrid.setDetailsGenerator( | |||
row -> new Label("details for " + row.toString())); | |||
@@ -77,21 +77,54 @@ public class TreeGridBigDetailsManager extends AbstractTestUI { | |||
treeGrid.collapse(items); | |||
}); | |||
collapseAll.setId("collapseAll"); | |||
@SuppressWarnings("deprecation") | |||
Button scrollTo55 = new Button("Scroll to 55", | |||
event -> treeGrid.scrollTo(55)); | |||
scrollTo55.setId("scrollTo55"); | |||
scrollTo55.setVisible(false); | |||
Button scrollTo3055 = new Button("Scroll to 3055", | |||
event -> treeGrid.scrollTo(3055)); | |||
scrollTo3055.setId("scrollTo3055"); | |||
scrollTo3055.setVisible(false); | |||
Button scrollToEnd = new Button("Scroll to end", | |||
event -> treeGrid.scrollToEnd()); | |||
scrollToEnd.setId("scrollToEnd"); | |||
scrollToEnd.setVisible(false); | |||
Button scrollToStart = new Button("Scroll to start", | |||
event -> treeGrid.scrollToStart()); | |||
scrollToStart.setId("scrollToStart"); | |||
scrollToStart.setVisible(false); | |||
Button toggle15 = new Button("Toggle 15", | |||
event -> treeGrid.setDetailsVisible(items.get(15), | |||
!treeGrid.isDetailsVisible(items.get(15)))); | |||
toggle15.setId("toggle15"); | |||
toggle15.setVisible(false); | |||
Button toggle3000 = new Button("Toggle 3000", | |||
event -> treeGrid.setDetailsVisible(items.get(3000), | |||
!treeGrid.isDetailsVisible(items.get(3000)))); | |||
toggle3000.setId("toggle3000"); | |||
toggle3000.setVisible(false); | |||
Button addGrid = new Button("Add grid", event -> { | |||
addComponent(treeGrid); | |||
getLayout().setExpandRatio(treeGrid, 2); | |||
scrollTo55.setVisible(true); | |||
scrollTo3055.setVisible(true); | |||
scrollToEnd.setVisible(true); | |||
scrollToStart.setVisible(true); | |||
toggle15.setVisible(true); | |||
toggle3000.setVisible(true); | |||
}); | |||
addGrid.setId("addGrid"); | |||
addComponents( | |||
new HorizontalLayout(showDetails, hideDetails, expandAll, | |||
collapseAll), | |||
new HorizontalLayout(addGrid, scrollTo55)); | |||
new HorizontalLayout(scrollTo55, scrollTo3055, scrollToEnd, | |||
scrollToStart), | |||
new HorizontalLayout(addGrid, toggle15, toggle3000)); | |||
getLayout().getParent().setHeight("100%"); | |||
getLayout().setHeight("100%"); |
@@ -1,9 +1,12 @@ | |||
package com.vaadin.tests.widgetset.client.grid; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import com.google.gwt.core.client.Duration; | |||
import com.google.gwt.core.client.Scheduler; | |||
import com.google.gwt.core.client.Scheduler.ScheduledCommand; | |||
import com.google.gwt.dom.client.TableCellElement; | |||
import com.google.gwt.user.client.DOM; | |||
@@ -16,6 +19,8 @@ import com.vaadin.client.widget.escalator.RowContainer; | |||
import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; | |||
import com.vaadin.client.widget.escalator.Spacer; | |||
import com.vaadin.client.widget.escalator.SpacerUpdater; | |||
import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent; | |||
import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler; | |||
import com.vaadin.client.widgets.Escalator; | |||
import com.vaadin.shared.ui.grid.ScrollDestination; | |||
import com.vaadin.tests.widgetset.client.v7.grid.PureGWTTestApplication; | |||
@@ -156,6 +161,7 @@ public class EscalatorBasicClientFeaturesWidget | |||
private int rowCounter = 0; | |||
private final List<Integer> columns = new ArrayList<>(); | |||
private final List<Integer> rows = new ArrayList<>(); | |||
private final Map<Integer, Integer> spacers = new HashMap<>(); | |||
@SuppressWarnings("boxing") | |||
public void insertRows(final int offset, final int amount) { | |||
@@ -247,6 +253,11 @@ public class EscalatorBasicClientFeaturesWidget | |||
cell.setColSpan(2); | |||
} | |||
} | |||
if (spacers.containsKey(cell.getRow()) && !escalator | |||
.getBody().spacerExists(cell.getRow())) { | |||
escalator.getBody().setSpacer(cell.getRow(), | |||
spacers.get(cell.getRow())); | |||
} | |||
} | |||
@Override | |||
@@ -262,7 +273,12 @@ public class EscalatorBasicClientFeaturesWidget | |||
public void removeRows(final int offset, final int amount) { | |||
for (int i = 0; i < amount; i++) { | |||
rows.remove(offset); | |||
if (spacers.containsKey(offset + i)) { | |||
spacers.remove(offset + i); | |||
} | |||
} | |||
// the following spacers get their indexes updated through | |||
// SpacerIndexChangedHandler | |||
} | |||
public void removeColumns(final int offset, final int amount) { | |||
@@ -313,6 +329,21 @@ public class EscalatorBasicClientFeaturesWidget | |||
createFrozenMenu(); | |||
createColspanMenu(); | |||
createSpacerMenu(); | |||
escalator.addHandler(new SpacerIndexChangedHandler() { | |||
@Override | |||
public void onSpacerIndexChanged(SpacerIndexChangedEvent event) { | |||
// remove spacer from old index and move to new index | |||
Integer height = data.spacers.remove(event.getOldIndex()); | |||
if (height != null) { | |||
data.spacers.put(event.getNewIndex(), height); | |||
} else { | |||
// no height, make sure the new index doesn't | |||
// point to anything else either | |||
data.spacers.remove(event.getNewIndex()); | |||
} | |||
} | |||
}, SpacerIndexChangedEvent.TYPE); | |||
} | |||
private void createFrozenMenu() { | |||
@@ -558,11 +589,24 @@ public class EscalatorBasicClientFeaturesWidget | |||
@Override | |||
public void init(Spacer spacer) { | |||
spacer.getElement().appendChild(DOM.createInputText()); | |||
updateRowPositions(spacer); | |||
} | |||
@Override | |||
public void destroy(Spacer spacer) { | |||
spacer.getElement().removeAllChildren(); | |||
updateRowPositions(spacer); | |||
} | |||
private void updateRowPositions(Spacer spacer) { | |||
if (spacer.getRow() < escalator.getBody() | |||
.getRowCount()) { | |||
Scheduler.get().scheduleFinally(() -> { | |||
escalator.getBody().updateRowPositions( | |||
spacer.getRow(), | |||
escalator.getBody().getRowCount()); | |||
}); | |||
} | |||
} | |||
}), menupath); | |||
@@ -575,12 +619,18 @@ public class EscalatorBasicClientFeaturesWidget | |||
private void createSpacersMenuForRow(final int rowIndex, | |||
String[] menupath) { | |||
menupath = new String[] { menupath[0], menupath[1], "Row " + rowIndex }; | |||
addMenuCommand("Set 100px", | |||
() -> escalator.getBody().setSpacer(rowIndex, 100), menupath); | |||
addMenuCommand("Set 50px", | |||
() -> escalator.getBody().setSpacer(rowIndex, 50), menupath); | |||
addMenuCommand("Remove", | |||
() -> escalator.getBody().setSpacer(rowIndex, -1), menupath); | |||
addMenuCommand("Set 100px", () -> { | |||
escalator.getBody().setSpacer(rowIndex, 100); | |||
data.spacers.put(rowIndex, 100); | |||
}, menupath); | |||
addMenuCommand("Set 50px", () -> { | |||
escalator.getBody().setSpacer(rowIndex, 50); | |||
data.spacers.put(rowIndex, 50); | |||
}, menupath); | |||
addMenuCommand("Remove", () -> { | |||
escalator.getBody().setSpacer(rowIndex, -1); | |||
data.spacers.remove(rowIndex); | |||
}, menupath); | |||
addMenuCommand("Scroll here (ANY, 0)", () -> escalator | |||
.scrollToSpacer(rowIndex, ScrollDestination.ANY, 0), menupath); | |||
addMenuCommand("Scroll here row+spacer below (ANY, 0)", () -> escalator | |||
@@ -596,6 +646,9 @@ public class EscalatorBasicClientFeaturesWidget | |||
} else { | |||
container.insertRows(offset, number); | |||
} | |||
if (container.getRowCount() > offset + number) { | |||
container.refreshRows(offset + number, container.getRowCount()); | |||
} | |||
} | |||
private void removeRows(final RowContainer container, int offset, | |||
@@ -606,6 +659,9 @@ public class EscalatorBasicClientFeaturesWidget | |||
} else { | |||
container.removeRows(offset, number); | |||
} | |||
if (container.getRowCount() > offset) { | |||
container.refreshRows(offset, container.getRowCount()); | |||
} | |||
} | |||
private void insertColumns(final int offset, final int number) { |
@@ -125,6 +125,11 @@ public class EscalatorProxy extends Escalator { | |||
throw new UnsupportedOperationException( | |||
"setNewRowCallback is not supported"); | |||
} | |||
@Override | |||
public void updateRowPositions(int index, int numberOfRows) { | |||
rowContainer.updateRowPositions(index, numberOfRows); | |||
} | |||
} | |||
private class RowContainerProxy implements RowContainer { |
@@ -1,5 +1,9 @@ | |||
package com.vaadin.tests.components.grid; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertTrue; | |||
import java.util.Locale; | |||
import java.util.stream.IntStream; | |||
import java.util.stream.Stream; | |||
@@ -21,10 +25,6 @@ import com.vaadin.testbench.elements.TextFieldElement; | |||
import com.vaadin.testbench.parallel.BrowserUtil; | |||
import com.vaadin.tests.tb3.MultiBrowserTest; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertTrue; | |||
public class GridComponentsTest extends MultiBrowserTest { | |||
@Test | |||
@@ -219,19 +219,14 @@ public class GridComponentsTest extends MultiBrowserTest { | |||
getScrollLeft(grid)); | |||
// Navigate back to fully visible TextField | |||
new Actions(getDriver()).sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB)) | |||
.perform(); | |||
pressKeyWithModifier(Keys.SHIFT, Keys.TAB); | |||
assertEquals( | |||
"Grid should not scroll when focusing the text field again. ", | |||
scrollMax, getScrollLeft(grid)); | |||
// Navigate to out of viewport TextField in Header | |||
new Actions(getDriver()).sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB)) | |||
.perform(); | |||
// After Chrome 75, sendkeys issues | |||
if (BrowserUtil.isChrome(getDesiredCapabilities())) { | |||
grid.getHeaderCell(1, 0).findElement(By.id("headerField")).click(); | |||
} | |||
pressKeyWithModifier(Keys.SHIFT, Keys.TAB); | |||
assertEquals("Focus should be in TextField in Header", "headerField", | |||
getFocusedElement().getAttribute("id")); | |||
assertEquals("Grid should've scrolled back to start.", 0, | |||
@@ -248,10 +243,30 @@ public class GridComponentsTest extends MultiBrowserTest { | |||
// Navigate to currently out of viewport TextField on Row 8 | |||
new Actions(getDriver()).sendKeys(Keys.TAB, Keys.TAB).perform(); | |||
assertTrue("Grid should be scrolled to show row 7", | |||
assertTrue("Grid should be scrolled to show row 8", | |||
Integer.parseInt(grid.getVerticalScroller() | |||
.getAttribute("scrollTop")) > scrollTopRow7); | |||
// Focus button in first visible row of Grid | |||
grid.getCell(2, 2).findElement(By.id("row_2")).click(); | |||
int scrollTopRow2 = Integer | |||
.parseInt(grid.getVerticalScroller().getAttribute("scrollTop")); | |||
// Navigate to currently out of viewport Button on Row 1 | |||
pressKeyWithModifier(Keys.SHIFT, Keys.TAB); | |||
pressKeyWithModifier(Keys.SHIFT, Keys.TAB); | |||
int scrollTopRow1 = Integer | |||
.parseInt(grid.getVerticalScroller().getAttribute("scrollTop")); | |||
assertTrue("Grid should be scrolled to show row 1", | |||
scrollTopRow1 < scrollTopRow2); | |||
// Continue further to the very first row | |||
pressKeyWithModifier(Keys.SHIFT, Keys.TAB); | |||
pressKeyWithModifier(Keys.SHIFT, Keys.TAB); | |||
assertTrue("Grid should be scrolled to show row 0", | |||
Integer.parseInt(grid.getVerticalScroller() | |||
.getAttribute("scrollTop")) < scrollTopRow1); | |||
// Focus button in last row of Grid | |||
grid.getCell(999, 2).findElement(By.id("row_999")).click(); | |||
// Navigate to out of viewport TextField in Footer | |||
@@ -288,4 +303,12 @@ public class GridComponentsTest extends MultiBrowserTest { | |||
assertFalse("Row " + i + " should not have a button", | |||
row.getCell(2).isElementPresent(ButtonElement.class)); | |||
} | |||
// Workaround for Chrome 75, sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB)) | |||
// doesn't work anymore | |||
private void pressKeyWithModifier(Keys keyModifier, Keys key) { | |||
new Actions(getDriver()).keyDown(keyModifier).perform(); | |||
new Actions(getDriver()).sendKeys(key).perform(); | |||
new Actions(getDriver()).keyUp(keyModifier).perform(); | |||
} | |||
} |
@@ -70,28 +70,27 @@ public class GridScrolledToBottomTest extends MultiBrowserTest { | |||
Actions actions = new Actions(driver); | |||
actions.clickAndHold(splitter).moveByOffset(0, -rowHeight / 2).release() | |||
.perform(); | |||
// the last row is now only half visible, and in DOM tree it's actually | |||
// the first row now but positioned to the bottom | |||
// the last row is now only half visible | |||
// can't query grid.getRow(99) now or it moves the row position, | |||
// have to use element query instead | |||
List<WebElement> rows = grid.findElement(By.className("v-grid-body")) | |||
.findElements(By.className("v-grid-row")); | |||
WebElement firstRow = rows.get(0); | |||
WebElement lastRow = rows.get(rows.size() - 1); | |||
WebElement secondToLastRow = rows.get(rows.size() - 2); | |||
// ensure the scrolling didn't jump extra | |||
assertEquals("Person 99", | |||
firstRow.findElement(By.className("v-grid-cell")).getText()); | |||
assertEquals("Person 98", | |||
lastRow.findElement(By.className("v-grid-cell")).getText()); | |||
assertEquals("Person 98", secondToLastRow | |||
.findElement(By.className("v-grid-cell")).getText()); | |||
// re-calculate current end position | |||
gridBottomY = grid.getLocation().getY() + grid.getSize().getHeight(); | |||
// ensure the correct final row really is only half visible at the | |||
// bottom | |||
assertThat(gridBottomY, greaterThan(firstRow.getLocation().getY())); | |||
assertThat(firstRow.getLocation().getY() + rowHeight, | |||
assertThat(gridBottomY, greaterThan(lastRow.getLocation().getY())); | |||
assertThat(lastRow.getLocation().getY() + rowHeight, | |||
greaterThan(gridBottomY)); | |||
} | |||
} |
@@ -103,12 +103,12 @@ public class GridColumnResizeModeTest extends GridBasicsTest { | |||
// ANIMATED resize mode | |||
drag(handle, 100); | |||
assertTrue( | |||
assertTrue("Expected width: " + cell.getSize().getWidth(), | |||
getLogRow(0).contains("Column resized: caption=Column 1, width=" | |||
+ cell.getSize().getWidth())); | |||
drag(handle, -100); | |||
assertTrue( | |||
assertTrue("Expected width: " + cell.getSize().getWidth(), | |||
getLogRow(0).contains("Column resized: caption=Column 1, width=" | |||
+ cell.getSize().getWidth())); | |||
@@ -117,12 +117,12 @@ public class GridColumnResizeModeTest extends GridBasicsTest { | |||
sleep(250); | |||
drag(handle, 100); | |||
assertTrue( | |||
assertTrue("Expected width: " + cell.getSize().getWidth(), | |||
getLogRow(0).contains("Column resized: caption=Column 1, width=" | |||
+ cell.getSize().getWidth())); | |||
drag(handle, -100); | |||
assertTrue( | |||
assertTrue("Expected width: " + cell.getSize().getWidth(), | |||
getLogRow(0).contains("Column resized: caption=Column 1, width=" | |||
+ cell.getSize().getWidth())); | |||
} |
@@ -3,12 +3,16 @@ package com.vaadin.tests.components.grid.basicfeatures.escalator; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertNull; | |||
import static org.junit.Assert.assertTrue; | |||
import java.io.IOException; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.openqa.selenium.By; | |||
import org.openqa.selenium.WebElement; | |||
import com.vaadin.testbench.TestBenchElement; | |||
import com.vaadin.testbench.elements.NotificationElement; | |||
import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; | |||
@@ -49,13 +53,29 @@ public class EscalatorBasicsTest extends EscalatorBasicClientFeaturesTest { | |||
scrollHorizontallyTo(50); | |||
selectMenuPath(GENERAL, DETACH_ESCALATOR); | |||
waitForElementNotPresent(By.className("v-escalator")); | |||
selectMenuPath(GENERAL, ATTACH_ESCALATOR); | |||
waitForElementPresent(By.className("v-escalator")); | |||
assertEquals("Vertical scroll position", 50, getScrollTop()); | |||
assertEquals("Horizontal scroll position", 50, getScrollLeft()); | |||
TestBenchElement bodyCell = getBodyCell(2, 0); | |||
WebElement viewport = findElement( | |||
By.className("v-escalator-tablewrapper")); | |||
WebElement header = findElement(By.className("v-escalator-header")); | |||
// ensure this is the first (partially) visible cell | |||
assertTrue( | |||
viewport.getLocation().getX() > bodyCell.getLocation().getX()); | |||
assertTrue(viewport.getLocation().getX() < bodyCell.getLocation().getX() | |||
+ bodyCell.getSize().getWidth()); | |||
assertTrue(header.getLocation().getY() | |||
+ header.getSize().getHeight() > bodyCell.getLocation().getY()); | |||
assertTrue(header.getLocation().getY() | |||
+ header.getSize().getHeight() < bodyCell.getLocation().getY() | |||
+ bodyCell.getSize().getHeight()); | |||
assertEquals("First cell of first visible row", "Row 2: 0,2", | |||
getBodyCell(0, 0).getText()); | |||
bodyCell.getText()); | |||
} | |||
private void assertEscalatorIsRemovedCorrectly() { |
@@ -275,12 +275,17 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { | |||
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); | |||
/* | |||
* we check for row -3 instead of -1, because escalator has two rows | |||
* we check for row -2 instead of -1, because escalator has one row | |||
* buffered underneath the footer | |||
*/ | |||
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_75); | |||
Thread.sleep(500); | |||
assertEquals("Row 75: 0,75", getBodyCell(-3, 0).getText()); | |||
TestBenchElement cell75 = getBodyCell(-2, 0); | |||
assertEquals("Row 75: 0,75", cell75.getText()); | |||
// confirm the scroll position | |||
WebElement footer = findElement(By.className("v-escalator-footer")); | |||
assertEquals(footer.getLocation().y, | |||
cell75.getLocation().y + cell75.getSize().height); | |||
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_25); | |||
Thread.sleep(500); | |||
@@ -406,17 +411,52 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { | |||
} | |||
@Test | |||
public void spacersAreInCorrectDomPositionAfterScroll() { | |||
public void spacersAreInCorrectDomPositionAfterScroll() | |||
throws InterruptedException { | |||
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); | |||
scrollVerticallyTo(32); // roughly one row's worth | |||
scrollVerticallyTo(40); // roughly two rows' worth | |||
// both rows should still be within DOM after this little scrolling, so | |||
// the spacer should be the third element within the body (index: 2) | |||
WebElement tbody = getEscalator().findElement(By.tagName("tbody")); | |||
WebElement spacer = getChild(tbody, 1); | |||
WebElement spacer = getChild(tbody, 2); | |||
String cssClass = spacer.getAttribute("class"); | |||
assertTrue( | |||
"element index 1 was not a spacer (class=\"" + cssClass + "\")", | |||
"element index 2 was not a spacer (class=\"" + cssClass + "\")", | |||
cssClass.contains("-spacer")); | |||
// Scroll to last DOM row (Row 20). The exact position varies a bit | |||
// depending on the browser. | |||
int scrollTo = 172; | |||
while (scrollTo < 176) { | |||
scrollVerticallyTo(scrollTo); | |||
Thread.sleep(500); | |||
// if spacer is still the third (index: 2) body element, i.e. not | |||
// enough scrolling to re-purpose any rows, scroll a bit further | |||
spacer = getChild(tbody, 2); | |||
cssClass = spacer.getAttribute("class"); | |||
if (cssClass.contains("-spacer")) { | |||
++scrollTo; | |||
} else { | |||
break; | |||
} | |||
} | |||
if (getChild(tbody, 20).getText().startsWith("Row 22:")) { | |||
// Some browsers scroll too much, spacer should be out of visual | |||
// range | |||
assertNull("Element found where there should be none", | |||
getChild(tbody, 21)); | |||
} else { | |||
// second row should still be within DOM but the first row out of | |||
// it, so the spacer should be the second element within the body | |||
// (index: 1) | |||
spacer = getChild(tbody, 1); | |||
cssClass = spacer.getAttribute("class"); | |||
assertTrue("element index 1 was not a spacer (class=\"" + cssClass | |||
+ "\")", cssClass.contains("-spacer")); | |||
} | |||
} | |||
@Test |
@@ -1,12 +1,16 @@ | |||
package com.vaadin.tests.components.treegrid; | |||
import static org.hamcrest.Matchers.greaterThanOrEqualTo; | |||
import static org.hamcrest.Matchers.not; | |||
import static org.hamcrest.number.IsCloseTo.closeTo; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertNotEquals; | |||
import static org.junit.Assert.assertNotNull; | |||
import static org.junit.Assert.assertThat; | |||
import java.util.List; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.openqa.selenium.StaleElementReferenceException; | |||
import org.openqa.selenium.WebDriver; | |||
@@ -15,6 +19,7 @@ import org.openqa.selenium.support.ui.ExpectedCondition; | |||
import org.openqa.selenium.support.ui.ExpectedConditions; | |||
import com.vaadin.testbench.By; | |||
import com.vaadin.testbench.TestBenchElement; | |||
import com.vaadin.testbench.elements.ButtonElement; | |||
import com.vaadin.testbench.elements.TreeGridElement; | |||
import com.vaadin.tests.tb3.MultiBrowserTest; | |||
@@ -33,13 +38,18 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
private static final String HIDE_DETAILS = "hideDetails"; | |||
private static final String ADD_GRID = "addGrid"; | |||
private static final String SCROLL_TO_55 = "scrollTo55"; | |||
private static final String SCROLL_TO_3055 = "scrollTo3055"; | |||
private static final String SCROLL_TO_END = "scrollToEnd"; | |||
private static final String SCROLL_TO_START = "scrollToStart"; | |||
private static final String TOGGLE_15 = "toggle15"; | |||
private static final String TOGGLE_3000 = "toggle3000"; | |||
private TreeGridElement treeGrid; | |||
private int expectedSpacerHeight = 0; | |||
private int expectedRowHeight = 0; | |||
private ExpectedCondition<Boolean> expectedConditionDetails(final int root, | |||
final int branch, final int leaf) { | |||
final Integer branch, final Integer leaf) { | |||
return new ExpectedCondition<Boolean>() { | |||
@Override | |||
public Boolean apply(WebDriver arg0) { | |||
@@ -49,9 +59,14 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
@Override | |||
public String toString() { | |||
// waiting for... | |||
if (leaf != null) { | |||
return String.format( | |||
"Leaf %s/%s/%s details row contents to be found", | |||
root, branch, leaf); | |||
} | |||
return String.format( | |||
"Leaf %s/%s/%s details row contents to be found", root, | |||
branch, leaf); | |||
"Branch %s/%s details row contents to be found", root, | |||
branch); | |||
} | |||
}; | |||
} | |||
@@ -87,6 +102,11 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
return null; | |||
} | |||
private WebElement getRow(int index) { | |||
return treeGrid.getBody().findElements(By.className("v-treegrid-row")) | |||
.get(index); | |||
} | |||
private void ensureExpectedSpacerHeightSet() { | |||
if (expectedSpacerHeight == 0) { | |||
expectedSpacerHeight = treeGrid | |||
@@ -94,9 +114,6 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
.getHeight(); | |||
assertThat((double) expectedSpacerHeight, closeTo(27d, 2d)); | |||
} | |||
if (expectedRowHeight == 0) { | |||
expectedRowHeight = treeGrid.getRow(0).getSize().getHeight(); | |||
} | |||
} | |||
private void assertSpacerCount(int expectedSpacerCount) { | |||
@@ -129,10 +146,6 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
previousSpacer = spacer; | |||
continue; | |||
} | |||
if (spacer.getLocation().y == 0) { | |||
// FIXME: find out why there are cases like this out of order | |||
continue; | |||
} | |||
// -1 should be enough, but increased tolerance to -3 for FireFox | |||
// and IE11 since a few pixels' discrepancy isn't relevant for this | |||
// fix | |||
@@ -148,16 +161,24 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
treeGrid.findElements(By.className(CLASSNAME_ERROR)).size()); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetails_toggleOneTwice_hideAll() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
private void addGrid() { | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
expectedRowHeight = treeGrid.getRow(0).getSize().getHeight(); | |||
} | |||
@Before | |||
public void before() { | |||
openTestURL(); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetails_toggleOneTwice_hideAll() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
waitUntil(expectedConditionDetails(0, 0, 0)); | |||
ensureExpectedSpacerHeightSet(); | |||
@@ -205,14 +226,9 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
@Test | |||
public void expandAllOpenAllInitialDetails_toggleAll() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
addGrid(); | |||
waitUntil(expectedConditionDetails(0, 0, 0)); | |||
ensureExpectedSpacerHeightSet(); | |||
@@ -231,9 +247,9 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
assertSpacerPositions(); | |||
// FIXME: TreeGrid fails to update cache correctly when you expand all | |||
// and after a long, long wait you end up with 3321 open details rows | |||
// and row 63/8/0 in view instead of 95 and 0/0/0 as expected. | |||
// WaitUntil timeouts by then. | |||
// and triggers client-side exceptions for rows that fall outside of the | |||
// cache because they try to extend the cache with a range that isn't | |||
// connected to the cached range | |||
if (true) {// remove this block after fixed | |||
return; | |||
} | |||
@@ -241,7 +257,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
// State should have returned to what it was before collapsing. | |||
waitUntil(expectedConditionDetails(0, 0, 0)); | |||
waitUntil(expectedConditionDetails(0, 0, 0), 15); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
@@ -251,13 +267,8 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
@Test | |||
public void expandAllOpenNoInitialDetails_showSeveral_toggleOneByOne() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
addGrid(); | |||
// open details for several rows, leave one out from the hierarchy that | |||
// is to be collapsed | |||
@@ -309,18 +320,30 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
assertNoErrors(); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetails_hideOne() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
// check the position of a row | |||
int oldY = treeGrid.getCell(2, 0).getLocation().getY(); | |||
// hide the spacer from previous row | |||
treeGrid.getCell(1, 0).click(); | |||
// ensure the investigated row moved | |||
assertNotEquals(oldY, treeGrid.getCell(2, 0).getLocation().getY()); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetailsScrolled_toggleOne_hideAll() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
addGrid(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
$(ButtonElement.class).id(SCROLL_TO_55).click(); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
waitUntil(expectedConditionDetails(1, 2, 0)); | |||
ensureExpectedSpacerHeightSet(); | |||
int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER)) | |||
@@ -333,8 +356,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0))); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// FIXME: gives 128, not 90 as expected | |||
// assertSpacerCount(spacerCount); | |||
assertSpacerCount(spacerCount); | |||
treeGrid.expandWithClick(50); | |||
@@ -342,8 +364,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
waitUntil(expectedConditionDetails(1, 2, 0)); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// FIXME: gives 131, not 90 as expected | |||
// assertSpacerCount(spacerCount); | |||
assertSpacerCount(spacerCount); | |||
// test that repeating the toggle still doesn't change anything | |||
@@ -352,16 +373,14 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0))); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// FIXME: gives 128, not 90 as expected | |||
// assertSpacerCount(spacerCount); | |||
assertSpacerCount(spacerCount); | |||
treeGrid.expandWithClick(50); | |||
waitUntil(expectedConditionDetails(1, 2, 0)); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// FIXME: gives 131, not 90 as expected | |||
// assertSpacerCount(spacerCount); | |||
assertSpacerCount(spacerCount); | |||
// test that hiding all still won't break things | |||
@@ -373,17 +392,13 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
@Test | |||
public void expandAllOpenAllInitialDetailsScrolled_toggleAll() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
addGrid(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
$(ButtonElement.class).id(SCROLL_TO_55).click(); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
waitUntil(expectedConditionDetails(1, 1, 0)); | |||
waitUntil(expectedConditionDetails(1, 3, 0)); | |||
ensureExpectedSpacerHeightSet(); | |||
int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER)) | |||
@@ -400,7 +415,8 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// FIXME: collapsing too many rows after scrolling still causes a chaos | |||
// FIXME: collapsing and expanding too many rows after scrolling still | |||
// fails to reset to the same state | |||
if (true) { // remove this block after fixed | |||
return; | |||
} | |||
@@ -408,7 +424,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
// State should have returned to what it was before collapsing. | |||
waitUntil(expectedConditionDetails(1, 1, 0)); | |||
waitUntil(expectedConditionDetails(1, 3, 0)); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
@@ -418,27 +434,24 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
@Test | |||
public void expandAllOpenNoInitialDetailsScrolled_showSeveral_toggleOneByOne() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
addGrid(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
$(ButtonElement.class).id(SCROLL_TO_55).click(); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
assertSpacerCount(0); | |||
// open details for several rows, leave one out from the hierarchy that | |||
// is to be collapsed | |||
treeGrid.getCell(50, 0).click(); | |||
treeGrid.getCell(51, 0).click(); | |||
treeGrid.getCell(52, 0).click(); | |||
// no click for cell (53, 0) | |||
treeGrid.getCell(54, 0).click(); | |||
treeGrid.getCell(55, 0).click(); | |||
treeGrid.getCell(56, 0).click(); | |||
treeGrid.getCell(57, 0).click(); | |||
treeGrid.getCell(58, 0).click(); | |||
treeGrid.getCell(50, 0).click(); // Branch 1/2 | |||
treeGrid.getCell(51, 0).click(); // Leaf 1/2/0 | |||
treeGrid.getCell(52, 0).click(); // Leaf 1/2/1 | |||
// no click for cell (53, 0) // Leaf 1/2/2 | |||
treeGrid.getCell(54, 0).click(); // Branch 1/3 | |||
treeGrid.getCell(55, 0).click(); // Leaf 1/3/0 | |||
treeGrid.getCell(56, 0).click(); // Leaf 1/3/1 | |||
treeGrid.getCell(57, 0).click(); // Leaf 1/3/2 | |||
treeGrid.getCell(58, 0).click(); // Branch 1/4 | |||
int spacerCount = 8; | |||
waitUntil(expectedConditionDetails(1, 2, 0)); | |||
@@ -507,4 +520,351 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest { | |||
assertNoErrors(); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetailsScrolled_hideOne() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
$(ButtonElement.class).id(SCROLL_TO_55).click(); | |||
// check the position of a row | |||
int oldY = treeGrid.getCell(52, 0).getLocation().getY(); | |||
// hide the spacer from previous row | |||
treeGrid.getCell(51, 0).click(); | |||
// ensure the investigated row moved | |||
assertNotEquals(oldY, treeGrid.getCell(52, 0).getLocation().getY()); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetailsScrolledFar_toggleOne_hideAll() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
$(ButtonElement.class).id(SCROLL_TO_3055).click(); | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
ensureExpectedSpacerHeightSet(); | |||
int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER)) | |||
.size(); | |||
assertSpacerPositions(); | |||
treeGrid.collapseWithClick(3051); | |||
// collapsing one shouldn't affect spacer count, just update the cache | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0))); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
assertSpacerCount(spacerCount); | |||
treeGrid.expandWithClick(3051); | |||
// expanding back shouldn't affect spacer count, just update the cache | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
assertSpacerCount(spacerCount); | |||
// test that repeating the toggle still doesn't change anything | |||
treeGrid.collapseWithClick(3051); | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 4, 0))); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
assertSpacerCount(spacerCount); | |||
treeGrid.expandWithClick(3051); | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
assertSpacerCount(spacerCount); | |||
// test that hiding all still won't break things | |||
$(ButtonElement.class).id(HIDE_DETAILS).click(); | |||
waitForElementNotPresent(By.className(CLASSNAME_SPACER)); | |||
assertNoErrors(); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetailsScrolledFar_toggleAll() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
$(ButtonElement.class).id(SCROLL_TO_3055).click(); | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
ensureExpectedSpacerHeightSet(); | |||
int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER)) | |||
.size(); | |||
assertSpacerPositions(); | |||
$(ButtonElement.class).id(COLLAPSE_ALL).click(); | |||
waitForElementNotPresent(By.className(CLASSNAME_LEAF)); | |||
// There should still be a full cache's worth of details rows open, | |||
// just not the same rows than before collapsing all. | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// FIXME: collapsing and expanding too many rows after scrolling still | |||
// fails to reset to the same state | |||
if (true) { // remove this block after fixed | |||
return; | |||
} | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
// State should have returned to what it was before collapsing. | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
assertNoErrors(); | |||
} | |||
@Test | |||
public void expandAllOpenNoInitialDetailsScrolledFar_showSeveral_toggleOneByOne() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
addGrid(); | |||
$(ButtonElement.class).id(SCROLL_TO_3055).click(); | |||
assertSpacerCount(0); | |||
// open details for several rows, leave one out from the hierarchy that | |||
// is to be collapsed | |||
treeGrid.getCell(3051, 0).click(); // Branch 74/4 | |||
treeGrid.getCell(3052, 0).click(); // Leaf 74/4/0 | |||
treeGrid.getCell(3053, 0).click(); // Leaf 74/4/1 | |||
// no click for cell (3054, 0) // Leaf 74/4/2 | |||
treeGrid.getCell(3055, 0).click(); // Branch 74/5 | |||
treeGrid.getCell(3056, 0).click(); // Leaf 74/5/0 | |||
treeGrid.getCell(3057, 0).click(); // Leaf 74/5/1 | |||
treeGrid.getCell(3058, 0).click(); // Leaf 74/5/2 | |||
treeGrid.getCell(3059, 0).click(); // Branch 74/6 | |||
int spacerCount = 8; | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
assertSpacerCount(spacerCount); | |||
ensureExpectedSpacerHeightSet(); | |||
assertSpacerPositions(); | |||
// toggle the branch with partially open details rows | |||
treeGrid.collapseWithClick(3051); | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 4, 0))); | |||
assertSpacerCount(spacerCount - 2); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
treeGrid.expandWithClick(3051); | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// toggle the branch with fully open details rows | |||
treeGrid.collapseWithClick(3055); | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 5, 0))); | |||
assertSpacerCount(spacerCount - 3); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
treeGrid.expandWithClick(3055); | |||
waitUntil(expectedConditionDetails(74, 5, 0)); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
// repeat both toggles to ensure still no errors | |||
treeGrid.collapseWithClick(3051); | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 4, 0))); | |||
assertSpacerCount(spacerCount - 2); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
treeGrid.expandWithClick(3051); | |||
waitUntil(expectedConditionDetails(74, 4, 0)); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
treeGrid.collapseWithClick(3055); | |||
waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 5, 0))); | |||
assertSpacerCount(spacerCount - 3); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
treeGrid.expandWithClick(3055); | |||
waitUntil(expectedConditionDetails(74, 5, 0)); | |||
assertSpacerCount(spacerCount); | |||
assertSpacerHeights(); | |||
assertSpacerPositions(); | |||
assertNoErrors(); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetailsScrolledFar_hideOne() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
$(ButtonElement.class).id(SCROLL_TO_3055).click(); | |||
// check the position of a row | |||
int oldY = treeGrid.getCell(3052, 0).getLocation().getY(); | |||
// hide the spacer from previous row | |||
treeGrid.getCell(3051, 0).click(); | |||
// ensure the investigated row moved | |||
assertNotEquals(oldY, treeGrid.getCell(52, 0).getLocation().getY()); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetails_checkScrollPositions() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
addGrid(); | |||
TestBenchElement tableWrapper = treeGrid.getTableWrapper(); | |||
$(ButtonElement.class).id(SCROLL_TO_55).click(); | |||
waitUntil(expectedConditionDetails(1, 3, 0)); | |||
WebElement detailsRow = getSpacer(1, 3, 0); | |||
assertNotNull("Spacer for row 55 not found", detailsRow); | |||
int wrapperY = tableWrapper.getLocation().getY(); | |||
int wrapperHeight = tableWrapper.getSize().getHeight(); | |||
int detailsY = detailsRow.getLocation().getY(); | |||
int detailsHeight = detailsRow.getSize().getHeight(); | |||
assertThat("Scroll to 55 didn't scroll as expected", | |||
(double) detailsY + detailsHeight, | |||
closeTo(wrapperY + wrapperHeight, 1d)); | |||
$(ButtonElement.class).id(SCROLL_TO_3055).click(); | |||
waitUntil(expectedConditionDetails(74, 5, null)); | |||
detailsRow = getSpacer(74, 5, null); | |||
assertNotNull("Spacer for row 3055 not found", detailsRow); | |||
detailsY = detailsRow.getLocation().getY(); | |||
detailsHeight = detailsRow.getSize().getHeight(); | |||
assertThat("Scroll to 3055 didn't scroll as expected", | |||
(double) detailsY + detailsHeight, | |||
closeTo(wrapperY + wrapperHeight, 1d)); | |||
$(ButtonElement.class).id(SCROLL_TO_END).click(); | |||
waitUntil(expectedConditionDetails(99, 9, 2)); | |||
detailsRow = getSpacer(99, 9, 2); | |||
assertNotNull("Spacer for last row not found", detailsRow); | |||
detailsY = detailsRow.getLocation().getY(); | |||
detailsHeight = detailsRow.getSize().getHeight(); | |||
// the layout jumps sometimes, check again | |||
wrapperY = tableWrapper.getLocation().getY(); | |||
wrapperHeight = tableWrapper.getSize().getHeight(); | |||
assertThat("Scroll to end didn't scroll as expected", | |||
(double) detailsY + detailsHeight, | |||
closeTo(wrapperY + wrapperHeight, 1d)); | |||
$(ButtonElement.class).id(SCROLL_TO_START).click(); | |||
waitUntil(expectedConditionDetails(0, 0, 0)); | |||
WebElement firstRow = getRow(0); | |||
TestBenchElement header = treeGrid.getHeader(); | |||
assertThat("Scroll to start didn't scroll as expected", | |||
(double) firstRow.getLocation().getY(), | |||
closeTo(wrapperY + header.getSize().getHeight(), 1d)); | |||
} | |||
@Test | |||
public void expandAllOpenNoInitialDetails_testToggleScrolling() { | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
addGrid(); | |||
TestBenchElement tableWrapper = treeGrid.getTableWrapper(); | |||
int wrapperY = tableWrapper.getLocation().getY(); | |||
WebElement firstRow = getRow(0); | |||
int firstRowY = firstRow.getLocation().getY(); | |||
TestBenchElement header = treeGrid.getHeader(); | |||
int headerHeight = header.getSize().getHeight(); | |||
assertThat("Unexpected initial scroll position", (double) firstRowY, | |||
closeTo(wrapperY + headerHeight, 1d)); | |||
$(ButtonElement.class).id(TOGGLE_15).click(); | |||
firstRowY = firstRow.getLocation().getY(); | |||
assertThat( | |||
"Toggling row 15's details open should have caused scrolling", | |||
(double) firstRowY, not(closeTo(wrapperY + headerHeight, 1d))); | |||
$(ButtonElement.class).id(SCROLL_TO_START).click(); | |||
firstRowY = firstRow.getLocation().getY(); | |||
assertThat("Scrolling to start failed", (double) firstRowY, | |||
closeTo(wrapperY + headerHeight, 1d)); | |||
$(ButtonElement.class).id(TOGGLE_15).click(); | |||
firstRowY = firstRow.getLocation().getY(); | |||
assertThat( | |||
"Toggling row 15's details closed should not have caused scrolling", | |||
(double) firstRowY, closeTo(wrapperY + headerHeight, 1d)); | |||
$(ButtonElement.class).id(TOGGLE_3000).click(); | |||
firstRowY = firstRow.getLocation().getY(); | |||
assertThat( | |||
"Toggling row 3000's details open should not have caused scrolling", | |||
(double) firstRowY, closeTo(wrapperY + headerHeight, 1d)); | |||
$(ButtonElement.class).id(SCROLL_TO_55).click(); | |||
WebElement row = getRow(0); | |||
assertNotEquals("First row should be out of visual range", firstRowY, | |||
row); | |||
$(ButtonElement.class).id(TOGGLE_15).click(); | |||
assertEquals( | |||
"Toggling row 15's details open should not have caused scrolling " | |||
+ "when row 15 is outside of visual range", | |||
row, getRow(0)); | |||
} | |||
} |
@@ -3,6 +3,7 @@ package com.vaadin.tests.components.treegrid; | |||
import static org.hamcrest.Matchers.greaterThanOrEqualTo; | |||
import static org.hamcrest.number.IsCloseTo.closeTo; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertNotEquals; | |||
import static org.junit.Assert.assertNotNull; | |||
import static org.junit.Assert.assertThat; | |||
@@ -292,4 +293,25 @@ public class TreeGridDetailsManagerTest extends MultiBrowserTest { | |||
assertNoErrors(); | |||
} | |||
@Test | |||
public void expandAllOpenAllInitialDetails_hideOne() { | |||
openTestURL(); | |||
$(ButtonElement.class).id(EXPAND_ALL).click(); | |||
$(ButtonElement.class).id(SHOW_DETAILS).click(); | |||
$(ButtonElement.class).id(ADD_GRID).click(); | |||
waitForElementPresent(By.className(CLASSNAME_TREEGRID)); | |||
treeGrid = $(TreeGridElement.class).first(); | |||
// check the position of a row | |||
int oldY = treeGrid.getCell(2, 0).getLocation().getY(); | |||
// hide the spacer from previous row | |||
treeGrid.getCell(1, 0).click(); | |||
// ensure the investigated row moved | |||
assertNotEquals(oldY, treeGrid.getCell(2, 0).getLocation().getY()); | |||
} | |||
} |