Procházet zdrojové kódy

Sort DOM elements for better WAI-ARIA support (#13334)

Change-Id: I3fe6b2a8ad2b72b91db61135bd6505dcfa53034d
tags/7.4.0.alpha2
Henrik Paul před 10 roky
rodič
revize
61b04537b3

+ 174
- 6
client/src/com/vaadin/client/ui/grid/Escalator.java Zobrazit soubor

@@ -191,14 +191,16 @@ abstract class JsniWorkaround {
*/
protected JavaScriptObject touchEndFunction;

protected TouchHandlerBundle touchHandlerBundle;

protected JsniWorkaround(final Escalator escalator) {
scrollListenerFunction = createScrollListenerFunction(escalator);
mousewheelListenerFunction = createMousewheelListenerFunction(escalator);

final TouchHandlerBundle bundle = new TouchHandlerBundle(escalator);
touchStartFunction = bundle.getTouchStartHandler();
touchMoveFunction = bundle.getTouchMoveHandler();
touchEndFunction = bundle.getTouchEndHandler();
touchHandlerBundle = new TouchHandlerBundle(escalator);
touchStartFunction = touchHandlerBundle.getTouchStartHandler();
touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
touchEndFunction = touchHandlerBundle.getTouchEndHandler();
}

/**
@@ -431,6 +433,12 @@ public class Escalator extends Widget {
animationHandle = AnimationScheduler.get()
.requestAnimationFrame(mover, escalator.bodyElem);
event.getNativeEvent().preventDefault();

/*
* this initializes a correct timestamp, and also renders the
* first frame for added responsiveness.
*/
mover.execute(Duration.currentTimeMillis());
}

public void touchEnd(@SuppressWarnings("unused")
@@ -440,6 +448,7 @@ public class Escalator extends Widget {
if (touches == 0) {
escalator.scroller.handleFlickScroll(deltaX, deltaY,
lastTime);
escalator.body.domSorter.reschedule();
}
}
}
@@ -1902,6 +1911,8 @@ public class Escalator extends Widget {
* The order in which row elements are rendered visually in the browser,
* with the help of CSS tricks. Usually has nothing to do with the DOM
* order.
*
* @see #sortDomElements()
*/
private final LinkedList<Element> visualRowOrder = new LinkedList<Element>();

@@ -1939,6 +1950,60 @@ public class Escalator extends Widget {
setTopRowLogicalIndex(topRowLogicalIndex + diff);
}

private class DeferredDomSorter {
private static final int SORT_DELAY_MILLIS = 50;

// as it happens, 3 frames = 50ms @ 60fps.
private static final int REQUIRED_FRAMES_PASSED = 3;

private final AnimationCallback frameCounter = new AnimationCallback() {
@Override
public void execute(double timestamp) {
framesPassed++;
boolean domWasSorted = sortIfConditionsMet();
if (!domWasSorted) {
animationHandle = AnimationScheduler.get()
.requestAnimationFrame(this);
}
}
};

private int framesPassed;
private double startTime;
private AnimationHandle animationHandle;

public void reschedule() {
resetConditions();
animationHandle = AnimationScheduler.get()
.requestAnimationFrame(frameCounter);
}

private boolean sortIfConditionsMet() {
boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS;
boolean conditionsMet = enoughFramesHavePassed
&& enoughTimeHasPassed;

if (conditionsMet) {
resetConditions();
sortDomElements();
}

return conditionsMet;
}

private void resetConditions() {
if (animationHandle != null) {
animationHandle.cancel();
animationHandle = null;
}
startTime = Duration.currentTimeMillis();
framesPassed = 0;
}
}

private DeferredDomSorter domSorter = new DeferredDomSorter();

public BodyRowContainer(final Element bodyElement) {
super(bodyElement);
}
@@ -2086,6 +2151,15 @@ public class Escalator extends Widget {

if (rowsWereMoved) {
fireRowVisibilityChangeEvent();

if (scroller.touchHandlerBundle.touches == 0) {
/*
* this will never be called on touch scrolling. That is
* handled separately and explicitly by
* TouchHandlerBundle.touchEnd();
*/
domSorter.reschedule();
}
}
}

@@ -2187,6 +2261,7 @@ public class Escalator extends Widget {
}

fireRowVisibilityChangeEvent();
sortDomElements();
}
return addedRows;
}
@@ -2733,6 +2808,9 @@ public class Escalator extends Widget {
logicalTargetIndex);
}
}

