@@ -52,6 +52,8 @@ import com.vaadin.client.ui.AbstractComponentConnector; | |||
import com.vaadin.client.ui.AbstractHasComponentsConnector; | |||
import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; | |||
import com.vaadin.client.ui.SimpleManagedLayout; | |||
import com.vaadin.client.ui.layout.ElementResizeEvent; | |||
import com.vaadin.client.ui.layout.ElementResizeListener; | |||
import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent; | |||
import com.vaadin.client.widget.escalator.events.RowHeightChangedHandler; | |||
import com.vaadin.client.widget.grid.CellReference; | |||
@@ -496,17 +498,67 @@ public class GridConnector extends AbstractHasComponentsConnector | |||
private final Map<String, ComponentConnector> idToDetailsMap = new HashMap<String, ComponentConnector>(); | |||
private final Map<String, Integer> idToRowIndex = new HashMap<String, Integer>(); | |||
private final Map<Element, ScheduledCommand> elementToResizeCommand = new HashMap<Element, Scheduler.ScheduledCommand>(); | |||
private final ElementResizeListener detailsRowResizeListener = new ElementResizeListener() { | |||
@Override | |||
public void onElementResize(ElementResizeEvent e) { | |||
if (elementToResizeCommand.containsKey(e.getElement())) { | |||
Scheduler.get().scheduleFinally( | |||
elementToResizeCommand.get(e.getElement())); | |||
} | |||
} | |||
}; | |||
/* calculated when the first details row is opened */ | |||
private Double spacerCellBorderHeights = null; | |||
@Override | |||
public Widget getDetails(int rowIndex) { | |||
String id = getId(rowIndex); | |||
if (id == null) { | |||
if (id == null || !hasDetailsOpen(rowIndex)) { | |||
return null; | |||
} | |||
ComponentConnector componentConnector = idToDetailsMap.get(id); | |||
idToRowIndex.put(id, rowIndex); | |||
return componentConnector.getWidget(); | |||
Widget widget = componentConnector.getWidget(); | |||
getLayoutManager().addElementResizeListener(widget.getElement(), | |||
detailsRowResizeListener); | |||
elementToResizeCommand.put(widget.getElement(), | |||
createResizeCommand(rowIndex, widget.getElement())); | |||
return widget; | |||
} | |||
private ScheduledCommand createResizeCommand(final int rowIndex, | |||
final Element element) { | |||
return new ScheduledCommand() { | |||
@Override | |||
public void execute() { | |||
// It should not be possible to get here without calculating | |||
// the spacerCellBorderHeights or without having the details | |||
// row open, nor for this command to be triggered while | |||
// layout is running, but it's safer to check anyway. | |||
if (spacerCellBorderHeights != null | |||
&& !getLayoutManager().isLayoutRunning() | |||
&& hasDetailsOpen(rowIndex)) { | |||
double height = getLayoutManager().getOuterHeightDouble( | |||
element) + spacerCellBorderHeights; | |||
getWidget().setDetailsHeight(rowIndex, height); | |||
} | |||
} | |||
}; | |||
} | |||
private boolean hasDetailsOpen(int rowIndex) { | |||
JsonObject row = getWidget().getDataSource().getRow(rowIndex); | |||
if (row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)) { | |||
String id = row.getString(GridState.JSONKEY_DETAILS_VISIBLE); | |||
return id != null && !id.isEmpty(); | |||
} | |||
return false; | |||
} | |||
@Override | |||
@@ -519,8 +571,16 @@ public class GridConnector extends AbstractHasComponentsConnector | |||
getLayoutManager().setNeedsMeasureRecursively(componentConnector); | |||
getLayoutManager().layoutNow(); | |||
return getLayoutManager().getOuterHeightDouble( | |||
componentConnector.getWidget().getElement()); | |||
Element element = componentConnector.getWidget().getElement(); | |||
if (spacerCellBorderHeights == null) { | |||
// If theme is changed, new details generator is created from | |||
// scratch, so this value doesn't need to be updated elsewhere. | |||
spacerCellBorderHeights = WidgetUtil | |||
.getBorderTopAndBottomThickness( | |||
element.getParentElement()); | |||
} | |||
return getLayoutManager().getOuterHeightDouble(element); | |||
} | |||
/** | |||
@@ -568,6 +628,11 @@ public class GridConnector extends AbstractHasComponentsConnector | |||
} | |||
for (String id : removedDetails) { | |||
Element element = idToDetailsMap.get(id).getWidget() | |||
.getElement(); | |||
elementToResizeCommand.remove(element); | |||
getLayoutManager().removeElementResizeListener(element, | |||
detailsRowResizeListener); | |||
idToDetailsMap.remove(id); | |||
idToRowIndex.remove(id); | |||
} |
@@ -9117,6 +9117,19 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
return visibleDetails.contains(Integer.valueOf(rowIndex)); | |||
} | |||
/** | |||
* Update details row height. | |||
* | |||
* @since | |||
* @param rowIndex | |||
* the index of the row for which to update details height | |||
* @param height | |||
* new height of the details row | |||
*/ | |||
public void setDetailsHeight(int rowIndex, double height) { | |||
escalator.getBody().setSpacer(rowIndex, height); | |||
} | |||
/** | |||
* Requests that the column widths should be recalculated. | |||
* <p> |
@@ -0,0 +1,135 @@ | |||
/* | |||
* Copyright 2000-2017 Vaadin Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not | |||
* use this file except in compliance with the License. You may obtain a copy of | |||
* the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |||
* License for the specific language governing permissions and limitations under | |||
* the License. | |||
*/ | |||
package com.vaadin.tests.components.grid; | |||
import com.vaadin.event.ItemClickEvent; | |||
import com.vaadin.event.ItemClickEvent.ItemClickListener; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.tests.components.AbstractTestUI; | |||
import com.vaadin.ui.Button; | |||
import com.vaadin.ui.Button.ClickEvent; | |||
import com.vaadin.ui.Component; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.Grid.DetailsGenerator; | |||
import com.vaadin.ui.Grid.RowReference; | |||
import com.vaadin.ui.Label; | |||
import com.vaadin.ui.VerticalLayout; | |||
import com.vaadin.ui.themes.Reindeer; | |||
import com.vaadin.ui.themes.ValoTheme; | |||
/** | |||
* Tests that details row resizes along with the contents properly. | |||
* | |||
* @author Vaadin Ltd | |||
*/ | |||
public class GridLayoutDetailsRowResize extends AbstractTestUI { | |||
@Override | |||
protected void setup(VaadinRequest request) { | |||
final Grid grid = new Grid(); | |||
grid.setSizeFull(); | |||
grid.addColumn("name", String.class); | |||
grid.addColumn("born", Integer.class); | |||
grid.addRow("Nicolaus Copernicus", 1543); | |||
grid.addRow("Galileo Galilei", 1564); | |||
grid.addRow("Johannes Kepler", 1571); | |||
addComponent(grid); | |||
grid.setDetailsGenerator(new DetailsGenerator() { | |||
@Override | |||
public Component getDetails(final RowReference rowReference) { | |||
final VerticalLayout detailsLayout = new VerticalLayout(); | |||
detailsLayout.setId("details"); | |||
detailsLayout.setSizeFull(); | |||
detailsLayout.setHeightUndefined(); | |||
final Label lbl1 = new Label("test1"); | |||
lbl1.setId("lbl1"); | |||
lbl1.setWidth("200px"); | |||
detailsLayout.addComponent(lbl1); | |||
final Label lbl2 = new Label("test2"); | |||
lbl2.setId("lbl2"); | |||
detailsLayout.addComponent(lbl2); | |||
final Label lbl3 = new Label("test3"); | |||
lbl3.setId("lbl3"); | |||
detailsLayout.addComponent(lbl3); | |||
final Label lbl4 = new Label("test4"); | |||
lbl4.setId("lbl4"); | |||
lbl4.setVisible(false); | |||
detailsLayout.addComponent(lbl4); | |||
final Button button = new Button("Toggle visibility", | |||
new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
lbl4.setVisible(!lbl4.isVisible()); | |||
} | |||
}); | |||
button.setId("btn"); | |||
detailsLayout.addComponent(button); | |||
return detailsLayout; | |||
} | |||
}); | |||
grid.addItemClickListener(new ItemClickListener() { | |||
@Override | |||
public void itemClick(final ItemClickEvent event) { | |||
final Object itemId = event.getItemId(); | |||
grid.setDetailsVisible(itemId, !grid.isDetailsVisible(itemId)); | |||
} | |||
}); | |||
addComponent(new Button("Toggle theme", new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
if (ValoTheme.THEME_NAME.equals(getUI().getTheme())) { | |||
getUI().setTheme(Reindeer.THEME_NAME); | |||
} else { | |||
getUI().setTheme(ValoTheme.THEME_NAME); | |||
} | |||
} | |||
})); | |||
addComponent(new Button("Open details", new Button.ClickListener() { | |||
@Override | |||
public void buttonClick(ClickEvent event) { | |||
for (Object itemId : grid.getContainerDataSource() | |||
.getItemIds()) { | |||
grid.setDetailsVisible(itemId, true); | |||
} | |||
} | |||
})); | |||
} | |||
@Override | |||
protected String getTestDescription() { | |||
return "Detail row should be correctly resized when its contents change."; | |||
} | |||
@Override | |||
protected Integer getTicketNumber() { | |||
return 7341; | |||
} | |||
} |
@@ -0,0 +1,160 @@ | |||
/* | |||
* Copyright 2000-2017 Vaadin Ltd. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not | |||
* use this file except in compliance with the License. You may obtain a copy of | |||
* the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |||
* License for the specific language governing permissions and limitations under | |||
* the License. | |||
*/ | |||
package com.vaadin.tests.components.grid; | |||
import static org.hamcrest.MatcherAssert.assertThat; | |||
import static org.hamcrest.core.Is.is; | |||
import static org.hamcrest.number.IsCloseTo.closeTo; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Map.Entry; | |||
import java.util.TreeMap; | |||
import org.junit.Test; | |||
import org.openqa.selenium.By; | |||
import org.openqa.selenium.WebElement; | |||
import com.vaadin.testbench.elements.ButtonElement; | |||
import com.vaadin.testbench.elements.GridElement; | |||
import com.vaadin.testbench.elements.LabelElement; | |||
import com.vaadin.testbench.elements.VerticalLayoutElement; | |||
import com.vaadin.testbench.parallel.TestCategory; | |||
import com.vaadin.tests.tb3.MultiBrowserTest; | |||
/** | |||
* Tests that details row resizes along with the contents properly. | |||
* | |||
* @author Vaadin Ltd | |||
*/ | |||
@TestCategory("grid") | |||
public class GridLayoutDetailsRowResizeTest extends MultiBrowserTest { | |||
@Test | |||
public void testLabelHeights() { | |||
openTestURL(); | |||
waitForElementPresent(By.className("v-grid")); | |||
GridElement grid = $(GridElement.class).first(); | |||
grid.getCell(2, 0).click(); | |||
waitForElementPresent(By.id("lbl2")); | |||
VerticalLayoutElement layout = $(VerticalLayoutElement.class) | |||
.id("details"); | |||
int layoutHeight = layout.getSize().height; | |||
ButtonElement button = $(ButtonElement.class).id("btn"); | |||
int buttonHeight = button.getSize().height; | |||
// height should be divided equally | |||
double expectedLabelHeight = (layoutHeight - buttonHeight) / 3; | |||
assertLabelHeight("lbl1", expectedLabelHeight); | |||
assertLabelHeight("lbl2", expectedLabelHeight); | |||
assertLabelHeight("lbl3", expectedLabelHeight); | |||
assertDetailsRowHeight(layoutHeight); | |||
// ensure fourth label isn't present yet | |||
assertElementNotPresent(By.id("lbl4")); | |||
button.click(); | |||
waitForElementPresent(By.id("lbl4")); | |||
// get layout height after the new label has been added | |||
layoutHeight = layout.getSize().height; | |||
expectedLabelHeight = (layoutHeight - buttonHeight) / 4; | |||
assertLabelHeight("lbl1", expectedLabelHeight); | |||
assertLabelHeight("lbl2", expectedLabelHeight); | |||
assertLabelHeight("lbl3", expectedLabelHeight); | |||
assertLabelHeight("lbl4", expectedLabelHeight); | |||
assertDetailsRowHeight(layoutHeight); | |||
} | |||
@Test | |||
public void testMultipleDetailsRows() { | |||
openTestURL(); | |||
waitForElementPresent(By.className("v-grid")); | |||
ButtonElement detailsButton = $(ButtonElement.class) | |||
.caption("Open details").first(); | |||
detailsButton.click(); | |||
waitForElementPresent(By.id("lbl2")); | |||
List<ButtonElement> buttons = $(ButtonElement.class) | |||
.caption("Toggle visibility").all(); | |||
assertThat("Unexpected amount of details rows.", buttons.size(), is(3)); | |||
Map<ButtonElement, Integer> positions = new LinkedHashMap<ButtonElement, Integer>(); | |||
Map<Integer, ButtonElement> ordered = new TreeMap<Integer, ButtonElement>(); | |||
for (ButtonElement button : buttons) { | |||
positions.put(button, button.getLocation().getY()); | |||
ordered.put(button.getLocation().getY(), button); | |||
} | |||
int labelHeight = 0; | |||
for (LabelElement label : $(LabelElement.class).all()) { | |||
if ("test1".equals(label.getText())) { | |||
labelHeight = label.getSize().height; | |||
} | |||
} | |||
// toggle the contents | |||
for (ButtonElement button : buttons) { | |||
button.click(); | |||
} | |||
int i = 0; | |||
for (Entry<Integer, ButtonElement> entry : ordered.entrySet()) { | |||
++i; | |||
ButtonElement button = entry.getValue(); | |||
assertThat( | |||
String.format("Unexpected button position: details row %s.", | |||
i), | |||
(double) button.getLocation().getY(), | |||
closeTo(positions.get(button) + (i * labelHeight), 1d)); | |||
} | |||
// toggle the contents | |||
for (ButtonElement button : buttons) { | |||
button.click(); | |||
} | |||
// assert original positions back | |||
for (ButtonElement button : buttons) { | |||
assertThat(String.format("Unexpected button position."), | |||
(double) button.getLocation().getY(), | |||
closeTo(positions.get(button), 1d)); | |||
} | |||
} | |||
private void assertLabelHeight(String id, double expectedHeight) { | |||
// 1px leeway for calculations | |||
assertThat("Unexpected label height.", | |||
(double) $(LabelElement.class).id(id).getSize().height, | |||
closeTo(expectedHeight, 1d)); | |||
} | |||
private void assertDetailsRowHeight(int layoutHeight) { | |||
// check that details row height matches layout height (1px leeway) | |||
WebElement detailsRow = findElement(By.className("v-grid-spacer")); | |||
assertThat("Unexpected details row height", (double) layoutHeight, | |||
closeTo(detailsRow.getSize().height, 1d)); | |||
} | |||
} |