- Initial implementation erroneously assumed that ScrollDestination.START would only be used for scrolling up and ScrollDestination.END for scrolling down. That's obviously not what they are for, otherwise everyone would be using ScrollDestination.ANY. - Moved actual scrolling to within the helper method that originally only calculated the new scroll position. Parent method became too long otherwise. Fixes #11706tags/8.10.0.alpha1
case START: | case START: | ||||
// target row at the top of the viewport, include buffer | // target row at the top of the viewport, include buffer | ||||
// row if there is room for one | // row if there is room for one | ||||
logicalTargetIndex = Math | |||||
newTopRowLogicalIndex = Math | |||||
.max(firstVisibleIndexIfScrollingUp - 1, 0); | .max(firstVisibleIndexIfScrollingUp - 1, 0); | ||||
newTopRowLogicalIndex = logicalTargetIndex; | |||||
if (getVisibleRowRange().contains(newTopRowLogicalIndex)) { | |||||
logicalTargetIndex = oldTopRowLogicalIndex | |||||
+ visualRangeLength; | |||||
} else { | |||||
logicalTargetIndex = newTopRowLogicalIndex; | |||||
} | |||||
break; | break; | ||||
default: | default: | ||||
throw new IllegalArgumentException( | |||||
"Internal: Unsupported ScrollDestination: " | |||||
+ destination.name()); | |||||
String msg = "Internal: Unsupported ScrollDestination: "; | |||||
throw new IllegalArgumentException(msg + destination.name()); | |||||
} | } | ||||
// adjust visual range if necessary | // adjust visual range if necessary | ||||
boolean rowsWereMoved = newTopRowLogicalIndex != oldTopRowLogicalIndex; | boolean rowsWereMoved = newTopRowLogicalIndex != oldTopRowLogicalIndex; | ||||
// update scroll position if necessary | // update scroll position if necessary | ||||
double scrollTop = calculateScrollPositionForScrollToRowSpacerOrBoth( | |||||
targetRowIndex, destination, padding, scrollType); | |||||
if (scrollTop != getScrollTop()) { | |||||
setScrollTop(scrollTop); | |||||
setBodyScrollPosition(tBodyScrollLeft, scrollTop); | |||||
} | |||||
adjustScrollPositionForScrollToRowSpacerOrBoth(targetRowIndex, | |||||
destination, padding, scrollType); | |||||
if (rowsWereMoved) { | if (rowsWereMoved) { | ||||
fireRowVisibilityChangeEvent(); | fireRowVisibilityChangeEvent(); | ||||
} | } | ||||
/** | /** | ||||
* Calculates scroll position for | |||||
* Adjusts scroll position for | |||||
* {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)}, | * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)}, | ||||
* reuse at your own peril. | * reuse at your own peril. | ||||
* | * | ||||
* @param destination | * @param destination | ||||
* @param padding | * @param padding | ||||
* @param scrollType | * @param scrollType | ||||
* @return expected scroll position | |||||
*/ | */ | ||||
private double calculateScrollPositionForScrollToRowSpacerOrBoth( | |||||
private void adjustScrollPositionForScrollToRowSpacerOrBoth( | |||||
int targetRowIndex, ScrollDestination destination, | int targetRowIndex, ScrollDestination destination, | ||||
double padding, ScrollType scrollType) { | double padding, ScrollType scrollType) { | ||||
/* | /* | ||||
- sectionHeight + padding; | - sectionHeight + padding; | ||||
// ensure that we don't overshoot beyond bottom | // ensure that we don't overshoot beyond bottom | ||||
scrollTop = Math.min(scrollTop, | scrollTop = Math.min(scrollTop, | ||||
getRowTop(getRowCount() - 1) + getDefaultRowHeight() | |||||
+ spacerContainer | |||||
.getSpacerHeight(getRowCount() - 1) | |||||
- sectionHeight); | |||||
getRowTop(getRowCount()) - sectionHeight); | |||||
// if padding is set we want to overshoot or undershoot, | // if padding is set we want to overshoot or undershoot, | ||||
// otherwise make sure the top of the row or spacer is | // otherwise make sure the top of the row or spacer is | ||||
// in view | // in view | ||||
case END: | case END: | ||||
if (ScrollType.ROW.equals(scrollType) | if (ScrollType.ROW.equals(scrollType) | ||||
&& rowTop + getDefaultRowHeight() | && rowTop + getDefaultRowHeight() | ||||
+ padding > getScrollTop() + sectionHeight) { | |||||
// within visual range but end of row below the viewport | |||||
// or not enough padding, shift a little | |||||
+ padding != getScrollTop() + sectionHeight) { | |||||
// row should be at the bottom of the viewport | |||||
scrollTop = rowTop + getDefaultRowHeight() - sectionHeight | scrollTop = rowTop + getDefaultRowHeight() - sectionHeight | ||||
+ padding; | + padding; | ||||
// ensure that we don't overshoot beyond bottom | |||||
scrollTop = Math.min(scrollTop, | |||||
getRowTop(getRowCount() - 1) + getDefaultRowHeight() | |||||
+ spacerContainer | |||||
.getSpacerHeight(getRowCount() - 1) | |||||
- sectionHeight); | |||||
} else if (rowTop + getDefaultRowHeight() + spacerHeight | } else if (rowTop + getDefaultRowHeight() + spacerHeight | ||||
+ padding > getScrollTop() + sectionHeight) { | |||||
// within visual range but end of spacer below the viewport | |||||
// or not enough padding, shift a little | |||||
+ padding != getScrollTop() + sectionHeight) { | |||||
// spacer should be at the bottom of the viewport | |||||
scrollTop = rowTop + getDefaultRowHeight() + spacerHeight | scrollTop = rowTop + getDefaultRowHeight() + spacerHeight | ||||
- sectionHeight + padding; | - sectionHeight + padding; | ||||
// ensure that we don't overshoot beyond bottom | |||||
scrollTop = Math.min(scrollTop, | |||||
getRowTop(getRowCount()) - sectionHeight); | |||||
} else { | } else { | ||||
// we are fine where we are | // we are fine where we are | ||||
scrollTop = getScrollTop(); | scrollTop = getScrollTop(); | ||||
+ (spacerHeight / 2.0); | + (spacerHeight / 2.0); | ||||
} | } | ||||
scrollTop = center - Math.ceil(sectionHeight / 2.0); | scrollTop = center - Math.ceil(sectionHeight / 2.0); | ||||
// ensure that we don't overshoot beyond bottom | |||||
scrollTop = Math.min(scrollTop, | |||||
getRowTop(getRowCount() - 1) + getDefaultRowHeight() | |||||
+ spacerContainer | |||||
.getSpacerHeight(getRowCount() - 1) | |||||
- sectionHeight); | |||||
// ensure that we don't overshoot beyond top | |||||
scrollTop = Math.max(0, scrollTop); | |||||
break; | break; | ||||
case START: | case START: | ||||
if (!ScrollType.SPACER.equals(scrollType) | if (!ScrollType.SPACER.equals(scrollType) | ||||
&& Math.max(rowTop - padding, 0) < getScrollTop()) { | |||||
// row top above the viewport or not enough padding, shift a | |||||
// little | |||||
&& Math.max(rowTop - padding, 0) != getScrollTop()) { | |||||
// row should be at the top of the viewport | |||||
scrollTop = Math.max(rowTop - padding, 0); | scrollTop = Math.max(rowTop - padding, 0); | ||||
} else if (ScrollType.SPACER.equals(scrollType) | } else if (ScrollType.SPACER.equals(scrollType) | ||||
&& Math.max(rowTop + getDefaultRowHeight() - padding, | && Math.max(rowTop + getDefaultRowHeight() - padding, | ||||
0) < getScrollTop()) { | |||||
// spacer top above the viewport or not enough padding, | |||||
// shift a little | |||||
0) != getScrollTop()) { | |||||
// spacer should be at the top of the viewport | |||||
scrollTop = Math | scrollTop = Math | ||||
.max(rowTop + getDefaultRowHeight() - padding, 0); | .max(rowTop + getDefaultRowHeight() - padding, 0); | ||||
} else { | } else { | ||||
default: | default: | ||||
scrollTop = getScrollTop(); | scrollTop = getScrollTop(); | ||||
} | } | ||||
return scrollTop; | |||||
// ensure that we don't overshoot beyond bottom | |||||
scrollTop = Math.min(scrollTop, | |||||
getRowTop(getRowCount()) - sectionHeight); | |||||
// ensure that we don't overshoot beyond top | |||||
scrollTop = Math.max(0, scrollTop); | |||||
if (scrollTop != getScrollTop()) { | |||||
setScrollTop(scrollTop); | |||||
setBodyScrollPosition(tBodyScrollLeft, scrollTop); | |||||
} | |||||
} | } | ||||
@Override | @Override |
package com.vaadin.tests.components.grid; | |||||
import java.util.Arrays; | |||||
import java.util.stream.IntStream; | |||||
import com.vaadin.server.VaadinRequest; | |||||
import com.vaadin.shared.ui.grid.ScrollDestination; | |||||
import com.vaadin.tests.components.AbstractTestUI; | |||||
import com.vaadin.ui.Button; | |||||
import com.vaadin.ui.Grid; | |||||
import com.vaadin.ui.NativeSelect; | |||||
import com.vaadin.ui.TextField; | |||||
public class GridScrollDestination extends AbstractTestUI { | |||||
@Override | |||||
protected void setup(VaadinRequest request) { | |||||
Grid<Integer> grid = new Grid<>(); | |||||
grid.addColumn(Integer::intValue).setCaption("number"); | |||||
grid.setItems(IntStream.range(0, 100).boxed()); | |||||
TextField tf = new TextField("row index", "50"); | |||||
NativeSelect<ScrollDestination> ns = new NativeSelect<>( | |||||
"scroll destination", | |||||
Arrays.asList(ScrollDestination.values())); | |||||
ns.setValue(ScrollDestination.ANY); | |||||
Button button = new Button("Scroll to above row index", (event) -> { | |||||
int rowIndex = Integer.parseInt(tf.getValue()); | |||||
grid.scrollTo(rowIndex, ns.getValue()); | |||||
}); | |||||
addComponents(tf, ns, button, grid); | |||||
} | |||||
} |
package com.vaadin.tests.components.grid; | |||||
import static org.hamcrest.number.IsCloseTo.closeTo; | |||||
import static org.junit.Assert.assertEquals; | |||||
import static org.junit.Assert.assertThat; | |||||
import java.util.List; | |||||
import org.junit.Test; | |||||
import org.openqa.selenium.WebElement; | |||||
import com.vaadin.shared.ui.grid.ScrollDestination; | |||||
import com.vaadin.testbench.By; | |||||
import com.vaadin.testbench.TestBenchElement; | |||||
import com.vaadin.testbench.elements.ButtonElement; | |||||
import com.vaadin.testbench.elements.GridElement; | |||||
import com.vaadin.testbench.elements.NativeSelectElement; | |||||
import com.vaadin.tests.tb3.SingleBrowserTest; | |||||
public class GridScrollDestinationTest extends SingleBrowserTest { | |||||
private ButtonElement button; | |||||
private GridElement grid; | |||||
private TestBenchElement header; | |||||
private TestBenchElement tableWrapper; | |||||
@Override | |||||
public void setup() throws Exception { | |||||
super.setup(); | |||||
openTestURL(); | |||||
button = $(ButtonElement.class).first(); | |||||
grid = $(GridElement.class).first(); | |||||
header = grid.getHeader(); | |||||
tableWrapper = grid.getTableWrapper(); | |||||
} | |||||
private void assertElementAtTop(WebElement row) { | |||||
assertThat((double) row.getLocation().getY(), closeTo( | |||||
header.getLocation().getY() + header.getSize().getHeight(), | |||||
1d)); | |||||
} | |||||
private void assertElementAtBottom(WebElement row) { | |||||
assertThat( | |||||
(double) row.getLocation().getY() + row.getSize().getHeight(), | |||||
closeTo((double) tableWrapper.getLocation().getY() | |||||
+ tableWrapper.getSize().getHeight(), 1d)); | |||||
} | |||||
private void assertElementAtMiddle(WebElement row) { | |||||
assertThat((double) row.getLocation() | |||||
.getY() + (row.getSize().getHeight() / 2), closeTo( | |||||
(double) tableWrapper.getLocation().getY() | |||||
+ header.getSize().getHeight() | |||||
+ ((tableWrapper.getSize().getHeight() | |||||
- header.getSize().getHeight()) / 2), | |||||
1d)); | |||||
} | |||||
@Test | |||||
public void destinationAny() { | |||||
// ScrollDestination.ANY selected by default | |||||
// scroll down | |||||
button.click(); | |||||
// expect the row at the bottom of the viewport | |||||
List<WebElement> rows = grid.getBody() | |||||
.findElements(By.className("v-grid-row")); | |||||
// last rendered row is a buffer row, inspect second to last | |||||
WebElement row = rows.get(rows.size() - 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtBottom(row); | |||||
// scroll to end | |||||
grid.scrollToRow((int) grid.getRowCount() - 1); | |||||
// ensure row 50 is out of visual range, first two rows are out of view | |||||
// and getText can't find the contents so inspect the third row | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(2); | |||||
assertGreater(row.getText() + " is not greater than 52", | |||||
Integer.valueOf(row.getText()), 52); | |||||
// scroll up | |||||
button.click(); | |||||
// expect the row at the top of the viewport | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
// first rendered row is a buffer row, inspect second | |||||
row = rows.get(1); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtTop(row); | |||||
// scroll up by a few rows | |||||
grid.scrollToRow(45); | |||||
// refresh row references | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(6); | |||||
assertEquals("50", row.getText()); | |||||
// scroll while already within viewport | |||||
button.click(); | |||||
// expect no change since the row is still within viewport | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(6); | |||||
assertEquals("50", row.getText()); | |||||
} | |||||
@Test | |||||
public void destinationEnd() { | |||||
$(NativeSelectElement.class).first() | |||||
.selectByText(ScrollDestination.END.name()); | |||||
// scroll down | |||||
button.click(); | |||||
// expect the row at the bottom of the viewport | |||||
List<WebElement> rows = grid.getBody() | |||||
.findElements(By.className("v-grid-row")); | |||||
// last rendered row is a buffer row, inspect second to last | |||||
WebElement row = rows.get(rows.size() - 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtBottom(row); | |||||
// scroll to end | |||||
grid.scrollToRow((int) grid.getRowCount() - 1); | |||||
// ensure row 50 is out of visual range, first two rows are out of view | |||||
// and getText can't find the contents so inspect the third row | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(2); | |||||
assertGreater(row.getText() + " is not greater than 52", | |||||
Integer.valueOf(row.getText()), 52); | |||||
// scroll up | |||||
button.click(); | |||||
// expect the row at the bottom of the viewport | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
// last rendered row is a buffer row, inspect second to last | |||||
row = rows.get(rows.size() - 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtBottom(row); | |||||
// scroll down by a few rows | |||||
grid.scrollToRow(55); | |||||
// refresh row references | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(rows.size() - 7); | |||||
assertEquals("50", row.getText()); | |||||
// scroll while already within viewport | |||||
button.click(); | |||||
// expect the row at the bottom of the viewport again | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(rows.size() - 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtBottom(row); | |||||
} | |||||
@Test | |||||
public void destinationStart() { | |||||
$(NativeSelectElement.class).first() | |||||
.selectByText(ScrollDestination.START.name()); | |||||
// scroll down | |||||
button.click(); | |||||
// expect the row at the top of the viewport | |||||
List<WebElement> rows = grid.getBody() | |||||
.findElements(By.className("v-grid-row")); | |||||
// first rendered row is a buffer row, inspect second | |||||
WebElement row = rows.get(1); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtTop(row); | |||||
// scroll to end | |||||
grid.scrollToRow((int) grid.getRowCount() - 1); | |||||
// ensure row 50 is out of visual range, first two rows are out of view | |||||
// and getText can't find the contents so inspect the third row | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(2); | |||||
assertGreater(row.getText() + " is not greater than 52", | |||||
Integer.valueOf(row.getText()), 52); | |||||
// scroll up | |||||
button.click(); | |||||
// expect the row at the top of the viewport | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
// first rendered row is a buffer row, inspect second | |||||
row = rows.get(1); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtTop(row); | |||||
// scroll up by a few rows | |||||
grid.scrollToRow(45); | |||||
// refresh row references | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(6); | |||||
assertEquals("50", row.getText()); | |||||
// scroll while already within viewport | |||||
button.click(); | |||||
// expect the row at the top of the viewport again | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(1); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtTop(row); | |||||
} | |||||
@Test | |||||
public void destinationMiddle() { | |||||
NativeSelectElement destinationSelect = $(NativeSelectElement.class) | |||||
.first(); | |||||
destinationSelect.selectByText(ScrollDestination.MIDDLE.name()); | |||||
// scroll down | |||||
button.click(); | |||||
// expect the row at the middle of the viewport | |||||
List<WebElement> rows = grid.getBody() | |||||
.findElements(By.className("v-grid-row")); | |||||
// inspect the middle row | |||||
WebElement row = rows.get(rows.size() / 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtMiddle(row); | |||||
// scroll to end | |||||
grid.scrollToRow((int) grid.getRowCount() - 1); | |||||
// ensure row 50 is out of visual range, first two rows are out of view | |||||
// and getText can't find the contents so inspect the third row | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(2); | |||||
assertGreater(row.getText() + " is not greater than 52", | |||||
Integer.valueOf(row.getText()), 52); | |||||
// scroll up | |||||
button.click(); | |||||
// expect the row at the middle of the viewport | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
// first rendered row is a buffer row, inspect second | |||||
row = rows.get(rows.size() / 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtMiddle(row); | |||||
// scroll down by a few rows | |||||
destinationSelect.selectByText(ScrollDestination.START.name()); | |||||
button.click(); | |||||
// refresh row references | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(1); | |||||
assertEquals("50", row.getText()); | |||||
// scroll while already within viewport | |||||
destinationSelect.selectByText(ScrollDestination.MIDDLE.name()); | |||||
button.click(); | |||||
// expect the row at the top of the viewport again | |||||
rows = grid.getBody().findElements(By.className("v-grid-row")); | |||||
row = rows.get(rows.size() / 2); | |||||
assertEquals("50", row.getText()); | |||||
assertElementAtMiddle(row); | |||||
} | |||||
} |