fireRowVisibilityChangeEvent();
sortDomElements();
}

updateTopRowLogicalIndex(-removedAbove.length());
@@ -2742,8 +2820,6 @@ public class Escalator extends Widget {
* or it won't work correctly (due to setScrollTop invocation)
*/
scroller.recalculateScrollbarsForVirtualViewport();

fireRowVisibilityChangeEvent();
}

private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
@@ -3160,6 +3236,98 @@ public class Escalator extends Widget {

Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
}

/**
* Sorts the rows in the DOM to correspond to the visual order.
*
* @see #visualRowOrder
*/
private void sortDomElements() {
final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
Profiler.enter(profilingName);

/*
* Focus is lost from an element if that DOM element is (or any of
* its parents are) removed from the document. Therefore, we sort
* everything around that row instead.
*/
final Element activeRow = getEscalatorRowWithFocus();

if (activeRow != null) {
assert activeRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
assert visualRowOrder.contains(activeRow) : "Trying to sort around a row that doesn't exist in visualRowOrder.";
}

/*
* Two cases handled simultaneously:
*
* 1) No focus on rows. We iterate visualRowOrder backwards, and
* take the respective element in the DOM, and place it as the first
* child in the body element. Then we take the next-to-last from
* visualRowOrder, and put that first, pushing the previous row as
* the second child. And so on...
*
* 2) Focus on some row within Escalator body. Again, we iterate
* visualRowOrder backwards. This time, we use the focused row as a
* pivot: Instead of placing rows from the bottom of visualRowOrder
* and placing it first, we place it underneath the focused row.
* Once we hit the focused row, we don't move it (to not reset
* focus) but change sorting mode. After that, we place all rows as
* the first child.
*/

/*
* If we have a focused row, start in the mode where we put
* everything underneath that row. Otherwise, all rows are placed as
* first child.
*/
boolean insertFirst = (activeRow == null);

final ListIterator<Element> i = visualRowOrder
.listIterator(visualRowOrder.size());
while (i.hasPrevious()) {
Element tr = i.previous();

if (tr == activeRow) {
insertFirst = true;
} else if (insertFirst) {
root.insertFirst(tr);
} else {
root.insertAfter(tr, activeRow);
}
}

Profiler.leave(profilingName);
}

/**
* Get the escalator row that has focus.
*
* @return The escalator row that contains a focused DOM element, or
* <code>null</code> if focus is outside of a body row.
*/
private Element getEscalatorRowWithFocus() {
Element activeRow = null;

final Element activeElement = Util.getFocusedElement();

if (root.isOrHasChild(activeElement)) {
Element e = activeElement;

while (e != null && e != root) {
/*
* You never know if there's several tables embedded in a
* cell... We'll take the deepest one.
*/
if (e.getTagName().equalsIgnoreCase("TR")) {
activeRow = e;
}
e = e.getParentElement();
}
}

return activeRow;
}
}

