diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridColumnScrollPage.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridColumnScrollPage.java new file mode 100644 index 00000000000..ca47ec568f6 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/GridColumnScrollPage.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2025 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.flow.component.grid.it; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.flow.component.grid.ColumnRendering; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.Grid.Column; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +/** + * Test page for Grid column scrolling functionality. + */ +@Route("vaadin-grid/grid-column-scroll") +public class GridColumnScrollPage extends Div { + + public GridColumnScrollPage() { + Grid grid = new Grid<>(); + + // Set width to ensure not all columns are visible at once + grid.setWidth("800px"); + + // Enable lazy column rendering + grid.setColumnRendering(ColumnRendering.LAZY); + + // Add many columns to ensure horizontal scrolling is needed + for (int i = 0; i < 20; i++) { + final int columnIndex = i; + Column column = grid + .addColumn(item -> item.getValue(columnIndex)) + .setHeader("Column " + i).setWidth("150px"); + column.setKey("col" + i); + } + + // Add test data + List items = new ArrayList<>(); + for (int row = 0; row < 100; row++) { + items.add(new TestItem(row)); + } + grid.setItems(items); + + add(grid); + } + + public static class TestItem { + private final int rowIndex; + + public TestItem(int rowIndex) { + this.rowIndex = rowIndex; + } + + public String getValue(int columnIndex) { + return "R" + rowIndex + "C" + columnIndex; + } + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridScrollToColumnIT.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridScrollToColumnIT.java new file mode 100644 index 00000000000..7da47d83943 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/GridScrollToColumnIT.java @@ -0,0 +1,159 @@ +/* + * Copyright 2000-2025 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.flow.component.grid.it; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.grid.testbench.GridColumnElement; +import com.vaadin.flow.component.grid.testbench.GridElement; +import com.vaadin.flow.component.grid.testbench.GridTHTDElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.tests.AbstractComponentIT; + +/** + * Test for scrolling to columns in Grid, especially important when + * columnRendering is set to lazy. + */ +@TestPath("vaadin-grid/grid-column-scroll") +public class GridScrollToColumnIT extends AbstractComponentIT { + + private GridElement grid; + + @Before + public void init() { + open(); + waitForDevServer(); + grid = $(GridElement.class).first(); + } + + @Test + public void testScrollToColumn_bringsColumnIntoView() { + // Assuming grid has many columns and some are out of view + List columns = grid.getAllColumns(); + Assert.assertTrue("Grid should have more than 5 columns for this test", + columns.size() > 5); + + // Get a column that might be out of view + GridColumnElement lastColumn = columns.get(columns.size() - 1); + + // Scroll to the last column + grid.scrollToColumn(lastColumn); + + // Verify the column is now in view + Assert.assertTrue("Column should be in view after scrolling", + grid.isColumnInView(lastColumn)); + } + + @Test + public void testScrollToColumnByIndex_bringsColumnIntoView() { + List columns = grid.getVisibleColumns(); + Assert.assertTrue("Grid should have more than 5 columns for this test", + columns.size() > 5); + + int lastIndex = columns.size() - 1; + + // Scroll to the last column by index + grid.scrollToColumn(lastIndex); + + // Verify we can get the cell without it being null + GridTHTDElement cell = grid.getCell(0, lastIndex); + Assert.assertNotNull( + "Cell should not be null after scrolling to column", cell); + } + + @Test + public void testIsColumnInView_detectsVisibleColumns() { + List columns = grid.getVisibleColumns(); + Assert.assertFalse("Grid should have columns", columns.isEmpty()); + + // First column should typically be in view + GridColumnElement firstColumn = columns.get(0); + Assert.assertTrue("First column should be in view", + grid.isColumnInView(firstColumn)); + } + + @Test + public void testGetCell_automaticallyScrollsColumnIntoView() { + List columns = grid.getAllColumns(); + Assert.assertTrue("Grid should have more than 10 columns for this test", + columns.size() > 10); + + // Try to get a cell from a column that's likely out of view + GridColumnElement farColumn = columns.get(columns.size() - 1); + + // This should automatically scroll the column into view + GridTHTDElement cell = grid.getCell(0, farColumn); + + Assert.assertNotNull( + "Cell should not be null after automatic scrolling", cell); + Assert.assertTrue("Column should be in view after getCell", + grid.isColumnInView(farColumn)); + } + + @Test + public void testScrollToColumn_withLazyColumnRendering() { + // This test specifically addresses the issue mentioned in #8046 + // When columnRendering is lazy, cells out of view return null + + List columns = grid.getAllColumns(); + Assert.assertTrue("Grid should have more than 5 columns for this test", + columns.size() > 5); + + // First, try to get cells from the first row + for (int i = 0; i < columns.size(); i++) { + GridColumnElement column = columns.get(i); + + // Ensure column is scrolled into view + if (!grid.isColumnInView(column)) { + grid.scrollToColumn(column); + } + + // Now the cell should be accessible + GridTHTDElement cell = grid.getRow(0).getCell(column); + Assert.assertNotNull("Cell at column " + i + + " should not be null after scrolling", cell); + } + } + + @Test + public void testScrollToColumn_multipleScrolls() { + List columns = grid.getVisibleColumns(); + Assert.assertTrue("Grid should have more than 10 columns for this test", + columns.size() > 10); + + // Scroll to last column + GridColumnElement lastColumn = columns.get(columns.size() - 1); + grid.scrollToColumn(lastColumn); + Assert.assertTrue("Last column should be in view", + grid.isColumnInView(lastColumn)); + + // Scroll back to first column + GridColumnElement firstColumn = columns.get(0); + grid.scrollToColumn(firstColumn); + Assert.assertTrue("First column should be in view", + grid.isColumnInView(firstColumn)); + + // Scroll to middle column + GridColumnElement middleColumn = columns.get(columns.size() / 2); + grid.scrollToColumn(middleColumn); + Assert.assertTrue("Middle column should be in view", + grid.isColumnInView(middleColumn)); + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java b/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java index 055cc1f44d2..5ba0bdb8127 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java +++ b/vaadin-grid-flow-parent/vaadin-grid-testbench/src/main/java/com/vaadin/flow/component/grid/testbench/GridElement.java @@ -60,6 +60,66 @@ void scrollToRowByFlatIndex(int rowFlatIndex) { waitUntilLoadingFinished(); } + /** + * Scrolls horizontally to bring the specified column into view. This is + * useful when working with grids that have lazy column rendering. + * + * @param column + * the column to scroll into view + */ + public void scrollToColumn(GridColumnElement column) { + // Scroll to column using its sizer cell offset + executeScript(""" + const grid = arguments[0]; + const columnId = arguments[1]; + const columns = grid._getColumns(); + const col = columns.find(c => c.__generatedTbId === columnId); + if (col && col._sizerCell) { + grid.$.table.scrollLeft = col._sizerCell.offsetLeft; + grid.__updateColumnsBodyContentHidden(); + } + """, this, column.get__generatedId()); + + // Wait for rendering to complete + waitUntilLoadingFinished(); + + // Wait for column to be in view + waitUntil(driver -> isColumnInView(column)); + + } + + /** + * Scrolls horizontally to bring the column at the specified index into + * view. This is useful when working with grids that have lazy column + * rendering. + * + * @param columnIndex + * the index of the column to scroll into view + */ + public void scrollToColumn(int columnIndex) { + GridColumnElement column = getVisibleColumns().get(columnIndex); + scrollToColumn(column); + } + + /** + * Checks if the specified column is currently in the visible viewport. + * + * @param column + * the column to check + * @return {@code true} if the column is visible, {@code false} otherwise + */ + public boolean isColumnInView(GridColumnElement column) { + Boolean result = (Boolean) executeScript( + """ + const grid = arguments[0]; + const columnId = arguments[1]; + const col = grid._getColumns().find(c => c.__generatedTbId === columnId); + return col ? grid.__isColumnInViewport(col) : false; + """, + this, column.get__generatedId()); + return Boolean.TRUE.equals(result); + } + /** * Gets the page size used when fetching data. * @@ -115,7 +175,7 @@ public GridTHTDElement getCell(int rowIndex, int colIndex) { /** * Gets the grid cell for the given row and column. *

- * Automatically scrolls the given row into view + * Automatically scrolls the given row and column into view * * @param rowIndex * the row index @@ -128,6 +188,11 @@ public GridTHTDElement getCell(int rowIndex, GridColumnElement column) { scrollToRowByFlatIndex(rowIndex); } + // Also scroll column into view if needed + if (!isColumnInView(column)) { + scrollToColumn(column); + } + GridTRElement row = getRow(rowIndex); return row.getCell(column); } @@ -145,13 +210,15 @@ public GridTHTDElement getCell(int rowIndex, GridColumnElement column) { public GridTHTDElement getCell(String contents) throws NoSuchElementException { - String script = "const grid = arguments[0];" - + "const contents = arguments[1];" - + "const rowsInDom = Array.from(arguments[0].$.items.children);" - + "var tds = [];" - + "rowsInDom.forEach(function(tr) { Array.from(tr.children).forEach(function(td) { tds.push(td);})});" - + "const matches = tds.filter(function(td) { return td._content.textContent == contents});" - + "return matches.length ? matches[0] : null;"; + String script = """ + const grid = arguments[0]; + const contents = arguments[1]; + const rowsInDom = Array.from(arguments[0].$.items.children); + var tds = []; + rowsInDom.forEach(function(tr) { Array.from(tr.children).forEach(function(td) { tds.push(td);})}); + const matches = tds.filter(function(td) { return td._content.textContent == contents}); + return matches.length ? matches[0] : null; + """; TestBenchElement td = (TestBenchElement) executeScript(script, this, contents); if (td == null) { @@ -213,11 +280,13 @@ public List getRows(int firstRowIndex, int lastRowIndex) + (rowCount - 1) + " but were " + firstRowIndex + " and " + lastRowIndex); } - String script = "var grid = arguments[0];" - + "var firstRowIndex = arguments[1];" - + "var lastRowIndex = arguments[2];" - + "var rowsInDom = grid._getRenderedRows();" - + "return Array.from(rowsInDom).filter((row) => { return row.index >= firstRowIndex && row.index <= lastRowIndex;});"; + String script = """ + var grid = arguments[0]; + var firstRowIndex = arguments[1]; + var lastRowIndex = arguments[2]; + var rowsInDom = grid._getRenderedRows(); + return Array.from(rowsInDom).filter((row) => { return row.index >= firstRowIndex && row.index <= lastRowIndex;}); + """; Object rows = executeScript(script, this, firstRowIndex, lastRowIndex); if (rows != null) { return ((ArrayList) rows).stream().map( @@ -286,18 +355,20 @@ public List getAllColumns() { } protected void generatedColumnIdsIfNeeded() { - String generateIds = "const grid = arguments[0];" - + "if (!grid.__generatedTbId) {"// - + " grid.__generatedTbId = 1;"// - + "}" // - + "grid._getColumns().forEach(function(column) {" - + " if (!column.__generatedTbId) {" - + " column.__generatedTbId = grid.__generatedTbId++;" // - + " }" // - + "});"; + String generateIds = """ + const grid = arguments[0]; + if (!grid.__generatedTbId) { + grid.__generatedTbId = 1; + } + grid._getColumns().forEach(function(column) { + if (!column.__generatedTbId) { + column.__generatedTbId = grid.__generatedTbId++; + } + }); + return grid._getColumns().length; + """; executeScript(generateIds, this); - // } /**