private class ColumnConfigurationImpl implements ColumnConfiguration {

+ 12
- 1
uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java Zobrazit soubor

@@ -33,10 +33,15 @@ import com.vaadin.ui.TextField;
@Widgetset(TestingWidgetSet.NAME)
public class BasicEscalator extends AbstractTestUI {
public static final String ESCALATOR = "escalator";

public static final String INSERT_ROWS_OFFSET = "iro";
public static final String INSERT_ROWS_AMOUNT = "ira";
public static final String INSERT_ROWS_BUTTON = "irb";

public static final String REMOVE_ROWS_OFFSET = "rro";
public static final String REMOVE_ROWS_AMOUNT = "rra";
public static final String REMOVE_ROWS_BUTTON = "rrb";

private final Random random = new Random();

@Override
@@ -71,8 +76,10 @@ public class BasicEscalator extends AbstractTestUI {

final Layout removeRowsLayout = new HorizontalLayout();
final TextField removeRowsOffset = new TextField();
removeRowsOffset.setId(REMOVE_ROWS_OFFSET);
removeRowsLayout.addComponent(removeRowsOffset);
final TextField removeRowsAmount = new TextField();
removeRowsAmount.setId(REMOVE_ROWS_AMOUNT);
removeRowsLayout.addComponent(removeRowsAmount);
removeRowsLayout.addComponent(new Button("remove rows",
new Button.ClickListener() {
@@ -84,7 +91,11 @@ public class BasicEscalator extends AbstractTestUI {
.getValue());
grid.removeRows(offset, amount);
}
}));
}) {
{
setId(REMOVE_ROWS_BUTTON);
}
});
addComponent(removeRowsLayout);

final Layout insertColumnsLayout = new HorizontalLayout();

+ 156
- 11
uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java Zobrazit soubor

@@ -17,11 +17,17 @@ package com.vaadin.tests.components.grid;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
@@ -32,6 +38,11 @@ import com.vaadin.tests.tb3.MultiBrowserTest;
@TestCategory("grid")
public class BasicEscalatorTest extends MultiBrowserTest {

private static final int SLEEP = 300;

private static final Pattern ROW_PATTERN = Pattern
.compile("Row (\\d+): \\d+,\\d+");

@Test
public void testInitialState() throws Exception {
openTestURL();
@@ -55,7 +66,7 @@ public class BasicEscalatorTest extends MultiBrowserTest {
*/
Thread.sleep(100);

scrollEscalatorVerticallyTo(1000);
setScrollTop(getVerticalScrollbar(), 1000);
assertBodyCellWithContentIsFound("Row 50: 0,50");
}

@@ -70,7 +81,7 @@ public class BasicEscalatorTest extends MultiBrowserTest {
Thread.sleep(100);

// scroll to bottom
scrollEscalatorVerticallyTo(100000000);
setScrollTop(getVerticalScrollbar(), 100000000);

/*
* this test does not test DOM reordering, therefore we don't rely on
@@ -124,11 +135,6 @@ public class BasicEscalatorTest extends MultiBrowserTest {
getDriver().get(testUrl);
}

private void scrollEscalatorVerticallyTo(double px) {
executeScript("arguments[0].scrollTop = " + px,
getGridVerticalScrollbar());
}

private Object executeScript(String script, WebElement element) {
@SuppressWarnings("hiding")
final WebDriver driver = getDriver();
@@ -142,9 +148,148 @@ public class BasicEscalatorTest extends MultiBrowserTest {
}
}

private WebElement getGridVerticalScrollbar() {
return getDriver()
.findElement(
By.xpath("//div[contains(@class, \"v-escalator-scroller-vertical\")]"));
@Test
public void domIsInitiallySorted() throws Exception {
openTestURL();

final List<WebElement> rows = getBodyRows();
assertTrue("no body rows found", !rows.isEmpty());
for (int i = 0; i < rows.size(); i++) {
String text = rows.get(i).getText();
String expected = "Row " + i;
assertTrue("Expected \"" + expected + "...\" but was " + text,
text.startsWith(expected));
}
}

@Test
public void domIsSortedAfterInsert() throws Exception {
openTestURL();

final int rowsToInsert = 5;
final int offset = 5;
insertRows(offset, rowsToInsert);

final List<WebElement> rows = getBodyRows();
int i = 0;
for (; i < offset + rowsToInsert; i++) {
final String expectedStart = "Row " + i;
final String text = rows.get(i).getText();
assertTrue("Expected \"" + expectedStart + "...\" but was " + text,
text.startsWith(expectedStart));
}

for (; i < rows.size(); i++) {
final String expectedStart = "Row " + (i - rowsToInsert);
final String text = rows.get(i).getText();
assertTrue("(post insert) Expected \"" + expectedStart
+ "...\" but was " + text, text.startsWith(expectedStart));
}
}

@Test
public void domIsSortedAfterRemove() throws Exception {
openTestURL();

final int rowsToRemove = 5;
final int offset = 5;
removeRows(offset, rowsToRemove);

final List<WebElement> rows = getBodyRows();
int i = 0;
for (; i < offset; i++) {
final String expectedStart = "Row " + i;
final String text = rows.get(i).getText();
assertTrue("Expected " + expectedStart + "... but was " + text,
text.startsWith(expectedStart));
}

/*
* We check only up to 10, since after that, the indices are again
* reset, because new rows have been generated. The row numbers that
* they are given depends on the widget size, and it's too fragile to
* rely on some special assumptions on that.
*/
for (; i < 10; i++) {
final String expectedStart = "Row " + (i + rowsToRemove);
final String text = rows.get(i).getText();
assertTrue("(post remove) Expected " + expectedStart
+ "... but was " + text, text.startsWith(expectedStart));
}
}

@Test
public void domIsSortedAfterScroll() throws Exception {
openTestURL();
setScrollTop(getVerticalScrollbar(), 500);

/*
* Let the DOM reorder itself.
*
* TODO TestBench currently doesn't know when Grid's DOM structure is
* stable. There are some plans regarding implementing support for this,
* so this test case can (should) be modified once that's implemented.
*/
sleep(SLEEP);

List<WebElement> rows = getBodyRows();
int firstRowNumber = parseFirstRowNumber(rows);

for (int i = 0; i < rows.size(); i++) {
final String expectedStart = "Row " + (i + firstRowNumber);
final String text = rows.get(i).getText();
assertTrue("(post remove) Expected " + expectedStart
+ "... but was " + text, text.startsWith(expectedStart));
}
}

private static int parseFirstRowNumber(List<WebElement> rows)
throws NumberFormatException {
final WebElement firstRow = rows.get(0);
final String firstRowText = firstRow.getText();
final Matcher matcher = ROW_PATTERN.matcher(firstRowText);
if (!matcher.find()) {
fail("could not find " + ROW_PATTERN.pattern() + " in \""
+ firstRowText + "\"");
}
final String number = matcher.group(1);
return Integer.parseInt(number);
}

private void insertRows(final int offset, final int amount) {
final WebElement offsetInput = vaadinElementById(BasicEscalator.INSERT_ROWS_OFFSET);
offsetInput.sendKeys(String.valueOf(offset), Keys.RETURN);

final WebElement amountInput = vaadinElementById(BasicEscalator.INSERT_ROWS_AMOUNT);
amountInput.sendKeys(String.valueOf(amount), Keys.RETURN);

final WebElement button = vaadinElementById(BasicEscalator.INSERT_ROWS_BUTTON);
button.click();
}

private void removeRows(final int offset, final int amount) {
final WebElement offsetInput = vaadinElementById(BasicEscalator.REMOVE_ROWS_OFFSET);
offsetInput.sendKeys(String.valueOf(offset), Keys.RETURN);

final WebElement amountInput = vaadinElementById(BasicEscalator.REMOVE_ROWS_AMOUNT);
amountInput.sendKeys(String.valueOf(amount), Keys.RETURN);

final WebElement button = vaadinElementById(BasicEscalator.REMOVE_ROWS_BUTTON);
button.click();
}

private void setScrollTop(WebElement element, long px) {
executeScript("arguments[0].scrollTop = " + px, element);
}

private List<WebElement> getBodyRows() {
return getDriver().findElements(By.xpath("//tbody/tr/td[1]"));
}

private WebElement getVerticalScrollbar() {
return getDriver().findElement(
By.xpath("//div["
+ "contains(@class, 'v-escalator-scroller-vertical')"
+ "]"));
}
}

Načítá se…
Zrušit
Uložit